clojure-kieli - uef · 2016-01-14 · clojure is a functional lisp language that runs on the java...
TRANSCRIPT
Clojure-kieli
Mikael Puhakka
Pro gradu -tutkielma
Tietojenkäsittelytieteen laitos
Tietojenkäsittelytiede
Syyskuu 2015
ITÄ-SUOMEN YLIOPISTO, Luonnontieteiden ja metsätieteiden tiedekunta, JoensuuTietojenkäsittelytieteen laitosTietojenkäsittelytiede
Puhakka, Mikael Erkki Juhana: Clojure-kieliPro gradu -tutkielma, 59 s., ei liitteitä.Pro gradu -tutkielman ohjaaja: FT Matti NykänenSyyskuu 2015
Tiivistelmä:
Clojure on funktionaalinen lisp-perheen kieli, joka toimii Java Virtual Machinen päällä.Tutkimme Clojuren tuomia ominaisuuksia, ja kuinka ne edesauttavat jatkuvasti moni-mutkaistuvien sovellusprojektien yksinkertaistamisessa. Näihin kuuluvat tässä työssäesiteltävät ominaisuudet ja piirteet, kuten arvosemantiikka, funktionaalisuus, hallittumuutostenhallinta ja ilmaisuongelma ratkaisuineen. Ahkerasti suorittava kieli käyttääviivästettyä laskentaa tietorakenteissaan tuoden mukanaan laiskan laskennan element-tejä. Clojuren perustyypit ja tietorakenteet ovat pysyviä ja muuttumattomia, mikä saaaikaan arvosemantiikan. Nykyään paljon käytetyillä sovellusalustoilla käännetyt oh-jelmat ovat enemmän alustariippumattomia kuin ennen, ja ovat pitkälti myös yhtä no-peita kuin natiivikoodi optimoivien tulkkien ansiosta. Myös verkkoselaimissa enim-mäkseen toimiva JavaScript voidaan tulkita sovellusalustaksi. Clojuren toimiessa JavaVirtual Machinen päällä ja ClojureScriptin toimiessa JavaScriptin päällä kielellä onikäänsä nähden suuri vaikutusalue. Funktionaalisen ja oliopohjaisen paradigman mää-ritelmiä katselmoidaan kirjallisuudesta. Paradigmojen tavat ratkaista ongelmia eroavathuomattavasti toisistaan. Päädymme siihen tulokseen, että funktionaalinen ohjelmoin-tikieli painottaa funktioiden käyttöä perusrakennuspalasena, ja että Clojure on epäpuh-das funktionaalinen kieli. Ilmaisuongelma kysyy tietotyyppien ja niiden operaatioidenyhteensovittamisesta niin, että mukaan voidaan tuoda myöhemmin lisää tietotyyppe-jä ja operaatioita ilman, että entistä koodia tarvitsee muokata tai kääntää uudelleen.Erilaisista ratkaisuista mielekkäin on geneeristen funktioiden käyttö. Clojure tarjoaaerään ratkaisun ilmaisuongelmaan geneeristen funktioiden avulla. Sovellusprojektinja -koodin monimutkaisuutta voidaan hallita lukuisilla tekniikoilla. Muutamia tapo-ja käsitellään: sovellusaluekohtaisten kielten kehittäminen ongelmakuvauksiin, pysyväja oletuksena muuttumaton tieto tapana välttää hallitsematon muutoksenteko, ja kes-kitetty rajapinta koordinoiduille muutoksille silloin kun muutokset ovat välttämättö-miä. Clojure on näiden ominaisuuksien ansiosta hyvin varustautunut yksinkertaista-maan ratkaisuja.
Avainsanat: Clojure, lisp, sovellusalustat, funktionaalinen ohjelmointi, oliopohjainenohjelmointi
ACM-luokat (ACM Computing Classification System, 1998 version): D.2.3, D.3.2,D.3.3
i
UNIVERSITY OF EASTERN FINLAND, Faculty of Science and Forestry, JoensuuSchool of ComputingComputer Science
Puhakka, Mikael Erkki Juhana: The Clojure Programming LanguageMaster’s Thesis, 59 p., no appendicesSupervisor of the Master’s Thesis: PhD Matti NykänenSeptember 2015
Abstract:
Clojure is a functional Lisp language that runs on the Java Virtual Machine. In thiswork we study the features that Clojure brings to reduce ever-increasing complexityin software projects. These features include value semantics, functionality, controlledmutation and the expression problem with some solutions. An eager language, Clojureuses lazily evaluated data structures to simulate lazy evaluation. The values and datastructures in Clojure are immutable and persistent, which results in value semantics.Application platforms are greatly used in modern development. Modern software de-veloped on an application platform is more platform independent than before. Thanksto the matured optimizing interpreters, such software can be as fast as one compiled fornative hardware. JavaScript, ubiquitously running on modern web browsers, can alsobe considered as an application platform. Clojure running on Java Virtual Machine andClojureScript running on JavaScript make the languages have a great reach, despitethe young age of the projects. We will review some definitions of both functional andobject-oriented paradigms from literature. The methods of problem solving differ re-markably between the paradigms. A functional programming language emphasizes thefunction as a basic building block. We will conclude that Clojure is a nonpure functio-nal language. The expression problem asks whether data types and operations can befit together so that more types or operations can be added later on without modifyingor recompiling any existing code. Out of several solutions we study the most promi-nent: generic functions. Clojure provides a solution to the expression problem throughan application of generic functions. The complexity of software projects and code canbe managed with numerous techniques. We will cover some of them: domain speci-fic languages as a method to describe problems in languages tailored to the problemdomain; persistent and immutable-by-default data as a way to avoid uncontrolled mo-difications; and a centralized interface for coordinated changes when such a situationis truly warranted. With these features, Clojure is well-equipped to simplify softwaresolutions.
Keywords: Clojure, lisp, software platforms, functional programming, object-orientedprogramming
CR Categories (ACM Computing Classification System, 1998 version): D.2.3, D.3.2,D.3.3
ii
Sanasto
ACID Atomic, Consistent, Isolated, Durable: muutostapahtumien neljä ta-
voiteltavaa ominaisuutta.
Arvo Muuttumaton tiedonjyvänen.
CLR Common Language Runtime: ajonaikainen ympäristö .NET-
ympäristössä käytettävän tavukoodin suoritukselle.
Ensiarvoisuus Ensiarvoisia funktioita voidaan käsitellä kuten muita arvoja.
Erikoiskutsu Kielen sisäänrakennettu käsky, jota ei ole kirjoitettu kielellä itsellään.
JIT Just In Time: suorituksen aikana tehtävä JIT-optimointi pystyy huo-
mioimaan suoritettavan koodin kulloisen kontekstin ja yleisesti ot-
taen tekemään paremman optimoinnin kuin etukäteen optimoitaes-
sa.
JVM Java Virtual Machine: ajonaikainen ympäristö Java-tavukoodin suo-
ritukselle.
Kuvaus Kokoelma avain-arvo-pareja. Usein sanakirjan käsitteellä.
Makro Käännösaikana luettavat ja suoritettavat funktiot ohjelmakoodista
ohjelmakoodiksi.
MVCC Multi-Version Concurrency Control: malli hallita usean entiteetin
muutoksia koordinoidusti.
POD Plain Old Data: tietoa ja tietorakenteita, joilla ei ole metodeja käsi-
tellä itseään.
STM Software Transactional Memory: tapa hallita entiteettien muutoksen-
tekoa jaetussa muistissa eri säikeiden välillä.
TCO Tail Call Optimization: funktion häntärekursion optimointi kutsupi-
non hallitsemiseksi.
Viite Arvo, joka osoittaa tiettyyn entiteettiin, jota voidaan lukea tai muut-
taa hallitusti epäpuhtain funktioin.
iii
Sisältö
1 Johdanto 1
2 Clojuren syntaksi ja perusteet 42.1 Kielen perusteet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.2 Tietotyypit ja tietorakenteet . . . . . . . . . . . . . . . . . . . . . . . 8
2.2.1 Skalaarityypit . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2.2 Tietorakenteet . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.3 Kokoelmien toteuttamista abstraktioista . . . . . . . . . . . . 11
2.3 Makrot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.4 Kommunikointi Java-kirjastojen kanssa . . . . . . . . . . . . . . . . 15
3 Sovellusalustojen merkitys pienille kielille 173.1 Java Virtual Machine . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.2 Common Language Runtime ja JRE . . . . . . . . . . . . . . . . . . 20
3.3 JavaScript-alustat: IonMonkey, V8 ja muut . . . . . . . . . . . . . . . 21
4 Oliopohjainen ja funktionaalinen paradigma 244.1 Oliopohjaisen ja funktionaalisen paradigman määritelmiä ja karakteri-
soivia ominaisuuksia . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.1.1 Oliopohjaisen paradigman määritelmiä . . . . . . . . . . . . 26
4.1.2 Funktionaalisen paradigman määritelmiä . . . . . . . . . . . 27
4.2 Clojure on funktionaalinen kieli . . . . . . . . . . . . . . . . . . . . 29
5 Ilmaisuongelma 315.1 Ongelmakuvaus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
5.2 Eräitä ratkaisuja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5.3 Monimetodit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
6 Kompleksisuudenhallinta 386.1 Makrot ja sovellusaluekohtaiset kielet . . . . . . . . . . . . . . . . . 38
6.2 Pysyvät tietorakenteet suunnittelussa . . . . . . . . . . . . . . . . . . 43
6.3 Tilanhallinta ja STM . . . . . . . . . . . . . . . . . . . . . . . . . . 46
7 Yhteenveto 50
Viitteet 56
iv
1 Johdanto
Ohjelmointi on kielineen ja paradigmoineen muuttunut kovasti viimeisen 50 vuoden ai-
kana. Matalan tason konekielestä noustiin ensin proseduraaliseen ohjelmointiin, josta
kehittyi myöhemmin vielä nykyään pääasiallisesti käytettävä oliopohjaisen ohjelmoin-
nin paradigma. Vaikka kielet, tekniikat ja paradigmat ovat kehittyneet ja jalostuneet
vuosien saatossa, on muuttuvan tilan hallinta ja jakaminen silti yhteistä kaikkien näi-
den paradigmojen välillä yleisessä sovellutuksessa. Sama jaetun tiedon hallittu muut-
taminen on edelleen sama ongelma kuin konekielten aikoina.
Sovelluksen sisäisen muistin käyttötapa ja allokointi on muuttunut merkittävästi vii-
meisen 50 vuoden aikana. Matalan tason ja pienien muistimäärien näpräämisestä on
siirrytty niin suuriin muistimääriin, että avoimen ja käytetyn muistin kirjanpito on ul-
koistettu useaan kertaan, useille sovelluskerroksille. Siinä missä ennen ohjelmoija pys-
tyi kirjaimellisesti pitämään paperilla ylhäällä käyttämiensä muistipaikkojensa osoit-
teet, nyt eivät enää 32-bittiset kokonaisluvut riitä osoittamaan kaikkia samanaikaisessa
käytössä olevia mahdollisia osoitteita.
Olemme tulleet pisteeseen, jossa voimme käyttää muistia ylellisesti. 50 vuotta sitten
jouduttiin ohjelmoimaan säästeliäästi jokaista muistipaikkaa huolellisesti vaalien ja uu-
delleenkäyttäen tehden muutoksia suoraan edellisen arvon päälle. Muistimäärän kehi-
tyksestä huolimatta nykyaikainen ohjelmointikulttuuri yhä pitäytyy vanhoissa tavoissa,
ja käsittelee muuttujia hienosti pukeutuneina muistiosoitteina. Muuttuvaa tilaa huonos-
ti kätkevä olioparadigma ei onnistu olemaan proseduraalista ohjelmointiparadigmaa
aidosti parempi tapa hallita kompleksisuutta. Syy on kielten käytännöissä, jotka kum-
puavat vanhoista tekniikoista. Vaikka valtakielet ja suositellut tekniikat mahdollista-
vatkin pysyvän tiedon kanssa työskentelyn, kielten vakiokirjastot ja käyttäjät edelleen
kirjoittavat käytännössä glorifoituja globaaleja muuttujia, tässä vaiheessa pinnallisesti
piilotettuna oppikirjaolioiksi kaikkine aksessorimetodeineen.
Sovellusprojektien laajuuksien jatkuvasti kasvaessa on yhä tärkeämmäksi muuttunut
tarve hallita sovelluskoodin sisäisiä ominaisuuksia niin, että koodia on helppoa lukea,
laajentaa ja ylläpitää. Hyvä arkkitehtuuri tekee koodista yksinkertaista, joka tarkoittaa
että komponenttien sisäiset riippuvuudet ovat selkeitä ja mielellään vähälukuisia. Vas-
taavasti monimutkainen tai kompleksinen koodi sisältää keskinäisiä riippuvuuksia, jot-
ka ovat usein lisäksi implisiittisiä. Vaikka pysyvä tieto ei suoraan tee ohjelmakoodista
1
yksinkertaista, muuttuva tila kannustaa kompleksisiin arkkitehtuureihin.
Moni nykyaikainen valtakieli, Java ja C# mukaanlukien, sallii luokkien ja niiden jäsen-
ten asettamisen muuttumattomaksi ja lopulliseksi. Koska tämä käytös ei ole oletuksena
voimassa, on tällaisten apukeinojen vaikutus kokonaiskuvassa vähäistä ja koodikan-
nasta riippuvaista. Valtakielten vakiokirjastojen idiomaattiset tietorakenneluokat ovat
myös aina muuttuvia. Muuttuva tieto on oletusarvoinen ja annettu tosiasia, jota moni
ei edes osaa kyseenalaistaa. Vladimir Bartolia mukaillen voisi sanoa, että mikään ei
ole muuttumatonta: kaikki on sallittua.
Eräs hyvä tapa vanhoja tapoja vastaan on pakottaa kielen käyttäjät hyville tavoille.
Hyvät, vaikka massasta poikkeavat oletukset toisaalta nostavat oppimiskynnystä.
Kieliä, joissa muuttumaton tieto tulee oletuksena, ja joiden vakiokirjastot mukailevat
tätä seikkaa, on ollut keskuudessamme vuosikymmeniä, mutta ne eivät ole ottaneet
suuresti tuulta alleen suuren yleisön edessä. Funktionaalinen ohjelmointi on pitkään
peräänkuuluttanut puhtaiden funktioiden ja “muuttumattomien muuttujien” tai arvojen
puolesta. Tekniikka vaatii toimivan roskienkeruun ja enemmän työmuistia kuin muisti-
paikkojen ja paikallaanmuuttelun kanssa työskentely, mutta vastineeksi muuttumatto-
muus vapauttaa ohjelmoijat yhdestä isosta huolenaiheesta lähestulkoon kokonaan.
————
Clojure on funktionaalinen, dynaamisesti tyypitetty lisp-perheen kieli. Rich Hickeyn
alkujaan suunnittelema ja kirjoittama kieli syntyi vuonna 2008. Kieli toimii Java Vir-
tual Machinen ajonaikaisen ympäristön päällä, mutta kielestä on olemassa toteutuk-
set myös JavaScriptille ja .NET-maailmasta tutun Common Language Runtimen pääl-
le. Clojure on kirjoitettu vastaamaan monimutkaistuvien sovellusprojektien haasteisiin
tuomalla ratkaisuja yksinkertaistavia ominaisuuksia helposti saataville käyttäjille.
Clojure on funktionaalinen kieli, jossa etusija on annettu puhtaille funktioille ja pysy-
ville tietorakenteille. Se ei ole kuitenkaan puhtaan funktionaalinen kieli, vaan sivuvai-
kutuksia on tarvittaessa helppo tehdä. Oliopohjaisesta ja funktionaalisesta lähestymis-
tavasta kerron vertaillen luvussa 4.
Clojure on lisp-kieliperheen nuori jäsen, joka suorittaa ahkerasti, mutta jossa on tapa-
na käyttää laiskoja jonoja, näin tarjoten välimallin ahkerasti ja laiskasti suorittaville
2
kielille. Muiden lispien tapaan Clojuressa on vain lausekkeita, ei käskyjä. Clojure on
useimpien muiden lispien tavoin homoikoninen, eli käyttää omia tietorakenteitaan oh-
jelmakoodinsa ilmaisemiseen. Makrot, jotka ovat funktioita koodista koodille, toimivat
Clojuressa kuten useimmissa muissa lispeissä. Makroista kerron kohdissa 2.3 ja 6.1.
Clojuren päätös toimia olemassaolevan sovelluspinon päällä on sallinut kielelle useita
etuja jo varhaisessa vaiheessa. Kielen alustana toimiva Java Virtual Machine toi ympä-
ristönsä mukana vuosien ajan hioutuneet tulkit ja kääntäjät, joiden suoritusnopeus on
suoraan valjastettu kielen juhdaksi. Lisäksi Javalle kirjoitetut kirjastot toimivat Cloju-
ressa sellaisinaan, ja Clojuren ja Javan välinen yhteistoiminta on suunniteltu vaivatto-
maksi. Tämän ansiosta Javan rikas kokoelma erilaisia kirjastoja on suoraan käytettä-
vissä verraten tuoreessa kielessä. Tästä kerron luvussa 3.
Clojure on dynaamisesti ja vahvasti tyypitetty kieli. Käännös JVM-tavukoodiksi on-
nistuu suorakäänteisesti, sillä kyseinen tavukoodiesitys on pitkälti tyypitöntä.
Clojure on tietokeskeinen: pysyvät ja muuttumattomat tietorakenteet ja oletuksena
muuttumaton tieto avaavat mahdollisuudet käsitellä tietoa avoimemmin, koska pelkoa
muiden suoritusyksiköiden tekemistä muutoksista ajonaikana ei ole. Funktionaalisuus
ja puhtaiden funktioiden suosiminen painottaa kirjoittamaan oliopohjaisen kapseloin-
nin sijasta tietoa muodosta toiseen käsitteleviä funktioita. Tämä sallii paremman ja yk-
sinkertaisemman tavan tehdä isoja järjestelmiä. Tietokeskeisyydestä kerron luvussa 6;
pysyvistä tietorakenteista ja muuttumattomuudesta kohdassa 6.2.
Clojuren Software Transactional Memory (STM) -pino on kielen tarjoama ratkaisu
muutoksenhallintaan, ja täyttää ACID-luokituksen kirjaimista kolme ensimmäistä. Si-
säänrakennettu STM-pino toimii hallitun, säikeidenvälisen muutoksenhallinnan kans-
sa. Eristetyt ja atomiset tapahtumat auttavat pitämään jaetun tiedon eheänä. Yhtenäisen
ja pienen rajapinnan kautta tehty tiedon ja tilan hallittu muuttaminen helpottaa sekin
taistelemaan kompleksisuutta vastaan. STM käsitellään kohdassa 6.3.
Ilmaisuongelma ja sen ratkaisut ovat eräs tapa kohdata kompleksisuutta sovelluksis-
sa. Kun ohjelman vaatimuksiin tulee ajan saatossa uusia, usein vieraita komponentteja,
kaiken saattaminen toimintaan ylläpidettävästi voi olla vaikeata. Tutkimme ilmaisuon-
gelmaa ja sen ratkaisuja kohdassa 5.
3
2 Clojuren syntaksi ja perusteet
Tässä luvussa esittelen lyhyesti Clojuren keskeisen syntaksin ja kielen peruskäyttöä.
Kielen ollessa vielä nuori erilaisia muutoksia tapahtuu nyt ja jatkossakin. Esitetyt ra-
kenteet toimivat ainakin kielen versioissa 1.4–1.7, joista uusimmassa tulee hieman
muista eroavia viestejä.
Tämä luku jakautuu seuraavasti: kohdassa 2.1 esittelen Clojuren perusteita ja kielen
suunnitteluratkaisuja. Kohdassa 2.2 käyn läpi kielen valmiina tarjoamat tietotyypit ja
tietorakenteet. Kohdassa 2.3 esittelen lisp-kielille ominaista makro-ohjelmointia Clo-
juren tapaan ja siihen liittyvät perusideat. Kohdassa 2.4 esittelen, kuinka Clojuresta
käsin voidaan luoda ja kutsua Java-olioita ja niiden metodeita.
2.1 Kielen perusteet
Clojure on dynaamisesti ja vahvasti tyypitetty ohjelmointikieli, joka käännetään Java
Virtual Machine -tavukoodiksi ennen suoritusta. Dynaaminen tyypitys tarkoittaa, et-
tä kielen tyyppejä ei tarkisteta staattisesti käännöksen aikana. Vahva tyypitys toisaalta
tarkoittaa, että kieli ei tulkitse ja vaihda tyypitettyjä arvoja vapaasti ilman käyttäjän
erillistä ilmoitusta. Clojure on ahkerasti suorittava Lisp-sukuinen kieli, joka perii mm.
funktiokutsujen etuliitenotaation ja S-lausekkein kirjoitettavan syntaksin. Tässä koh-
dassa esittelen hyvin tiiviisti kielen käytön oleelliset piirteet siinä määrin kuin tässä
työssä sitä tietämystä tarvitaan. Kattavat tekstit kielen yksityiskohtiin ja käyttöön löy-
tyvät kirjoittajien Emerick et al. (2012) ja Fogus et al. (2011) kirjoista.
Clojure on täysin lausekeorienteinen kieli: jokainen lauseke tuottaa aina arvon. Kuten
muissa lispeissä, Clojuren lausekkeet ovat niinsanottuja S-lausekkeita, jotka ovat joko
yksittäisiä arvoja, tietorakenteita tai funktiokutsuja.
Funktiokutsut suoritetaan kirjoittamalla funktion nimi sulkujen sisään ensimmäiseksi
ja mahdolliset argumentit sanavälein eroteltuna sen perään.
user=> (+ 1 2 3)
#=> 6
user=> (/ (+ 1 2) (- 4 1))
#=> 1
4
Clojuressa on kolmenlaisia kutsuttavia olioita: funktioita, makroja ja erikoiskutsuja
(special forms). Funktiot ovat perinteisiä mekanismeja tuottaa syötedatasta tulosda-
taa. Makrot ovat käännöksen aikana suoritettavia funktioita, jotka tuottavat syötekoo-
dista tuloskoodia. Erikoiskutsut ovat kielen sisäänrakennettuja erikoistapauksia, joilla
Clojure-koodi sidotaan osaksi JVM-tavukoodia. Erikoiskutsuja on lispeissä tyypilli-
sesti niin vähän kuin mahdollista: vain ne kielen ydintoiminnot, joita ei voida toteuttaa
funktioina tai makroina, ovat erikoiskutsuja.
Funktion luominen tapahtuu fn-erikoiskutsulla. Kutsusyntaksissa merkitään funktion
saamat argumentit, jos sellaisia on, hakasulkujen sisään. Argumenttien nimet merki-
tään hakasulkujen sisään tyhjein eroteltuna. Kutsun saamat loput argumentit ovat S-
lausekkeita, jotka tuottavat uusia arvoja tai kutsuvat muita funktioita. Funktion suo-
rituslohko on joko tyhjä tai se koostuu vähintään yhdestä lausekkeesta. Lausekkeet
suoritetaan peräkkäisjärjestyksessä, ja viimeinen suoritettava lauseke määrää samalla
funktion paluuarvon.
user=> (fn [x y] (+ x y))
#=> #<user$eval318$fn__319 user$eval318$fn__319@793057d2>
user=> ((fn [x y] (+ x y)) 2 10)
#=> 12
Arvojen nimeäminen globaalilla tasolla tapahtuu def-erikoiskutsua käyttämällä.
Funktioiden määrittelyä ja nimeämistä tapahtuu verraten paljon, ja sitä varten avuk-
si löytyy erityinen defn-makro.
(def vastaus 42)
(def plus ; sama kuin jälkimmäinen
(fn [a b]
(+ a b)))
(defn plus ; sama kuin edellinen
[a b]
(+ a b))
Arvojen nimeäminen lokaalissa näkyvyydessä tapahtuu let-makroa käyttämällä. Van-
hoja arvoja voi peittää let-ympäristön suorituksen ajaksi.
5
user=> (def vastaus 42)
#=> #’user/vastaus
user=> vastaus
#=> 42
user=> (let [vastaus 10
luku 5]
(+ vastaus luku))
#=> 15
user=> vastaus
#=> 42
Listoja voi rakentaa for-makron avulla. Huomioitavaa on, että for ei ole perintei-
nen imperatiivinen silmukkarakenne, vaan sillä tuotetaan uusia listoja deklaratiiviseen
tapaan.
user=> (for [x (range 10)]
(* x x))
#=> (0 1 4 9 16 25 36 49 64 81)
user=> (for [x (range 10)
:when (even? x)]
x)
#=> (0 2 4 6 8)
Ehtolauseet toteutetaan if-erikoiskutsun avulla. Clojuren if sisältää aina “then”- ja
“else”-haarat. Jälkimmäisen puuttuessa se tulkitaan tyhjeenä nil.
user=> (if (< 5 30)
:totta
:valhetta)
#=> :totta
Ketjutettuja ehto-lauseke-kokoelmia kirjoitetaan cond-makron avulla. Järjestyksessä
ensimmäisen toteenkäyvän ehdon parina oleva lauseke lasketaan:
user=> (cond
6
(> 1 5) :foo
(< 2 2) :bar
(< 1 2) :totuus)
#=> :totuus
Arvon mukaan valittavaa suorituspolkua varten on kirjoitettu valmis case-makro, joka
vastaa esimerkiksi Javan switch-käskyä. Toisin kuin Javan switch, Clojuren case
tukee myös rakenteisia arvoja.
user=> (case (+ 2 2)
3 :ei
4 :joo
5 :foo)
#=> :joo
Ehtolauserakenteiden suoritushaarat koostuvat aina yksittäisistä lausekkeista. Jos sivu-
vaikutusten takia on tarvetta kirjoittaa useita funktiokutsuja samaan haaraan, voidaan
käyttää do-erikoiskutsua:
user=> (if (even? 2)
(do
(println "Kakkonen on tasainen.")
:tosi))
"Kakkonen on tasainen."
#=> :tosi
Listan tai nimen lainaamalla käyttäen quote-kutsua tai ’-lyhennettä ilmoitetaan
kääntäjälle, että seuraavana tulevan nimen mahdollista arvoa ei tule hakea, tai seu-
raavana tulevaa listaa ei tule tulkita funktiokutsuna. Kaikki listan sisään jäävä jätetään
myös laskematta.
user=> vastaus
#=> 42
user=> ’vastaus
7
#=> vastaus
user=> (+ 1 2)
#=> 3
user=> ’(+ 1 2)
#=> (+ 1 2)
2.2 Tietotyypit ja tietorakenteet
Tässä kohdassa esittelen Clojuren skalaarityypit tai atomiset tyypit ja kielen hyvin
tukemat tietorakenteet. Lisäksi esittelen muutamia rajapintoja tai Clojure-jargoniksi
protokollia, joita Clojuren ja Javan tietorakenteet toteuttavat. Esimerkiksi alakohdas-
sa 2.2.3 käsiteltävä jonoabstraktio on ulkoisesti tavallinen Javan rajapinta ja Clojuren
protokolla.
2.2.1 Skalaarityypit
Clojuren tietotyypit ovat pitkälti samat kuin Javassa, mutta lisäyksiäkin on. Valmiita
skalaarityyppejä kieli määrittelee seuraavasti:
Kokonaisluvut ja liukuluvut ovat Javan ja JVM:n laatikoituja (boxed) tyyppe-
jä, jotka voivat Clojuressa tarvittaessa korottua (promote) automaattisesti
BigInteger- tai BigDecimal-tyyppisiksi, mikäli laskutoimituksen tulos ei sovi
olemaan normaali kokonaisluku. Vaihtoehtoisesti suurten lukujen kokonaisluku-
aritmetiikassa voidaan heittää poikkeus, jos operaation tuloksena syntyisi luvun
ylivuoto.
Rationaaliluvuilla voidaan esittää tarkkoja murtolukuesityksiä. Rationaalilukuja voi
esittää prefiksinotaation lisäksi algebrallisella notaatiolla: (/ 5 4) ja 5/4 tuot-
tavat saman Ratio-tyyppisen arvon.
Totuusarvot true ja false toimivat totuusarvovakioina kielessä. Lisäksi kielen sään-
nöt eri arvojen totuusarvoille ovat yksinkertaiset: nil ja false ovat epätosia,
kaikki muut arvot ovat ehtolausekkeissa tosia.
Tyhjearvo nil on kaikille tyypeille kelvollinen tapa ilmaista, että arvoa ei ole. Toisin
8
kuin Javan null-vakiota, Clojuressa tyhjettä nil käytetään luontevammin osana
algoritmeja ja paluuarvoja, sikäli kun sen käyttö sopii tilanteeseen.
Merkkivakiot kirjoitetaan kenoviivan avulla: kirjain a kirjoitetaan Clojuressa \a. Li-
säksi muutama tyhjemerkki voidaan kirjoittaa pitkässä muodossa: \newline ja
\space tuottavat rivinvaihdon ja välilyönnin vastaavasti.
Merkkijonot ovat vastaavat kuin Javassa: ne toimivat skalaareina, mutta merkkijonon
voi tulkita myös vektorina merkkejä. Merkkijonot kirjoitetaan kuin useimmissa
kielissä: "merkkijono".
Säännölliset lausekkeet ovat Javan java.util.regex.Pattern-olioita, mutta joi-
den kirjoittamiselle on Clojuressa tiivis syntaksi: #".*bar" kääntää valmiin
Pattern-olion käännösaikana.
Symbolit kuvaavat kielen nimiä. Symbolit ovat kielen ensiluokkaisia (first-class)
muuttujanimiä, joita voi käsitellä ohjelmallisesti. Useimmissa lisp-kielissä sym-
boleilla on suurempi painoarvo erilaisina lippuarvoina, mutta Clojuressa käyte-
tään tähän tarkoitukseen avainsanoja.
Avainsanat ovat itseisarvollisia vakioita, joita käytetään erilaisia lippuina ja kuvaus-
ten avaimina. Avainsanat merkitään kirjoittamalla kaksoispiste symbolin eteen:
:off. Esimerkin tyypillisestä avainsanojen käytöstä kuvauksissa antaa kuva 1
sivulla 11. Muissa kielissä analogisia välineitä ovat luettelotietotyypit (Enum) ja
staattiset vakiot.
Funktiot ovat funktionaaliseksi kutsutulle kielelle ensiasteisen tärkeitä arvoja. Cloju-
ressa funktioita luodaan (fn [a b c] (...)) -erikoiskutsulla.
Referenssityypeillä hallitaan atomisin tapahtumin koordinoitua muutostentekoa. Sa-
manaikaisessa suoritusympäristössä, kuten säieohjelmoinnissa, muuttuvan ja
jaetun tiedon tarve on todellinen. Referenssityypeillä yhtenäistetään ja forma-
lisoidaan kaikki paikallaan tehtävät muutokset helposti hallittavissa oleviin ab-
straktioihin. Referenssityyppejä ovat atom, agent, var ja ref.
Referenssityypit osoittavat pysyviin arvoihin, ja kohteen arvoa haettaessa (dere-
ference) palautetaan pysyvä arvo kohteen senhetkisestä tilasta, eli kutsunaikai-
nen otos (snapshot). (Fogus et al., 2011)
9
2.2.2 Tietorakenteet
Clojuressa on neljä pääasiallista ja hyvin tuettua tietorakennetta: listat, vektorit, jou-
kot ja kuvaukset. Skalaarityyppien tapaan kaikki Clojuren tietorakenteet ovat muut-
tumattomia. Muutokset kuhunkin tietorakenteeseen tuottavat uuden version ilmenty-
mästä. Rakenteellisen jakamisen ansiosta yhteiset osat tietorakenteista voidaan uudel-
leenkäyttää muistinkäytön optimoimiseksi. (Okasaki, 1999, luku 2) Muuttumattomuus
ja rakenteellinen jakaminen yhdessä muodostavat pysyvän (persistent) tietorakenteen
käsitteen.
Dynaamisesti tyypitetylle kielelle tyypillisesti kaikki tietorakenteet ovat heterogeeni-
siä, eli voivat sisältää minkätyyppisiä arvoja tahansa, mukaanlukien muut tietoraken-
teet ja Java-natiivit oliot. Clojure perii edeltävistä lispeistä sen perinteen, että elementit
erotellaan toisistaan tyhjeiden avulla. Pilkut ovatkin Clojure-kääntäjän näkökulmasta
sanavälejä.
Listat ovat lisp-henkisesti yhteen suuntaan linkitettyjä listoja. Sen seurauksena lisäyk-
set ja poistot alusta ovat vakioaikaisia ja elementin hakeminen lineaarista. Koska listat
ovat linkitettyjä ja koska Clojuressa on kääntäjän tukea muille usein käytetyille tieto-
rakenteille, listoja ei käytetä tyypillisessä Clojure-ohjelmassa suuresti. Tässä Clojure
poikkeaa perinteisemmistä lisp-perheen kielistä. (Emerick et al., 2012)
Clojure-ohjelmakoodi kirjoitetaan samalla notaatiolla kuin kielen listat, mikä tekee
kielestä homoikonisen: kielen ohjelmakoodi on samalla saman kielen dataa. Sik-
si tavallinen lista tulkitaan ensisijaisesti funktiokutsuna. Listoja voi käyttää tietora-
kenteina lainaamalla lista käyttämällä quote-erikoiskutsua kohdan 2.1 mukaisesti:
(quote (1 2 3)) ja ’(1 2 3) ovat ekvivalentteja tapoja merkitä samaa kolmen al-
kion listaa.
Vektorit ovat tyypillisesti kiinteämittaisia ja suorasaantisia (random-access) tauluk-
korakenteita. Clojuressa vektorit käyttäytyvät samoin, vaikka teknisesti vektorit ovat
32-haaraisia hakupuita. Tämä suunnitteluratkaisu syntyi pysyvyysvaatimuksen seu-
rauksena. Vektoreiden iterointi on edelleen aikavaativuusluokkaa O(n), vektorin koon
laskeminen on vakioaikainen operaatio ja elementin hakeminen indeksin mukaan on
O(log32 n).
Vektori kirjoitetaan hakasulkujen avulla: [1 2 3], [[1] :foo] ja [[]] ovat esimerk-
10
;; Kolme avain-arvo-paria sisältävä kuvaus{1 "yksi"2 "kaksi"3 "kolme"}
;; Avaimet voivat olla rakenteisia{[0 0] :origo[0 1] :pelaaja}
;; Sisäkkäiset kuvaukset ovat idiomaattisia{:nimi "Mikko":ikä 24:koulutus {:peruskoulu 2005
:lukio 2009:kandi 2011}}
Kuva 1: Esimerkkejä Clojuren kuvausten syntaksista.
kejä vektorinotaatiosta. Tyhjää vektoria merkitään seuraavasti: [].
Kuvaukset eli sanakirjat (maps) ovat avain-arvo-kokoelmia, joilla ei ole välttämättä
järjestystä. Kuvausabstraktiota vasten kielessä on valmiina kolme erilaista toteutusta:
hajautukset (haku on O(log32 n)), hakupuut (elementit ovat annetun kriteerin mukai-
sessa järjestyksessä ja haku on O(log2 n)) ja lisäysjärjestyksessä pysyvä “array map”
-toteutus, jolla on heikko, lineaarinen hakuaika.
Kuvaus kirjoitetaan aaltosulkujen sisään avain-arvo-pareina. Kuvassa 1 esittelen muu-
taman kuvauksen ladottuna kielelle idiomaattiseen tapaan.
Joukot ovat edellä esitetyn kuvauksen erityistapaus, jossa säilötään vain avaimia. Jouk-
koja saa hajautetulla tai lajitetulla toteutuksella. Joukkoja merkitään Clojuressa risuai-
tasymbolin ja aaltosulkujen kanssa: #{1 2 3 4}.
2.2.3 Kokoelmien toteuttamista abstraktioista
“It is better to have 100 functions operate on one data structure than 10 functions on
10 data structures.” – Alan Perlis Structure and Interpretation of Computer Programs
-kirjan esipuheessa. (Abelson et al., 1996)
Clojuren kokoelmat toteuttavat monia abstraktioita yhtenäistääkseen kokoelmien kä-
11
sittelyä. Kaikkia neljää päätietorakennetta voidaan käsitellä yhtenäistetyin funktioin,
jotka ovat täten polymorfisia. Kaikille kokoelmille määritellään useita tyypillisiä funk-
tioita, kuten count alkiomäärän laskemista varten, conj (lyh. conjoin) uusien alkioi-
den liittämiseksi mukaan ja empty, joka tuottaa polymorfisesti tyhjän, samaa tyyppiä
olevan kokoelman. Lisäksi kokoelmat toteuttavat funktion seq, jolla kokoelmasta tuo-
tetaan jononäkymä (sequence). (Emerick et al., 2012, luku 3) Jono on yhtenäistetty
tapa lukea kaikkia tietorakenteita jonomaisesti. Jonoabstraktio tarjoaa funktionaalisen
vastineen oliopohjaisissa kielissä tavatuille iteraattoreille.
Kaikki kokoelmat voidaan kääntää jonoiksi seq-funktion avulla. Joukot, vektorit ja
listat esittäytyvät peräkkäismäisinä jonoina kutsun jälkeen. Kuvaukset puretaan jonok-
si avain-arvo-vektoreita. (Emerick et al., 2012, luku 3) Jonoilla on omat metodinsa,
kuten first ja rest jonon ensimmäistä ja muita alkioita varten. Koska tyhjätkin ko-
koelmat tulkitaan Clojuren vertailuissa tosiksi totuusarvoiksi, on idiomaattista testata
kokoelman tyhjyys tarkastelemalla kokoelmaa jonoabstraktion takaa:
user=> (seq [1 2 3])
#=> (1 2 3)
user=> (seq [])
#=> nil ; epätosi ehtolauseissa.
user=> (seq {:nimi "Mikko" :ikä 32})
#=> ([:nimi "Mikko"] [:ikä 32])
Jonoabstraktio on Clojuressa läpitunkeva ilmiö. Clojure-kokoelmien lisäksi jonoab-
straktion toteuttavat kaikki java.util.* -tietorakenteet ja Java-kuvaukset. Lisäksi
kaikki Javan Iterable-rajapinnan toteuttavat oliot toimivat Clojuressa suoraan jonoi-
na. Javan primitiiviset taulukot ja jopa tyhje null toteuttaa myös rajapinnan. Java-
kehittäjä voi itse varmistaa jonoyhteensopivuuden toteuttamalla omissa luokissaan
clojure.lang.Seqable-rajapinnan. (Emerick et al., 2012)
Pääosa kokoelmia käsittelevistä Clojuren standardikirjaston funktioista, kuten map,
filter ja reduce, käsittelee argumenttina saamiaan kokoelmia implisiittisesti jo-
noina. Siispä esimerkiksi kuvausten käyttämiseksi map-funktion läpi tavataan käyttää
funktiota, joka saa argumentikseen sekä avaimen että arvon. Funktio map palauttaa lop-
putuloksen jonona, joten uudelleenkäännös alkuperäiseen tietorakenteeseen tulee teh-
dä tarvittaessa itse. Seuraava esimerkki poimii kuvauksen avaimet talteen ja valaisee
12
kuvauksista tehtyjen jonojen iterointia:
user=> (def kuv {:yy 1 :kaa 2 :koo 3})
#=> #’user/kuv
user=> (map first kuv)
#=> (:koo :kaa :yy) ; järjestys voi vaihdella
2.3 Makrot
Lisp-kielenä Clojuressakin on tuki makrofunktioille eli makroille. Kun rutiinin kääri-
minen funktioksi ei onnistu, voidaan kirjoittaa funktion sijaan makro. Makrot suori-
tetaan käännöksen aikana, eivätkä ne ole ensiarvoisia olioita. (Emerick et al., 2012,
luku 5: When To Use Macros) Koska makroilla käsitellään varsinaisen ajonaikaisen
tiedon sijaan staattista ohjelmakoodia muodosta toiseen, voidaan makroilla toteuttaa
uusia, yksinkertaisia ehtorakenteita idiomaattisesti. Esimerkiksi Graham (1994) onkin
sitä mieltä, että makrofunktiot tekevät lisp-kielistä ohjelmoitavia ohjelmointikieliä.
Kuvassa 2 esitän saman if-not -tyylisen ehtorakenteen sekä funktiona että makrona.
Rakenne käyttäytyy kuin if, mutta nyt ehdon on oltava epätosi “then”-haaraan men-
täessä. Koska ifnot on funktio ja Clojure suorittaa ahkerasti funktiokutsujen argu-
mentit auki ennen kutsua, rivillä A tehtävä funktiokutsu laskee auki kaikki kolme ar-
gumenttiaan suorittaen kaikki sivuvaikutukset samalla. Varsinaisen funktion suorituk-
sen alkaessa funktio näkee vain aukilasketut argumentit false, nil ja nil. Vastaava
makro saa syötteenään alkuperäiset, laskemattomat argumentit. Kuvassa 3 demonstroin
tätä eroa.
Makroversio ifnot-m suoritetaan sitä vastoin jo käännösaikana. Sen saamat argumen-
tit näkyvät makrolle Clojure-koodina. Tuloksena makro tuottaa uutta Clojure-koodia,
jolla korvataan koko alkuperäinen makrokutsu. Tätä toimenpidettä kutsutaan makro-
laajennukseksi (macro expansion), ja tämä tapahtuu funktion kääntämisen aikana. Kos-
ka makrot voivat laajentua uusiksi makrokutsuiksi, laajentamista jatketaan rekursiivi-
sesti kunnes syntyvässä koodissa ei ole enää kutsuja muihin kuin funktioihin tai eri-
koiskutsuihin.
13
;; Funktiokutsu: kaikki kolme argumenttia lasketaan;; auki ennen kutsua.user=> (defn ifnot [expr then else]
(if (not expr)then else))
user=> (ifnot (= 4 2) ;; (A)(println "neljä ei ole kaksi")(println "neljä on kaksi"))
neljä ei ole kaksineljä on kaksi#=> nil
;; Makroversio saa argumenttinsa puhtaana koodina,;; jota ei ole suoritettu.user=> (defmacro ifnot-m [expr then else]
(list ’if (list ’not expr)then else))
user=> (ifnot-m (= 4 2)(println "Ei sama")(println "Sama!"))
Ei sama#=> nil
Kuva 2: Funktioiden ja makrojen argumenttien suorittamisesta.
user=> (defn funk [& args] (println args))user=> (funk (+ 3 4) (= 4 4))(7 true)#=> nil
user=> (defmacro mak [& args] (println args))user=> (mak (+ 3 4) (= 4 4))((+ 3 4) (= 4 4))#=> nil
Kuva 3: Funktioiden ja makrojen argumenteista
14
2.4 Kommunikointi Java-kirjastojen kanssa
Clojuren ja Javan välillä vallitsee kuvainnollinen symbioosi. (Fogus et al., 2011) Ja-
valla kirjoitettuja luokkia ja koodia voi käsitellä Clojuresta käsin selkeällä syntaksilla.
Seuraavassa esittelen Foguksen ja Houserin (2011) kirjasta lainattuja esimerkkejä Clo-
juren ja Javan välisestä yhteistoiminnasta.
Staattisten vakioiden hakeminen ja staattisten metodien kutsuminen tapahtuu käyttä-
mällä kauttaviivaa luokan ja jäsenen välissä.
java.util.Locale/JAPAN
(Math/sqrt 9)
#=> 3.0
Idiomaattinen tapa luoda instansseja Java-luokista on käyttää luokan nimeä ja pistettä
sen perässä.
(java.util.HashMap. {"koo" 10})
#=> #<HashMap {koo=10}>
Piste nimen edessä tarkoittaa instanssin jäsenen hakemista tai metodin kutsua:
(.x (java.awt.Point. 10 20))
#=> 10
(.divide (java.math.BigDecimal. "42") 2M)
#=> 21M
Muutettavat ja julkiset oliojäsenet voidaan muuttaa erityisen set!-kutsun avulla. Täs-
sä julkista Point-olion x-jäsentä muutetaan luvusta 1 lukuun 4. Muutokset kohdistuvat
kertaalleen luotuun Point-olioon, jonka koordinaatit ovat laskennan jälkeen (4, 2). Tä-
tä erityiskutsua tarvitaan vain ja ainoastaan julkisia ja muutettavia jäseniä sisältävien
Java-olioiden käsittelemiseksi.
15
(set! (.x (java.awt.Point. 1 2))
4)
Lisäksi Java-entiteettejä voidaan tuoda lyhennetyin nimin Clojure-nimiavaruuksiin
import-kutsulla:
user=> (import java.util.HashMap)
#=> java.util.HashMap
user=> (HashMap. {})
#=> #<HashMap {}>
Clojure tarjoaa ..-makron ketjukutsujen siistimiseksi. Makron avulla Javassa tyypillis-
ten ketjutettujen kutsujen kääntäminen Clojureksi on suoraviivaista, eikä muuta koodin
lukusuuntaa:
// Java
new java.util.Date().toString().endsWith("2010");
;; Clojuren tavallinen pistenotaatio:
(.endsWith (.toString (java.util.Date.)) "2010")
;; Sama ..-makroa käyttäen:
(.. (java.util.Date.) toString (endsWith "2010"))
16
3 Sovellusalustojen merkitys pienille kielille
Loppukäyttäjille suunnattuja sovelluksia ei enää kirjoiteta suoraan tietylle laiteympä-
ristölle, vaan selvä osa sovelluksista käännetään välitason sovellusalustoille (applica-
tion platform), esimerkiksi Java Virtual Machinelle (JVM) tai Windows-ympäristössä
vakiintuneelle Common Language Runtimelle (CLR). Näin meneteltäessä luovutetaan
matalimpien tasojen toteuttaminen ja käyttöjärjestelmän kanssa keskusteleminen käy-
tetylle alustalle. Sovellusalustat tarjoavat tyypillisesti myös monipuolisen valikoiman
vakiokirjastoja nopeata korkean tason kehitystä varten.
Sovellusalusta toimii erillisena osana käyttöjärjestelmän päällä, ja siten sellaisen käyt-
tö voi edesauttaa ohjelman siirrettävyyttä käyttöjärjestelmien välillä. Esimerkiksi Javan
90-luvun myyntipuheisiin sisältyi lupaus “kirjoita kerran, aja kaikkialla” -tyylisestä so-
velluskehityksestä. (Sun Microsystems, 1996) Samasta sovellusalustasta voi olla usei-
ta ilmentymiä samanaikaisessa ajossa suorituksessa olevien sovellusten tarpeiden mu-
kaan. Yhden sovellusalustan samanaikainen hajauttaminen usealle eri käyttöjärjestel-
mälle tai yksikölle ei kuitenkaan kuulu vaatimuksiin.
Sovellusalusta voi olla pelkkä kattava ohjelmakirjasto eli sovelluskehys (application
framework), jolloin kirjoitettu koodi käännetään ja linkitetään kehyksen kanssa yhdek-
si sovellukseksi. Sovelluskehykset voivat toimia usealla käyttöjärjestelmällä ja alus-
talla, mutta niille kirjoitetut sovellukset joudutaan tyypillisesti kääntämään kullekin
ympäristölle erikseen.
Sovellusalusta voi toisaalta toteuttaa oman välikooditason, jolloin alustalle tehtyjä so-
velluksia voidaan jakaa eteenpäin käännettynä tavukoodina. Suoritettaessa käännettyä
sovellusta sovellusalustan oma suoritusaikainen ympäristö (runtime environment) te-
kee lopullisen muunnoksen käyttöympäristön mukaiselle laitteistolle. Välikoodikään-
nöstä ei tarvitse tehdä jokaiselle suunnitellulle kohdeympäristölle erikseen, vaan mah-
dollisesti binäärisen välikoodin jakelu riittää.
Kohdassa 3.1 käsittelen Clojurenkin kotina toimivan Java Virtual Machinen (JVM) tek-
nisiä ominaisuuksia ja sitä, kuinka JVM tukee Javan lisäksi muita kieliä. Kohdassa 3.2
vertailen lyhyesti Microsoftin Common Language Runtimen (CLR) teknisiä ominai-
suuksia hieman vanhempaan JVM-alustaan nähden. Kohdassa 3.3 käsittelen nykyai-
kaisissa verkkoselaimissa toimivan JavaScriptin saamaa uutta roolia alustariippumat-
tomana kielenä ja pienenä ympäristönä.
17
3.1 Java Virtual Machine
Tässä kohdassa esittelen Java Virtual Machinen ominaisuuksia kielialustana: sen puut-
teita ja tapoja kiertää alustan ongelmia.
Java Virtual Machine (JVM) on sovellusalusta, joka sisältää kattavat valmiskirjas-
tot ja ajonaikaisen suoritusympäristön (Java Runtime Environment; JRE), joka tulk-
kaa binääristä tavukoodia kääntäen sitä optimoivasti käytön mukaan (Just In Time -
kääntäminen). JIT-käännön ansiosta JVM-sovellukset voivat käytännössä olla yhtä no-
peita kuin C-kielellä kirjoitetut ohjelmat. (Serpette et al., 2002)
JVM:n käyttämä tavukoodiesitys ei ole sidottu alkuperäiseen kirjoituskieleen, vaan se
on kieliriippumatonta tavukoodia. Esimerkiksi Java-ohjelmaa käännettäessä tuloksena
syntyvä tavukoodi on JVM-tavukoodia; kaikki viitteet tavukoodin lähdekielestä ovat
hävinneet käännöksessä. Tämän seikan ansiosta Clojure pystyy JVM-kielenä hyödyn-
tämään kaikkea alustalle kirjoitettua koodia täysin yhteensopivasti. (Emerick et al.,
2012, luku 9)
JVM-tavukoodi on lisäksi alustariippumatonta ja siten sellaisenaan siirrettävää ym-
päristöstä toiseen. (Serpette et al., 2002) Sovelluksen toiminnan takaamiseksi pitäisi
riittää, että kohdeympäristölle on olemassa sopiva versio JRE:stä.
Clojuren ohella moni muukin kieli toimii JVM:n päällä: Scala (Odersky, 2014b), Groo-
vy (2014), Jython (2014) ja JRuby (2014) ovat muutamia esimerkkejä. Erityisesti kaksi
jälkimmäistä kieltä ovat läheisiä klooneja esikuvistaan, joilla on kyllä omatkin suori-
tusympäristönsä. Yleisimmät syyt tavoitella JVM-yhteensopivuutta tällä tavalla ovat
JRE:n kehittyneet ajonaikaiset optimoijat, hyvä siirrettävyys ja kypsyneet sovellus- ja
luokkakirjastot. (Emerick et al., 2012; Bres et al., 2004)
JRE tukee Serpetten et al. (2002) mukaan dynaamista tyyppitarkastusta ja roskien-
keruuta, jotka auttavat toteuttamaan dynaamisesti tyypitettyjä kieliä, kuten Scheme-
johdannaisia, JVM:n päälle. Clojure myös dynaamisesti tyypitettynä kielenä tarvitsee
tehokkaasti suoritettavaa JVM-tavukoodia varten tämänlaista tukea.
JRE on kuitenkin alunperin suunniteltu vain Javan suoritukseen, ja nykyisinkin se on
suunnattu korkean tason oliopohjaisten kielien alustaksi. (Serpette et al., 2002) Tämä
näkyy edelleen tavukoodikäskyjen oliokeskeisyydessä. Esimerkiksi vapaita funktioita
saati sulkeumia JRE ei Javan tapaan tunne lainkaan. (Monteiro et al., 2005; Serpette
18
et al., 2002)
JRE ei myöskään tue rekursiivissa funktiokutsuissa hyödyllistä häntäkutsujen opti-
mointia (tail call optimization; TCO). Tällöin rekursiivisia rakenteita hyödyntävät
kielet joutuvat joko hyväksymään mahdolliset pinoylivuodot, toteuttamaan hitaam-
pia vaihtoehtoja tai kirjoittamaan ylimääräistä kääntäjälogiikkaa häntärekursiokutsu-
jen purkamiseksi ilman suoritusympäristön tukea. (Bres et al., 2004)
Nämä puutteet on kuitenkin tehty kierrettäviksi. Vapaat funktiot voidaan kirjoittaa
julkisten luokkien julkisina ja staattisina metodeina. Sulkeumat voidaan vastaavasti
toteuttaa lisäämällä näille funktio-olioille kerran alustettuja yksityisiä jäsenmuuttu-
jia. (Monteiro et al., 2005) Javan versiossa 8 on myös tuki nimettömien funktioiden
vastineelle, funktionaalisille rajapinnoille. Tämä tapahtuu muiden JVM-kielten tapaan
kääntämällä käytetyt rakenteet JVM:n tuntemille oliokäsitteille. (Weiss, 2014; Oracle,
2015)
Clojuressa päätettiin ratkaista tämä kertyvän pinon ongelma toteuttamalla kieleen ek-
splisiittinen recur-kutsu, jonka käyttö takaa häntäkutsujen optimoinnin. Kutsua käyt-
tämällä suoritus hyppää joko määriteltävänä olevan funktion alkuun tai loop-makron
alkuun. Käytännössä loop on yksinkertainen nimettömän funktion sovellus. Koska
häntäkutsuja voi tehdä vain suoritusrungon lopussa, kääntäjä kykenee tunnistamaan
recur-kutsun virheellisen käytön, ja antamaan käännösvirheen käännösaikana. Impli-
siittinen häntäkutsuoptimointi voisi ilman kunnollisia kääntäjädiagnostiikoita jättää
valheellisen vaikutelman tehdyistä optimoinneista, ja altistaa ajonaikaisille pinonyli-
vuodoille.
Useamman funktion välinen keskinäinen rekursio (mutual recursion) toteutetaan yleis-
luonteisella trampoliinitekniikalla: jos rekursio ei lopu, suorittava funktio ei kutsu suo-
raan seuraavaa funktiota, vaan tekee sulkeuman tarvittavista argumenteista, ja palauttaa
sen välittäjäfunktiolle. Koska palautettavassa sulkeumassa on kaikki seuraavan iteraa-
tion tarvittavat argumentit mukana, välittäjäfunktio voi kutsua tätä nolla-argumenttista
funktiota jatkaakseen rekursiota, eikä sen tarvitse tuntea funktioita. Rekursio loppuu
ennaltasovitun käytännön perusteella, Clojuressa silloin kun joku funktioista palaut-
taa muuntyyppisen arvon kuin funktion. Clojuren vakiokirjastoon kuuluva funktio
trampoline (Clojure, 2014, core.clj:5790) toimii juuri näin kuvatulla tavalla.
19
3.2 Common Language Runtime ja JRE
Microsoftin .NET-ympäristö lanseerattiin vuonna 2001, kuusi vuotta JVM:n jälkeen.
Ympäristön suoritusaikainen ympäristö on nimeltään Common Language Runtime
(CLR) ja välimuotoinen tavukoodi tunnettiin ensin nimellä Microsoft Intermediate
Language (MSIL) ja nyttemmin nimellä Common Intermediate Language (CIL). Tässä
kohdassa esittelen, mitä ominaisuuksia .NET tukee vaihtoehtoisten kielten suunnitteli-
joille.
CLR on JRE:n tapaan pinopohjainen virtuaalikone, joka suorittaa alustariippumaton-
ta, välimuotoista tavukoodia. Microsoftin ylläpitämä suoritusympäristö toimii vain
Windowsilla, mutta avoin Mono-projekti toimii hyvin kaikilla suurilla ympäristöil-
lä. Kaikkia .NET-kirjastoja ei ole kirjoitettu alustariippumattomasti, mutta itse kieli,
tavukoodi ja ydinkirjastot ovat sellaisenaan suoritettavissa Monolla. (Mono Project,
2015) Microsoft on myös tuonut myös Linuxilla ja Mac OS X:llä toimivan .NET Co-
re -nimisen ratkaisun, jonka määränä on tarjota alustariippumaton suoritusympäristö
.NET-ympäristön ydinkirjastoille. (Microsoft, 2015) Projektin skaala on siis vastaava
kuin Monon.
CLR:n välikoodikäskyt ovat JRE:n tapaan vahvasti oliopohjaisen paradigman mukai-
sia. (Singer, 2003) CLR tukee monimuotoisuutta ja roskienkeruuta, ja sisältää JIT-
kääntäjän, kuten myös JRE. (Bres et al., 2004)
JRE:stä poiketen CLR tukee monipuolisemmin erilaisia kielirakenteita, joiden avul-
la ympäristölle on helpompi kirjoittaa kääntäjiä muistakin kuin oliopohjaisista kielis-
tä. (Bres et al., 2004; Singer, 2003) CLR tukee suoraan esimerkiksi luettelotyyppe-
jä (enumerations), rakenteisia tyyppejä (structures) ja arvotyyppejä (value types) eli
tyyppejä, joiden alkioiden identtisyys määritellään sisällön perusteella.
Lisäksi CLR osaa optimoida rekursiiviset häntäkutsut pinoa säästäviksi silmukkara-
kenteiksi. CLR tukee sulkeumia paremmin kuin JRE: ei suoraan, mutta ympäristöön
kuuluvien delegaattien välityksellä paremmin kuin JRE:n oliopohjaisissa ratkaisuissa.
Ympäristö tukee myös puhtaita funktioita ja funktio-osoittimia. (Bres et al., 2004)
20
3.3 JavaScript-alustat: IonMonkey, V8 ja muut
ECMA-standardoitu EcmaScript (ECMA-262, 2011) on dynaaminen ja heikosti tyy-
pitetty selaimille suunnattu skriptikieli, ja se tunnetaan paremmin web-sivuille dynaa-
mista sisältöä tuottavana JavaScriptinä. Kieli kehitettiin 90-luvulla, ja lyhyessä ajas-
sa JavaScript on noussut 2000-luvun aikana yhdeksi yhdenvertaiseksi komponentiksi
HTML:n ja CSS:n rinnalle.
Nykyisin voidaankin puhua web-sivujen lisäksi web-sovelluksista (web application),
jotka ovat JavaScriptiä vahvasti hyödyntäviä sivuja, ja joilla tehdään perinteisiä työ-
pöytäsovelluksille kuuluneita tehtäviä. Tämän ilmiön Jeff Atwood (2007) on nimennyt
itsensä mukaan Atwoodin laiksi: “mikä tahansa sovellus, joka voidaan kirjoittaa Ja-
vaScriptillä, kirjoitetaan ennen pitkää JavaScriptillä.”
Tätä lakia vahvistavat lukuisat tapaukset, joissa perinteinen työpöytäsovellus on siir-
retty web-sovellukseksi. Esimerkiksi Google Drive sisältää JavaScriptillä toteutetut
sovellukset tekstinkäsittelylle, taulukkolaskennalle ja esitysgrafiikalle. Microsoft on
myös kirjoittanut osittaiset siirrokset Office-paketistaan verkkoversioiksi. Harrastajat
ovat kirjoittaneet kokonaisia MS DOS -emulaattoreita JavaScriptille selainten ajetta-
vaksi.
Tämän kehityksen mahdollistaa jatkuva kilpailu selainkehittäjien kesken. Ollakseen
paras Web-standardien hyvän noudattamisen lisäksi sivujen piirtämisen tulee olla no-
peampaa kuin kilpailijoilla. Lyhyiden vasteaikojen ja sulavan käytettävyyden aikaan-
saamiseksi selainten on suoritettava sivujen lataus- ja alustusvaiheessa esiintyvä upo-
tettu JavaScript-koodi tehokkaasti. Kilpailu ja kehitys onkin tuottanut useita tehokkaita
JavaScript-suoritusympäristöjä.
Firefox-selainta kehittävä Mozilla ylläpitää omaa IonMonkey-nimistä JavaScript-
suoritusympäristöä. IonMonkey tukee JIT-kääntämistä konekielelle. Google puoles-
taan ylläpitää Chrome-selaimessa käytettyä V8-nimistä JavaScript-moottoria. Moot-
tori kääntää kaiken koodin laiteympäristön mukaiselle konekielelle ennen sen suoritta-
mista ollen näin ääripään esimerkki JIT-optimoivasta ympäristöstä. V8 on myös hyvin
tehokas suorituksessaan.
Eri selainten JavaScript-moottoreiden nopeuksia automatisoidusti suorittava testipal-
velu Are We Fast Yet (Mozilla, 2014) näyttää syyskuussa 2015, että IonMonkey ja V8
21
ovat molemmat verraten tasaväkisiä toisiaan vastaan. Lisäksi molemmat suoritusalustat
pärjäävät hyvin valituissa synteettisissä testeissä puhtaasta C++:sta käännettyä natii-
vikoodia vastaan. Luonnollisesti palvelu voi kärsiä suppeasta ja puolueellisesta testien
otoksesta.
Nämä jatkuvasti merkityksettömämmäksi muuttuvat nopeustekijät ja selainten alus-
tatuet kannustavat edelleen kehittämään JavaScript-pohjaisia sovelluksia perinteisem-
pien sovelluksien sijaan. Nykyaikainen verkkoselain voidaankin samaistaa erääksi so-
vellusalustaksi JVM:n ja .NET:n rinnalle. Uusien standardien myötä selaimet ovat al-
kaneet tukea esimerkiksi musiikin ja äänien soittamista ja laitteistokiihdytettyä 3D-
grafiikkaa.
JavaScriptiä on sanottu webin konekieleksi (Wirfs-Brock, 2013). Vertauskuvan alle-
kirjoittavat myös kielen kehitykseen suuresti vaikuttaneet Brendan Eich ja Douglas
Crockford. (Hanselman, 2011)
Saman vertauskuvan JavaScriptistä internetin konekielenä voi ajatella myös kielteises-
sä sävyssä: JavaScript on toki korkean tason kieli, mutta se sisältää paljon huonosti
suunniteltuja yksityiskohtia. (Crockford, 2008, liitteet A ja B) Dynaaminen ja heikko
tyypitys on erityisesti kritisoitu ongelma puhtaassa JavaScriptissä. Lisäksi JavaScriptin
prototyyppiperustainen oliomalli liian rajoittamattomana herättää huolta rajoittuneem-
pien oliokielten kehittäjien keskuudessa. (Crockford, 2008)
Hiljan onkin kirjoitettu kieliä, jotka on tarkoitettu käännettäväksi koneen luettavak-
si JavaScriptiksi. Näiden kielien tarkoitus on tarjota johdonmukaisempia abstraktioita
ja parempia idiomeja kuin puhtaassa JavaScriptissä. Esimerkkejä tällaisista kielistä on
antaa useita. Microsoftin TypeScript tarjoaa valinnaisen staattisen tyypityksen JavaSc-
riptin päälle. (Microsoft, 2014) Koodi käännetään puhtaaksi JavaScriptiksi tyyppitar-
kistusten jälkeen. CoffeeScript on toinen esimerkki, joka kääntää Pythonia muistutta-
vaa syntaksia JavaScriptiksi. Samalla CoffeeScript tuo yhtenäistetyn mallin funktioille,
sulkeumille ja olioille. (CoffeeScript, 2014) Google Web Toolkit (GWT) kääntää käyt-
töliittymäkoodia Javasta JavaScriptiksi. (Google, 2014) Haskellistakin on olemassa Ja-
vaScriptiksi käännettävä murre, Haste. (Haste, 2014)
Clojurella on oma JavaScript-kääntäjäprojekti. ClojureScript on kielen kehittäjien vi-
rallinen yritys tuoda Clojure selaimiin. Isäntäkielensä tavoin ClojureScript toimii koh-
deympäristössä voiden hyödyntää valmiita JavaScript-kirjastoja. Vaikka kaikkea Clo-
22
juresta ei ole toistaiseksi voitu siirtää sellaisenaan JavaScript-ympäristöön, useimmat
Clojuren idiomit ovat kuitenkin suoraan käytettävissä projektin tämänhetkisessä tilas-
sa.
ClojureScript-kääntäjä koostuu useista modulaarisista komponenteista, ja sallii uusien
ulostuloformaattien kirjoittamisen. Pääasiallisen JavaScript-kääntämisen lisäksi pro-
jektille on jo kirjoitettu kääntäjiä Schemen kautta C:lle ja Python-tavukoodille.
23
4 Oliopohjainen ja funktionaalinen paradigma
Oliopohjaisen paradigman perusidea syntyi jo 1950-luvulla ja eräs formaali määritel-
mä oliopohjaiselle ohjelmoinnille saatiin Simula-67:n myötä vuonna 1967. (Holmevik,
1994; Nierstrasz, 1989) Simula innoitti monien puhtaiden olio-ohjelmointikielten ke-
hitykseen. Puhdas olio-ohjelmointikieli on kieli, jonka suunnittelu- ja toteutusperiaate
on pitää kaikki asiat olioina. Eräs varhaisista puhtaista oliokielistä on Smalltalk, jota
kehiteltiin 70-luvulla ja virallisesti julkaistiin vuonna 1980. (Kay, 1993)
Oliopohjainen ohjelmointi nousi yleisimmäksi paradigmaksi 1990-luvulla ensin
C++:n ja sitten Javan siivittämänä. Erityisesti jälkimmäinen puhtaana oliopohjaise-
na kielenä pakottaa harjoittamaan oliopohjaista ajattelua ja suunnittelua syvemmälti,
koska kielessä ei alunpitäen ollut paljoa muita ohjelmointityylejä avustavia rakenteita.
Kaikenlainen laajempi logiikanhallinta oli toteutettava oliohierarkioiden ja -ajattelun
avulla.
Funktionaalisen paradigman alkuperän määritellään useimmiten olevan Alonzo Churc-
hin 1930-luvulla kehittämä lambdakalkyyli. (Barendregt, 1997) Lambdakalkyylissä
funktiot toimivat ensiluokkaisina olioina, joiden avulla tuotetaan kaikki laskennassa
tarvittavat arvot. Lambdakalkyyli myös formalisoi useita matemaattisia käsitteitä las-
kettavaan muotoon ja laskennan teorialla on silläkin juuria lambdakalkyylissä.
Funktionaalinen paradigma kulminoituu nykyään kolmen kielen tai kieliperheen kes-
ken. Ensin John McCarthy esitteli Lisp-kieleen johtaneet periaatteet uudesta mate-
matiikan ja laskentojen merkintätavasta. Tästä johdetut ohjelmointikielet mielletään
usein funktionaalisiksi, koska ne muistuttavat perustuksiltaan lambdakalkyyliä. Toi-
saalta mm. Robin Milnerin johdolla 1970-luvulla kehitetty ML toi omia funktionaa-
lisia elementtejään. Tänä päivänä Ocaml ja Microsoftin F# pitävät ML-johdannaisten
kielten kannatusta yllä.
Funktionaalisten kielten käyttö oli alussa vähäistä, mutta 80-luvulla jatkuneesta kehi-
tyksestä seurasi myös eräs suljettu kieli, David Turnerin suunnittelema Miranda. Vuon-
na 1985 julkistettu Miranda sisälsi monia ominaisuuksia, jotka tukivat staattisesti tyy-
pitettyä ja funktionaalista sovelluskehitystä. Näihin ominaisuuksiin lukeutuvat esimer-
kiksi laiska suoritus ja matemaattisin yhtälöin määritellyt funktiot. Mirandasta johdet-
tiin myöhemmin vuosikymmenen lopulla varta vasten kootun komitean johdolla avoin
johdannainen, Haskell. Haskell on staattisesti tyypitetty puhdas funktionaalinen kieli,
24
joka tukee rentoa ja laiskaa suoritusta.
Tässä luvussa käsittelemme ja vertailemme oliopohjaisen ja funktionaalisen paradig-
man eroja ongelmanratkaisussa. Kohdassa 4.1 teemme kirjallisuuskatsauksen erilai-
siin paradigmojen määritelmiin ja paradigmoja karakterisoiviin ominaisuuksiin. Koh-
dassa 4.2 tutkimme määriteltyjen paradigmojen pohjalta Clojuren funktionaalisuutta.
Lisäksi vertaamme Clojurea funktionaaliseksi tunnustettuun Haskelliin.
4.1 Oliopohjaisen ja funktionaalisen paradigman määritelmiä jakarakterisoivia ominaisuuksia
Tässä kohdassa tutkimme, löytyykö oliopohjaiselle paradigmalle ja funktionaalisel-
le paradigmalle konsensuksen saaneita määritelmiä kirjallisuudesta. Erityisesti paneu-
dumme funktionaaliselta ohjelmointikieleltä vaadittaviin ominaisuuksiin, jotta voim-
me vastata kohdassa 4.2 esitettävään kysymykseen: “onko Clojure funktionaalinen oh-
jelmointikieli?” yleisen konsensuksen mukaan.
Oliopohjaisen paradigman keskeinen ohjenuora on: “kaikki asiat ovat olioita” – every-
thing is an object. Steve Yeggen (2006) kärjistettyä kirjoitusta mukaellen oliopohjaiset
luokat ovat ohjelman määrittelykielen substantiiveja. Vastaavasti oliopohjaisessa ohjel-
moinnissa suunnittelu keskittyy olioiden ja substantiivien ympärille. Mikään toiminto,
eli verbi, ei toimi itsenäisesti, vaan vaatii substantiivin isännäkseen. Data on määritel-
ty näiden substantiivien avulla olioiksi, joilla on omaa tilaansa hyödyntäviä metodeja:
data on tällöin oliomaailman oppien mukaisesti “älykästä”. Olioajattelussa datan on
määrä pitää huoli omista asioistaan – eli oma tilansa yhtenäisenä – ja delegoida tehtä-
viä osaolioilleen. (Fogus et al., 2011; Freeman et al., 2004)
Funktionaalisessa paradigmassa vastaavasti kaikki toimet ovat itsenäisiä funktioita.
Funktiot toimivat kielen verbeinä ja ovat datan kanssa yhdenvertaisia elementtejä. Da-
ta kootaan kielen ja käytäntöjen tukemiin tietorakenteisiin. Datalla ei ole perinteisesti
funktionaalisessa maailmassa metodeja, eli se on oliomaailman oppien mukaisesti ta-
vallista, (POD; plain old data) tai “tyhmää” dataa. Funktionaalisessa ajattelussa data
pidetään tyhmänä ja niitä käsittelevät tietorakenteet ja funktiot älykkäinä. (Fogus et
al., 2011)
Tämä ei estä säilömästä funktioita tai muuta toiminnallisuutta tietorakenteisiin, sillä
25
funktiot eivät suoraan ole tarkoitettu sisältämänsä tietorakenteen käsittelyyn. Funktiot
ovat kuin muita säilöttäviä arvoja tietorakenteelle. Älykkään datan käsite koskee funk-
tionaalisuutta, joka käsittelee suljettuja ja kapsuloituja muuttujia. Funktionaaliseen pa-
radigmaan kuuluva funktiosulkeumien käyttö hämärtää tätä rajaa varsinaisten olioiden
ja funktioiden välillä: arvoja määritelmäänsä sulkenut funktio voi käsitellä tätä dataa
kapseloidussa mielessä, mutta tätä silti pidetään funktionaalisena, jopa puhtaana.
4.1.1 Oliopohjaisen paradigman määritelmiä
Yksittäinen olioparadigman ominaisuus, joka määrittelee koko paradigmaa eniten, on
tiedon ja tilan kapselointi (encapsulation) olioiden sisään. (Schärli et al., 2004; Nier-
strasz, 1989; Gorschek et al., 2010)
Schärli et al. (2004) kuvailevat olio-ohjelmointia kuvailemalla oliopohjaisen paradig-
man tärkeintä ominaisuutta, kapselointia. Tiedon hyvin tehty kapselointi olioiden si-
sään kuuluu heidän mukaansa olioparadigman oleellisiin ominaisuuksiin. Hyvin tehty
kapselointi määrittelee hyvin luokkien tarjoamat rajapinnat, ja helpottaa täten koodin
uudelleenkäytettävyyttä ja hallittavuutta.
Nierstrasz (1989) päätyy myös siihen tulokseen, että olioparadigman määräävin piirre
on kapselointi, ja tiivistää aiheesta kirjallisuuskatsauksessaan seuraavasti: “oliopohjai-
set käsitteet, kuten olioluokista alustaminen, luokkaperiytyminen, polymorfismi, ylei-
syys ja vahva tyypitys ovat kaikki riippuvaisia olioiden kapseloinnista.” Nierstrasz kir-
joittaa myös, että olioparadigma kannustaa käsittelemään “olioita” datan tai ohjelma-
koodien sijaan. Tämä menetelmä yhtyy Yeggen (2006) käsitykseen olioilla tehtävästä
mallintamisesta.
Nierstrasz (1989) toteaa raportissaan myös, että luokkahierarkiat ja -periytyminen ei-
vät ole oleellinen oliopohjaisuutta määrittävä tekijä. Nierstrasz ehdottaa, että jokainen
kieli, joka tarjoaa menetelmiä kapseloinnin hyödyntämiseen, on oliopohjainen kieli.
Lisäksi oliopohjainen kieli tyypillisesti helpottaa olioiden ohjelmoimista tarjoamalla
sopivia kielirakenteita niiden käsittelyyn.
Kay (1993) kuvailee Smalltalkia suunnitellessaan kielen oliopohjaisuutta muovailleita
suunnittelupäätöksiä: pysyvä tila, polymorfismi, olioiden alustaminen ja olioiden me-
todit tavoitteina ovat kaikki tekijöitä oliopohjaisuudessa. Näihin suuntaviivoihin nojaa-
26
va olio-ohjelmointi, Kayn mukaan, on kuitenkin kaukana nykyaikaisien oliopohjaisten
kielien, kuten C++:n ja Javan, periaatteista.
4.1.2 Funktionaalisen paradigman määritelmiä
Funktionaalinen paradigma on kirjallisuudessa hyvin hämärästi määritelty käsite. (Fo-
gus et al., 2011; Hutton, 2007; Turner, 1995) Artikkelien kirjoittajien näkemykset funk-
tionaalisuuden määritelmästä näyttävät riippuvan voimakkaasti heidän käyttämien-
sä kielten vahvoista puolista. Haskell-taustaisille funktionaalisuus tarkoittaa puhtaita
funktioita ja laiskaa suoritusta. Vastaavasti Clojure-kirjoissa funktionaalisuus on usein
sidottu pysyviin tietorakenteisiin.
Fogus et al. (2011) tiivistävät funktionaalisen ohjelmoinnin siihen, että sen ytimessä
on formaali laskennallisen teorian malli, lambdakalkyyli. Funktionaalinen paradigma
edellyttää heidän mukaansa sitä, että funktiot ovat ensiarvoisia elementtejä (first-class
objects/citizens), joita voidaan luoda, käsitellä, yhdistellä keskenään ja palauttaa toi-
sista funktioista. Toisin sanoen funktiot ovat kuin mitä hyvänsä arvoja kielen kannalta.
Halloway (2009) laskee funktionaaliseen paradigmaan mukaan myös puhtaiden funk-
tioiden (pure functions) käsitteen, pysyvät tietorakenteet ja laiskojen jonojen käytön.
Samalla linjalla ovat Emerick et al. (2012). He lukevat funktionaaliseen paradigmaan
kuuluvaksi pysyvät tietorakenteet, ensiarvoiset funktiot ja korkeamman asteen funk-
tiot. Kirjoittajat erityisesti painottavat funktionaalisen paradigman suosivan muuttu-
matonta tietoa. Funktionaalisuus on myös enemmän kuvainnollista kuin imperatiivista
ohjelmointia. Lopuksi funktionaalinen paradigma painottaa yhdisteltävyyttä.
Varhaisemmassa kirjallisuudessa funktionaalisuuteen riittää pelkkä funktioiden painot-
taminen ohjelmakoodin organisoinnissa. Hughes (1989) tiivistää funktionaalisen ohjel-
moinnin tarkoittavan pienistä, modulaarisista funktioista tehtäviä koosteita ja isompien
kokonaisuuksien rakentamista. Hutton (2007) on samalla linjalla funktionaalisen ohjel-
moinnin määritelmän kanssa. Hughes lisäksi katsoo, että laiska suoritus tai laiskojen
listojen – joita joissakin kielissä kutsutaan myös virroiksi (streams) – rooli on tärkeä
osa funktionaalista arkkitehtuuria.
Toiset, kuten Peyton Jones et al. (1993), jakavat edelleen rajan funktionaalisen kielen
ja puhtaan funktionaalisen kielen välille. Tämä ero määräytyy sen mukaan, kuinka oh-
27
jelmointikieli tukee sivuvaikutusten toteuttamista ohjelmakoodissa. Kirjoittajat määrit-
televät esimerkiksi lisp- ja ML-perheiden kielet epäpuhtaiksi funktionaalisiksi kieliksi,
koska niissä sivuvaikutusten kirjoittamista ei valvota kielen rakenteiden avulla. Vastaa-
vasti puhtaina funktionaalisina pidetyissä kielissä, kuten Haskellissa, sivuvaikutuksia
ei voida tehdä ilman eksplisiittisten formalismien käyttöä.
Bloch (2008) määrittelee funktionaalisen ohjelmoinnin olevan sitä, että funktiot tuotta-
vat uusia arvoja muuttamatta edellisiä. Bloch ei kuitenkaan lähteessä varsinaisesti ota
kantaa funktionaalisiin kieliin.
McNamara et al. (2000) mukailee edellisiä siinä suhteessa, että funktionaalinen ohjel-
mointi edellyttää funktioiden ensiarvoisuutta. Lisäksi kirjoittajat nostavat esille tärkeän
huomion funktionaalisesta koodista: funktionaalinen koodi ei käsittele muistipaikkoja
tai viittauksia niihin, vaan se keskittyy käsittelemään arvoja.
Näistä määritelmistä voimme poimia joitakin usein toistuvia ominaisuuksia. Funktioi-
den ensiarvoisuus ja korkeamman asteen funktioiden tuki ovat eniten mainittuja omi-
naisuuksia. Kaikissa määritelmissä myös esiintyy lambdakalkyylistä peräisin oleva kä-
site, että yksittäinen funktio on kielen perusyksikkö, eivät luokat tai oliot. Lisäksi py-
syvien ja muuttumattomien arvojen läsnäolo esiintyy valtaosassa määritelmistä. Tämä
olkoon funktionaalisen ohjelmointikielen määritelmä tässä työssä. Lisäksi määrittelen
puhtaan funktionaalisen kielen Simon Peyton Jonesin (1993) tapaan olevan sellainen
funktionaalinen kieli, jossa funktioiden sivuvaikutukset tulee kääriä kielen käyttämiin
rakenteisiin. Tällöin puhtaassa funktionaalisessa kielessä puhtaiden ja epäpuhtaiden
funktioiden sekoittamista keskenään ei voi tapahtua vahingossa.
Tästä funktionaalisen kielen määritelmästä voidaan päätellä joitakin seurauksia. Funk-
tiot ovat funktionaalisen kielen perusyksiköitä, rakennusosia. Funktiot on tulkittavis-
sa atomisina olioina, joille ei ole hyvin määriteltyä perintäjärjestelmää, kuten luokille
oliopohjaisissa kielissä on. Tästä seuraten funktioiden yhdisteet ovat luontaisin ja ylei-
sin tapa uudelleenkäyttää koodia funktionaalisessa kielessä. Tämä sopii esimerkiksi
Blochin (2008) esittämään ohjenuoraan suosia yhdisteitä perimisen sijaan sovellusark-
kitehtuureissa.
Pysyvän ja muuttumattoman tiedon kanssa työskennellessä olion identiteetti ei ole si-
dottu tiettyyn instanssiin ja sen muistipaikkaan, vaan vain olion arvolla on väliä. Toi-
sin sanoen arvo on identtinen toisen arvon kanssa, jos ja vain jos niiden sisällöt ovat
28
samat. Kielet, joissa tällaista arvosemantiikkaa ei käytetä, olioiden identtisyys määri-
tellään olioiden instanssien samuusvertailulla, käytännössä vertailemalla muistiosoit-
teiden samuutta. Clojuren kehittäjä Rich Hickey (2012) kuvaakin tätä ajattelumallia
paikkakeskeiseksi ohjelmoinniksi (PLOP; place-oriented programming). Myös Bloch
(2008) kannustaa tekemään luokista ja olioista muuttumattomia aina kun mahdollista.
4.2 Clojure on funktionaalinen kieli
Edellisessä kohdassa koostimme kirjallisuudessa esiintyneistä funktionaalisen paradig-
man määritelmistä yhteenvedon. Tätä koostetta hyväksikäyttäen voimme nyt analysoi-
da Clojuren funktionaalisuutta. Vertaamme Clojurea sekä edellälöydettyihin määritel-
miin että tunnustettuun funktionaaliseen kieleen, Haskelliin.
Clojuressa funktiot ovat ensiarvoisia elementtejä, eli niitä voidaan vastaanottaa ja pa-
lauttaa toisista funktioista. Lisäksi funktioita voidaan luoda funktioiden sisällä ja sa-
malla luoda sulkeumia (closures) arvojen ylitse. (Fogus et al., 2011, luku 7.1) Tämä
ominaisuus yksinään oikeuttaa useimmissa lähteissä Clojuren kutsumisen funktionaa-
liseksi kieleksi. Clojure myös asettaa funktiot ohjelmakoodin perusyksiköiksi, ja suosii
yhdisteltävyyttä toiminnallisuutta perivien hierarkioiden sijaan.
Clojure ei ole kuitenkaan puhdas funktionaalinen kieli. Ulkoisesti tarkastelemalla an-
netusta funktiosta ei voida päätellä mitään funktion puhtaudesta. On vain esitetty ni-
meämiskäytäntöjä, joiden mukaan toiminnallisia sivuvaikutuksia aiheuttavat funktiot
nimettäisiin huutomerkillä. (Emerick et al., 2012, luku 2: Pure Functions)
Emme valinneet laiskaa suoritusta tai laiskoja tietorakenteita funktionaalisen paradig-
man määritelmiin. Tarkastelemme kuitenkin näidenkin ominaisuuksien toteutumista.
Clojure suorittaa ahkerasti tarkoittaen, että jokaisen funktiokutsun argumentit laske-
taan auki ennen kutsun suorittamista. Lisäksi Clojuren konkreettiset tietorakenteet ovat
ahkeria. (Emerick et al., 2012, luku 3) Tämä tarkoittaa sitä, että tietorakenne säilytetään
aina suoritusmuistissa aukilaskettuna.
Clojuressa on kuitenkin jonoabstraktion, jota käsittelimme johdantoluvun kohdas-
sa 2.2.3, toteuttava “laiska jono” (lazy sequence) -toteutus. Koska valtaosa Clojuren
standardikirjaston funktioista käyttää kokoelmia juuri jonoabstraktion kautta, ja ky-
seistä abstraktiota on luontevaa käyttää koodissa, on laiskojen jonojen käyttö läpinä-
29
kyvää ja yleistä. Pääasiallisesti kaikki jonomuotoista tietoa tuottavat standardikirjaston
funktiot tuottavat laiskoja jonoja. Näihin funktioihin kuuluvat esimerkiksi listarakennin
for ja funktionaaliset perustyökalut map ja filter. Tällä keinoin laiskat jonot asettu-
vat vertailukelpoisiksi Haskellin laiskoille listoille: kummassakin tapauksessa sisältöä
lasketaan käytännössä auki vain tarpeen mukaan.
Kielitasolla Clojure ei tue laiskoja funktiosovelluksia, mutta lisp-kielenä Clojure tu-
kee kyllä makroja, joilla voidaan kaapata käännöksenaikaista syötettä ja esimerkiksi
kääriä annettua tietoa myöhemmin suoritettaviksi funktioiksi. Tällainen myöhemmin
tarvittavan laskennan kääriminen funktioksi onnistuu sopivien makrojen avulla hyvin
ja käy köyhän miehen laiskasta laskennasta. (Graham, 1994, luku 15) Käsittelimme
makrojen käyttöä kohdassa 2.3. Kuvassa 2 totesimme, että uusien ehtorakenteiden kir-
joittaminen vaatii ahkeralta kieleltä esimerkiksi makrotoiminnallisuutta toimiakseen
halutulla tavalla.
Vastaavanlaisten ehtorakenteiden kirjoittaminen Haskellissa, kielen ollessa perustuksi-
aan myöten laiska, onnistuu kirjoittamalla tavallisia funktioita. Ne osat funktion mää-
rittelystä tai argumenteista, joita ei koskaan sovelleta suorituksen aikana, jätetään yk-
sinkertaisesti suorittamatta.
30
5 Ilmaisuongelma
Ilmaisuongelma kysyy, miten laajennamme olemassaolevaa koodikantaa kattamaan
uusia käyttötapauksia. Tässä luvussa tutkimme eri kielten tarjoamia ratkaisuja ilmai-
suongelman ratkaisemiseksi.
Kohdassa 5.1 käymme läpi ilmaisuongelman määritelmän ja ongelmaan liittyvän sa-
naston. Kohdassa 5.2 tarkastelemme pintapuolisesti muutamia erilaisia lähestymista-
poja eri kielistä ilmaisuongelmaan, ja kohdassa 5.3 tutkimme erityisesti Clojuren tar-
joaman ratkaisun, monimetodien, ominaisuuksia.
5.1 Ongelmakuvaus
Ilmaisuongelman määrittelemiseksi käsittelemme ongelmaa kahden käsitteen kautta:
tietotyyppi on jokin, mahdollisesti rakenteinen tyyppi. Ilmaisuongelman kannalta tie-
totyyppien ongelmallisuus syntyy niiden heterogeenisyydestä, eli oletamme, että uu-
det tietotyypit eroavat ratkaisevasti entisestään tuetuista. Operaatio on jokin funktio
tai proseduuri, jonka lähtöjoukon olisi määrä kattaa kaikki tuetut tietotyypit.
Olkoon meillä järjestelmä, jossa on toteutettuja operaatioita usealle eri tietotyypille, ja
jossa ilmaisuongelma ilmenee. Järjestelmän toteutuksesta riippuen joko uusien tieto-
tyyppien tai uusien operaatioiden lisääminen järjestelmään on mahdotonta ilman van-
han koodin muokkaamista. Ilmaisuongelmaksi kutsutaan sitä ongelmaa, jossa järjestel-
mään halutaan tuoda uusia tietotyyppejä tai operaatioita ilman vanhan muokkaamista.
Torgersen (2004) esittelee ongelman kaksi kääntöpuolta.
Tietokeskeinen järjestelmä edustaa suoraviivaista oliopohjaista lähestymistapaa, jossa
operaatiot ovat luokkien virtuaalisia metodeita ja jotka kirjoitetaan kullekin perittävälle
tietotyypille (luokat) erikseen. Uusia tietotyyppejä on helppo tuoda järjestelmään kir-
joittamalla uusi aliluokka operaatiometodeineen. Sen sijaan uusien operaatioiden tuo-
minen edellyttäisi kaikkien ennestään kirjoitettujen tietotyyppien avaamista ja muok-
kaamista.
Operaatiokeskeinen lähestymistapa edustaa enemmän funktionaalista tai proseduraa-
lista lähestymistapaa, jossa kukin operaatio toimii itsenäisenä funktionaan, ja valitsee
annetun syötteen perusteella, mitä tarkalleen ottaen tehdään. Oliopohjaisissa kielissä
31
ja suunnittelumalleissa tämä kulkee vierailijamallin (Visitor pattern) nimellä. Uusien
operaatioiden lisääminen järjestelmään on helppoa, mutta uusien tietotyyppien lisää-
minen vastaavasti edellyttää jokaisen operaation muokkaamista.
Torgersen (2004) jatkaa, että ilmaisuongelman annetun ratkaisun tulisi tukea sekä tie-
totyyppien että operaatioiden lisäämistä järjestelmään siten, että mitään vanhaa toteu-
tusta ei tarvitse muokata. Torgersen edellyttää ratkaisuilta lisäksi, että ohjelmakoodia
ei toisteta suuresti useampaan kertaan missään, ja että kaikenlaiset tietotyyppien ja
operaatioiden kombinaatiot ovat mielekkäästi toteutettuna.
Ilmaisuongelman ratkaisut ovat ratkaisuja muuttuvien vaatimusten ongelmaan. Chris
Houser (2010) mainitsee esityksessään esimerkkinä raportintuottajaoperaation, joka
tukee ensin muutamia annettuja tietotyyppejä, mutta myöhemmin pitäisi päivittää tu-
kemaan uusia.
Esimerkki
Esittelen konkreettisen Java-henkisen tapauksen, joka mukailee Houserin esityksen
(2010) esimerkkiä. Olkoon meillä tilausjärjestelmä, joka mallintaa tilauksia nimeltä
Tilaustapahtuma. Järjestelmässä on myös tarvetta pitää kirjaa varastotuotteista, ja
sitä varten siellä on luokka Varastotuote. Luokilla on toki omat kenttänsä ja ne eivät
ole suoraan yhteensopivia keskenään. Nämä ovat esimerkkimme tietotyypit.
Esitetään vaatimus, että järjestelmän pitäisi osata tuottaa HTML-raportteja kum-
mastakin tyypistä. Olio-oppien mukaisesti syntyvä luokka HtmlRaportti ei sel-
laisenaan erityisesti tue kumpaakaan tyyppiä, vaan sille täytyy tuoda erityisen
Tietorivi-rajapinnan mukaista dataa. Tietorivi sisältää kaksi julkista metodia:
haeAttribuuttienNimet() ja haeArvo(attribuutinNimi).
Vaikka Tietorivi abstrahoikin lähdedatan riittävän yleiselle tasolle, nyt ilmai-
suongelma on ilmeinen, koska alunperin luodut luokat Tilaustapahtuma ja
Varastotuote eivät toteuta tätä rajapintaa. Ongelma on lähinnä teoreettinen, jos kaik-
ki tietotyypit tulevat omasta koodikannasta. Käytännön ongelma syntyy siinä vaihees-
sa, kun raportteja tulisi tuottaa sellaisista tyypeistä, jotka tulevat kielen omista kirjas-
toista tai kolmannen osapuolen mustista laatikoista, joita ei voi manipuloida.
32
5.2 Eräitä ratkaisuja
Sovitinmalli (Adapter pattern) on eräs ilmeisin tapa ratkaista uusien tietotyyppien sopi-
vuus vanhoihin operaatioihin. Oliopohjaisissa ratkaisuissa käärimme uuden, tuettavan
tietotyypin uudeksi luokaksi, joka toteuttaa operaatioiden tarvitsemat rajapinnat. Chris
Houser (2010) mainitsee muutamia ongelmia tässä ratkaisussa. Päällimmäisin ongel-
ma on samuussemantiikan katoaminen; kääritty tietotyyppi ja sen ilmentymä ei enää
vastaa alkuperäisen luokan oliota. Lisäksi Houser nostaa esille ylläpitokoodin tarpeet-
toman suuren määrän, jota tarvitaan verraten luonnollisen ongelman ratkaisemiseksi.
Paikkakoodi (Monkey patching) on eräs joidenkin kielten tarjoama ratkaisu laajentaa
annettuja luokkia ja olioita siten, että uudet laajennokset tulevat osaksi suljettuja luok-
kia ollen näin samanvertaisia jäseniä kuin alkuperäiset. Paikkaamalla epäyhteensopivat
luokat luomalla operaatioihin sopivat metodit voidaan ilmaisuongelma ratkaista. Me-
netelmällä on monia ongelmia, joista tiedon kapseloinnin rikkoutuminen ja käytettyjen
jäsennimien potentiaaliset yhteenotot ovat kaksi merkittävintä ongelmaa. Paikkakoo-
din etuna sovittimien käyttöön verrattuna identiteetti saadaan säilytettyä alkuperäisten
kanssa, vaikka sitten alkuperäisiä luokkia manipuloimalla. Prototyyppipohjainen Ja-
vaScript ja dynaaminen oliokieli Ruby sallivat luokkien paikkaamisen. (Houser, 2010)
Yleiset funktiot (generic functions) ovat muutamassa kielessä käytetty tapa harjoit-
taa oliopohjaista ohjelmointia määrittelemällä metodeja luokan ulkopuolella. Yleisten
funktioiden nimitys tulee Common Lisp -kielestä (Seibel, 2005, luku 16), mutta tek-
niikka on vanhempi. Common Lispissä yleisen funktion kirjoittaminen muistuttaa ul-
koisesti tavallisen funktion määrittelemistä, mutta pelkkä nimi ei määritäkään yksikä-
sitteistä suoritettavaa rutiinia: yleisen funktion nimi ja sen argumenttien tyypit yhdes-
sä muodostavat tavan osoittaa yksikäsitteisesti haluttu funktio. Vaikka samalla nimellä
näyttääkin olevan määritelty metodin useita eri toteutuksia, kieli tekee ajonaikaisen lä-
hettämisen oikealle toteutukselle argumenttien tyyppien perusteella. Jos ajattelemme
oliometodeita itsenäisinä funktioina, jotka päättävät suorituksestaan annettujen argu-
menttien perusteella, ovat seuraavat kaksi tapaa kutsua metodeita samat:
metodi(olio, argumentti)
<=>
olio.metodi(argumentti)
33
Tämä on sama idea kuin uudemmissa oliopohjaisissa kielissä, joissa luokan metodit
esitellään ja joskus toteutetaankin luokan sisällä. Kun metodin “yksinomistavan” luo-
kan käsite unohdetaan, metodia kutsuva olio ja sen luokka onkin vain yksi argument-
ti metodikutsulle lisää. Yleisiä funktioita voikin kirjoittaa ja lisätä Common Lispis-
sä luokkiin mielivaltaisissa vaiheissa, missä käännösyksikössä tai moduulissa tahansa.
Näin yleinen funktio ratkaisee ilmaisuongelman: uuden tietotyypin esittely vaatii vain
tarvittavan määrän uusia yleisiä funktioita uuden luokan kanssa.
Tyypilliset nykykielet, kuten Java ja C#, tukevat metodin lähettämistä vain yhden ar-
gumentin – tavallisesti metodin luokan tyypin – mukaan. Metodit määritellään luokan
määrittelyn yhteydessä ja ne saavat liittyvän olionsa viitteen implisiittisenä argument-
tinaan.
Reflektiivisiä ominaisuuksia sisältävät kielet ja suoritusalustat voivat tarvittaessa to-
teuttaa omat toteutuksensa yleisistä funktioista. Javassa esimerkiksi voisi kukin ylei-
nen funktio kääntyä yhdeksi luokan ilmentymäksi, jolla on julkinen rekisteröintimeto-
di. Tätä metodia kutsumalla voisi uusi toteutus ilmoittaa olemassaolostaan ja tukemis-
taan tyypeistä. Rekisteröinnin ohessa nämä tiedot “yleinen funktio -luokka” säilöisi
tietorakenteeseensa.
Luokan metodi voidaan aivan yhtä hyvin päättää useamman argumentin avulla, jolloin
puhumme monilähetyksestä (multiple dispatch). Esimerkiksi Common Lisp ja Cloju-
re tukevat yleisiä funktioita monilähetyksen kanssa. Common Lispissä yleinen funk-
tio voidaan valita yhden tai useamman luokan tyypin mukaan, Clojure erityisesti vielä
antaa käyttäjän määritellä mielivaltaisen funktion, jonka tuottaman arvon mukaan lä-
hettäminen tapahtuu. Clojuren käyttämästä yleisten funktioiden toteutuksesta keskus-
telemme syvemmin kohdassa 5.3.
Scala-kielessä on useita mekanismeja ilmaisuongelman ratkaisemiseksi. Niinkutsu-
tut tapausluokat (case classes) vastaavat yleisiä funktioita tyyppipohjaisen lähetyksen
kanssa, yhden tai useamman tyypin mukaan. (Odersky, 2014a, kohta 5.4) Lisäksi Scala
tukee implisiittisiä määritelmiä (implicit definitions) metodeille, luokille ja funktioil-
le. Tämä on paikkakoodin tapainen mekanismi, mutta tyyppiturvallinen tapa laajentaa
olemassaolevia rakenteita. Niinsanotut implisiittiset näkymät tarjoavat läpinäkyvän ta-
van antaa kielen tehdä sopivia tyyppimuunnoksia kehittäjän omalla koodilla. (Odersky,
2014a, luku 7)
34
user=> (defmulti Len type)#’user/Len
user=> (defmethod Len java.lang.String [s] (.length s))user=> (Len "abc")#=> 3
user=> (defmethod Len clojure.lang.IPersistentVector[v] (count v))
user=> (Len [1 2 10 5 4])#=> 5
Kuva 4: Clojuren monimetodit ilmaisuongelman eräänä ratkaisuna.
5.3 Monimetodit
Clojuren oma ratkaisu ilmaisuongelmaan on yleisten funktioiden toteutus nimeltään
“monimetodi” (multimethod). Monimetodin julkisivuna on yksi yhteinen nimi ja yhte-
näinen joukko argumentteja, ja metodin taustalla on useita eri toteutuksia, joista sopiva
valitaan ajonaikaisesti kutsussa käytettävien argumenttien arvojen mukaan. Tätä valin-
taa ja toteutuksen kutsumista sanotaan lähettämiseksi (dispatch). Toteutuksen valinnan
tekee käyttäjän määrittelemä valitsijafunktio.
Kuva 4 näyttää lyhyen esimerkin, jossa näytetään monimetodien käyttöön kuuluvia
makroja ja erikoiskutsuja. Makrolle defmulti annetaan monimetodin nimi ja käytetty
valitsijafunktio. Tämän valitsijafunktion lähtöjoukko on sama kuin monimetodin, eikä
funktion tuottamaa paluuarvoa käytetä muuhun käsittelyyn kuin sopivan toteutuksen
löytämiseen. Makrolla defmethod vastaavasti lisätään jokin uusi toteutus monimeto-
dille.
Koodissa toteutan erilaisten arvojen pituuksia mittailevan Len-monimetodin. Merkki-
jonoilta pituudeksi määrittelemme merkkijonon merkkien lukumäärän ja kokoelmilta
yleisesti niiden sisältämien alkioiden lukumäärän. Matkimme arvon luokan mukaan
tapahtuvaa yksinkertaista lähettämistä, ja siihen käytämme Clojuren vakiokirjastoon
kuuluvaa type-funktiota valitsijafunktiona.
Ilman yhtäkään toteutusta, johon arvo lähetettäisiin, monimetodilla ei tee mitään. To-
teutusten lisäämiseksi monimetodille käytämme makroa defmethod, joka käyttäytyy
kahden ensimmäisen argumenttinsa jälkeen kuin funktioita määrittelevä defn-makro.
35
user=> (defmulti pituus count)user=> (defmethod pituus :default [_] "joku muu pituus")user=> (defmethod pituus 1 [_] "pituus yksi")user=> (defmethod pituus 2 [_] "pituus kaksi")user=> (pituus "ab")#=> "pituus kaksi"user=> (pituus "a")#=> "pituus yksi"user=> (pituus "hei maailma!")#=> "joku muu pituus"
Kuva 5: Clojuren monimetodit voivat lähettää mielivaltaisen arvon perusteella. Oletus-käsittelijän käyttöä.
Ensimmäinen argumentti on viite siihen monimetodiin, johon olemme toteutusta li-
säämässä. Toinen argumentti on se valitsijafunktion laskema arvo, jonka mukaan tämä
toteutus on valmis käsittelemään monimetodin kutsun. Makron loput argumentit ovat
yhtenevät funktioita esittelevän defn-makron kanssa.
Kuvassa 4 ensin toteutamme Len-metodin Javan String-tyypille, ja täten String-
luokkaa on jatkettu uusilla metodeilla ilman alkuperäisen koodin muokkaamista tai
uudelleenkääntämistä. Vertailun vuoksi kuvan koodin lopussa teemme vielä vastaa-
vanlaisen toimenpiteen Clojuren vektoreille.
Kuvan 4 ratkaisua voimme jatkaa uusin operaatioin, eli funktionaaliseen tapaan kir-
joittamalla uusia monimetodeita. Mitään kuvan koodista ei myöskään tarvitse muokata
tätä laajennosta varten. Vastaavasti uuden tietotyypin esitteleminen ja tukeminen vaa-
tii vain yhden uuden defmethod-toteutuksen kirjoittamisen kutakin tuettua operaatiota
kohden. Clojuren monimetodit täyttävät näiltä osin kaikki ilmaisuongelman ratkaisulta
vaatimamme ominaisuudet.
Monimetodit ja valitsijafunktio voidaan kirjoittaa lähettämään mielivaltaisten arvojen
mukaan. Kuvan 5 esimerkissämme määrittelemme pituus-nimisen monimetodin, jo-
ka delegoi varsinaisen käsittelyn eteenpäin argumentin pituuden suhteen. Funktioargu-
mentti count palauttaa sille annetun kokoelman (mukaanlukien merkkijonojen) pituu-
den. Ensin määrittelemme monimetodillemme oletuskäsittelijän, jota kutsutaan siinä
tapauksessa, kun yksikään käsittelijä ei vastaa valitsijafunktion laskemaa arvoa.
Edellisessä esimerkissä lähetämme yhden argumentin perusteella, mutta valitsijafunk-
36
user=> (defmulti collide (fn [a b] [(odd? a) (odd? b)]))user=> (defmethod collide [true false] [_ _] "hmm...")user=> (collide 1 1)#=> IllegalArgumentException No method in multimethod ’collide’#=> for dispatch value: [true true]
user=> (collide 1 2)#=> "hmm..."
Kuva 6: Clojuren monimetodit monilähetyksessä: lähettäminen toimii rakenteisen ar-von lähetyksen mukaan.
tio voi vastaanottaa useita argumentteja ja vastaavasti palauttaa rakenteisen paluuarvon
— tavallisesti vektorin — jolla on riittävästi ulottuvuutta käsitellä kaikki ongelman
sanelemat tapaukset. Tässä tapauksessa oletusarvojen määrittely ja toteuttaminen jää
valitsijafunktion harteille.
Kuvassa 6 on esimerkki kahden muuttujan funktioista ja koosteisesta valitsijafunktion
paluuarvosta. Menetelmällä voimme imitoida usean tyypin mukaan tapahtuvaa lähet-
tämistä. Lisäksi kuvan 6 testikutsusta (collide 1 1) näemme, mitä käy, kun oletus-
käsittelijää ei ole määritelty.
Monimetodien toteutukset voivat myös mennä päällekkäin keskenään siten, että saa-
tuja argumentteja olisi valmis käsittelemään useampi toteutus. Clojure antaa asettaa
tietyn toteutuksen etusijalle ristiriitaisessa tapauksessa. Myös ad hoc -hierarkioiden
määritteleminen avainsanojen välille on mahdollista. (Fogus et al., 2011) Näitä hie-
rarkioita käyttämällä monimetodien valitsijafunktioissa voidaan mallintaa tyypillinen
olioalgebra.
Clojuren monimetodeilla ratkaisemme ilmaisuongelman käyttämällä yhtä ylimääräis-
tä kutsukerrosta metodia käyttävän koodin ja metodin välillä. Tämä välikerros näkyy
suorituskyvyssä yhtenä ylimääräisenä funktiokutsuna. Monimetodit voidaan toteuttaa
millä tahansa kielellä, jonka suoritusympäristö tukee reflektion käyttöä ajonaikaiseen
argumenttien tyyppien tutkimiseen.
37
6 Kompleksisuudenhallinta
Yksinkertainen ei ole aina helppoa. Rich Hickey (2011) perustelee asiaa sanakirja-
määritelmistä johtamalla: monimutkainen tarkoittaa kietoutunutta ja sisäisesti keske-
nään riippuvista osista muodostunutta kokonaisuutta. Yksinkertainen on monimutkai-
sen vastakohta: erillisiä komponentteja, jotka eivät ole toisiinsa toivottomasti sitoutu-
neita. Yksinkertainen arkkitehtuuri voi olla vaikea saada rakennettua ilman tarpeetto-
man työläitä rakenteita. Tässä luvussa käsittelemme joitakin tekniikoita, joilla Clojure
ja kielet yleensäkin taistelevat monimutkaisia rakenteita vastaan.
Jo luvussa 5 otimme kantaa yhteen kompleksisuutta karsivaan menetelmään. Tässä lu-
vussa jatkamme muutamien muiden työkalujen kanssa. Kohdassa 6.1 esittelen sovel-
lusaluekohtaiset kielet, kohdassa 6.2 esittelen pysyvät tietorakenteet ja muuttumatto-
mat arvot kestävän logiikan taustalla, ja kohdassa 6.3 käyn läpi Clojuren tapaa hallita
muutoksia hallitusti STM-moottorin avulla.
6.1 Makrot ja sovellusaluekohtaiset kielet
Lisp-kielissä ovat aina olleet vahvasti mukana makrofunktiot, ja niiden oikeaoppinen
käyttäminen on ratkaissut ongelmia siististi. Anekdotaalisesti voisi sanoa, että oikeaop-
pisessa lisp-ohjelmoinnissa ei ratkaista ongelmia, vaan kirjoitetaan sovellusaluekohtai-
sia kieliä (domain specific languages; DSL), jotka parhaiten sopivat ongelman teemaan
ja helpottavat ratkaisun miettimistä. Paul Graham (1994, luku 1) kirjoittaa makrojen
kyvyistä muovata kieltä niin syvästi kohti tarvittavaa muotoa, että ratkaistava ongelma
alkaa tuntua triviaalilta uusien työkalujen valossa.
Sovellusaluekohtainen kieli tai sovelluskieli kehittäjän näkökulmasta voi tarkoittaa yk-
sinkertaisesti sopivaa joukkoa funktioita ja luokkia, jotka on kirjoitettu liiketoiminta-
logiikka mielessä. Tätä voi tehdä missä tahansa kielessä, mutta lispien makroilla on
tavallisesti voitu entisestään hälventää kielen omien rakenteiden näkyvyys ongelman
“tieltä”.
Myös Clojuressa kielen tarjoamien rakenteiden ja sovelluskielten välinen raja hämär-
tyy vastaavalla tavalla makrojen myötä. Kielen tarjoama vakiokirjasto käy oikein suun-
nitellulle sovelluskielelle sellaisenaan työkaluiksi. (Fogus et al., 2011, luku 13.1) Fogus
ja Houser jatkavat, että jo käyttämällä oman sovelluskielen argumentteina ja paluuar-
38
[:html[:body[:h1 "Hei maailma"][:p "Hiccup tekee näin."]]]
<html><body><h1>Hei maailma</h1><p>Hiccup tekee näin.</p>
</body></html>
Kuva 7: Hiccup kääntää Clojure-tietorakenteita (ylempi) HTML-koodiksi (alempi).
voina Clojuren omia tietotyyppejä ja jonoabstraktiota on kirjastoa käyttävän kehittäjän
saatavilla huima määrä valmiiita ja idiomaattisia työkaluja. Clojuren tietorakenteista
HTML-koodia generoiva Hiccup-kirjasto on eräs esimerkki tästä, ja sen käsittelemme
tässä kohdassa myöhemmin.
Emerick et al. (2012, luku 5) tuovat esille, kuinka kielissä yleisesti kirjoitetaan ratkai-
suja kerros kerrokselta, aina nousten abstraktiotasoissa ylöspäin. Tämä tapa mallintaa
ja toteuttaa sovellus alhaalta ylöspäin on lisp-kielillä perinteisesti käytetty tapa, koska
lisp-kehotteet (REPL) sallivat nopeatempoista koodin kokeilemista järjestelmän tilassa
ilman. Kunnollisen abstraktiotason puuttuminen johtaa tarpeettomaan koodin toistoon
ja turhaan seremoniaan (boilerplate-koodiin). Kielissä, joissa on riittävän hyvä makro-
tuki, voidaan turha toisto vähentää olemattomaksi.
“If you give someone Fortran, he has Fortran. If you give someone Lisp, he has any
language he pleases.” – Guy Steele (Fogus et al., 2011)
Perinteisten lisp-makrojen idiomaattinen toiminta on sitä, että makroilla kuvaillaan so-
vellusalueen olioita ja asioita, ja makrot kääntyvät funktiomääritelmiksi taustalla. Näin
voidaan myös optimoida suoritusaikaa vaativa laskenta tehtäväksi kääntämisen aikana.
(Fogus et al., 2011; Graham, 1994)
Fogus ja Houser (2011, luku 13.1) luonnehtivat lisp-kielistä ongelmanratkaisua niin,
että sovellusalueen ongelman käsittely alkaa ongelman formalisoinnilla sovelluskie-
leksi. Vahvasti ja staattisesti tyypitetyissä kielissä vastaava formalisointi käsittäisi käy-
tettyjen tyyppien määrittelemistä ja funktioiden lähtö- ja maalijoukkojen luonnostelua.
39
(select author(with country)(where (like :first_name "Ch%"))(order :last_name :asc)(limit 1)(offset 1))
(select author(fields :first_name :last_name)(where (or (like :last_name "C%")
(= :first_name "Mark"))))
Kuva 8: Kaksi esimerkkiä Korma-kirjaston SQL:ksi kääntyvästä kyselykielestä. Esite-tyt funktiot ja makrot on tuotu Korman nimiavaruuksista. (Emerick et al., 2012)
Makroilla voi toisaalta kirjoittaa ja luoda funktioita, toisaalta siistiä ja rakentaa jo-
tain keskitettyä tietorakennetta pinnan alla ja tarjoilla valmis tuotos kehittäjän käytet-
täväksi. Nykyaikainen deklaratiivinen määritteleminen, eli halutun toiminnallisuuden
kuvaileminen tietorakenteina, ja sen lähettäminen argumentteina rakentajafunktioille
sopii myös makroille ja datakeskeiselle Clojurelle hyvin. Makrot eivät tässä mieles-
sä enää ole välttämättömiä työvälineitä saumattoman sovelluskielen kirjoittamiseksi,
mutta optimoinnin ja esteettisen siistinnän nimissä ne voivat pitää edelleen paikkansa.
Hiccup on suosittu kolmannen osapuolen Clojure-kirjasto, joka kääntää Clojure-
tietorakenteita XML- ja HTML-pohjaiseksi lähdekoodiksi. Kuva 7 antaa esimerkin
Hiccupin toiminnasta ja sen käyttämästä syntaksista. Koska Hiccupin kaikki argumen-
tit ovat puhdasta Clojurea, on mahdollista rakentaa ja muotoilla syöte Hiccupille käyt-
tämällä Clojuren kaikkia vakiokirjaston funktioita. Ero hyvin suunnitellun funktion,
mitä Hiccup käytännössä on, ja sovelluskielen välillä on suorastaan mitätön. Sekä Fo-
gus et al. (2011) että Emerick et al. (2012) pitävät kirjastoa idiomaattisena tyyppiesi-
merkkinä hyvästä ja dataorientoituneesta sovelluskielestä.
Eräs esimerkki sovelluskielestä on .NET:n versiossa 3.5 ja C#:n versiossa 3.0 lansee-
rattu LINQ-kyselykielimoottori. LINQ:n avulla voidaan tehdä SQL-kyselyitä muistut-
tavia lausekkeita, jotka kääntäjä optimoi käytettyjen tietorakenteiden ja tietolähteiden
mukaan tehokkaasti ajettavaksi tavukoodiksi. Esimerkiksi erilaiset aggregaatiot suuris-
ta tietomassoista ovat nopeita ja samalla lyhyitä kirjoittaa LINQ-kyselyinä.
Yleinen paikka sovelluskielelle syntyy tietokantahakujen ja SQL:n käytön tarpeesta.
40
(defn meters->feet [m] (* m 3.28083989501312))
(meters->feet 9.2);=> 30.1837270341207
Kuva 9: Yksikkömuunnosfunktio yksinkertaisimmillaan. (Fogus et al., 2011)
Siinä missä SQL on itsessään jo määritelmällisesti DSL, Clojurelle löytyy useita kir-
jastoja, joilla sen tietorakenteista voidaan edelleen kääntää SQL-kyselyitä tietokannalle
ajettavaksi. Eräs näin käytetyistä kirjastoista on Korma, jonka tukemasta sovelluskie-
lestä on lyhyt esimerkki kuvassa 8. (Emerick et al., 2012) Korma käyttää enemmän
funktio- ja makropohjaisia avusteita kyselyiden rakentamisessa, vaikka puhtaampi da-
takeskeinen syöte Hiccupin tapaan olisi yhtälailla mahdollista. Kuvan 8 select-makro
palauttaa esikäännetyn kyselykuvauksen, joka voidaan edelleen antaa varsinaisen ky-
selyn suorittavalle funktiolle.
Fogus ja Houser (2011, luku 13.1) demonstroivat yksikkömuunnoksiin keskittyvän so-
velluskielen kirjoittamista puhtaalta pöydältä. Siinä missä kielestä kävisi jo yksinker-
tainen kokoelma funktioita kuvan 9 tapaan, kannattaa heidän mukaansa toteutus abstra-
hoida laajennettavuuden nimissä jo alusta alkaen hieman vapaammaksi. Oikein valitun
abstraktion kanssa on luontevaa sovittaa mukaan myös yksikköjen välisiä suhteita niin,
että myös Clojureen vähemmän perehtynyt ymmärtää, mitä määritelmillä on tarkoitus
tehdä.
He esittelevätkin yhden apufunktion ja yhden makron – yhteensä 22 riviä koodia – joil-
la kuvan 10 mukainen yksikköjä määrittelevä makro syntyy. Sovelluskielelle syntynyt
syntaksi on sellaista muotoa, että Clojureen perehtymätönkin kykenee tulkitsemaan ja
kirjoittamaan määritelmiä. Taustalla piilevän koodin vähäisyys vähentää huolimatto-
muudesta ja muista ihmissyistä johtuvia ohjelmavirheitä ja auttaa ajattelemaan ongel-
maa enemmän sovellusalueen termein.
Foguksen ja Houserin (2011) versio hieman lyhennetyssä versiossa, missä korjasin
tekstissä olleen lyöntivirheen ja kirjoitin makron lyhyemmässä, vaikka hitaammas-
sa muodossa, menee kuvan 11 listauksen tapaan. Työskentelemme siis käyttäjältä
saatujen muunnossääntöjen (kuva 10) voimin ja haluamme tuottaa säännöistä yhden
uuden funktion, joka tekee yksikkömuunnoksia ennalta kiinnitettyyn yksikköön, ku-
van tapauksessa metreiksi. Makro defunits-of tuottaa käännöksen aikana funktion
41
(defunits-of distance :m:km 1000:cm 1/100:mm [1/10 :cm]:ft 0.3048:mile [5280 :ft])
(units-of-distance 23 :mm);=> 23/1000
Kuva 10: Pituusyksiköitä määrittelevän sovelluskielen käyttöesimerkki. (Fogus et al.,2011)
units-of-distance, kun sitä on kutsuttu esimerkin syötteellä.
Säännöt kerätään makron alussa kuvaukseksi, joka on seuraavaa muotoa:
{:m 1
:km 1000
:cm 1/100
:mm [1/10 :cm]}
Tämäntapainen kuvaus (makron määritelmässä käytetty arvo units-map) käy apu-
funktiolle relative-units hyvin argumentiksi niin, että “vaikea työ” on ulkoistettu
makron määritelmästä helpommin testattavaan funktioon. Rekursiivista sisäkkäisten
määritelmien aukipurkamista varten tämä onkin hyvä idea. Kuvan 11 versio säilöö tä-
män kuvauksen sellaisenaan generoitujen funktioiden sisään ja Foguksen ja Houserin
versio laskee makrovaiheessa kaikki yksiköt valmiiksi auki pienenä optimointitoimen-
piteenä.
Makro luo lopuksi apufunktion, joka nimetään käyttäjän toiveiden mukaisesti
unit-of- -alkuiseksi symboliksi. Itse funktion runko on yksinkertainen tulo, kun käy-
tämme apufunktiota avuksemme. Tuloksena on käytöltään Foguksen ja Houserin työn
kanssa toiminnallisesti identtinen makro.
42
(defn relative-units [u units](let [spec (u units)](if (nil? spec)(throw (Exception. (str "Undefined unit " u)))(if (vector? spec)(let [[conv to] spec](* conv(relative-units to units)))
spec))))
(defmacro defunits-of [name base-unit & conversions](let [magnitude (gensym "magnitude")
unit (gensym "unit")units-map (into {base-unit 1}
(map vec (partition 2 conversions)))]‘(defn ~(symbol (str "units-of-" name))[~magnitude ~unit](* ~magnitude(relative-units ~unit ~units-map)))))
Kuva 11: Yksikkömuunnosten apufunktio ja makro. (Fogus et al., 2011)
6.2 Pysyvät tietorakenteet suunnittelussa
Pysyvät ja muuttumattomat tietorakenteet johtavat siihen, että tiedon muuttelu on eri-
tyinen, erikseen suunniteltava operaatio, ja siten koordinoitava huolellisesti. Muuttu-
maton tieto oletuksena avaa ovet koodille, joka on oletuksena puhdasta.
Clojuren pysyvät tietorakenteet pohjautuvat käytännössä kahden pysyväksi suunnitel-
lun tietorakenteen päälle: linkitettyjen listojen ja hakupuiden päälle. Chris Okasakin
väitöskirjasta (1999) löytyvien algoritmien nojalla perustelen, kuinka nämä kaksi tie-
torakennetta voidaan toteuttaa pysyvästi yksinkertaisimmissa tapauksissa.
Pysyvä lista on Clojuren tapauksessa yhteen suuntaan linkitettynä triviaalitapaus: lis-
tasta L = (L0, L1, . . . , Ln) tehtäessä uutta kopiota lisäämällä uusi alkio a listan alkuun
saadaan aikaan uusi lista, joka alkaa alkiosta a: L′ = (a, L0, . . . , Ln). Lista L alkioi-
neen uudelleenkäytetään kokonaan lisäystapauksessa. Kahta listaa xs = (xs0, . . . , xsn)
ja ys = (ys0, . . . , ysm) yhdistettäessä joudutaan lista xs kopioimaan kokonaan, koska
alkion xsn osoitin pitää kirjoittaa osoittamaan tyhjeen sijasta kohti alkiota ys0. Vastaa-
vasti ketjureaktion tavoin alkio xsn−1 pitää päivittää osoittamaan kohti uutta alkiota xs′n
43
a
b
c h
d
f
g
p
(a) Esimerkkipuu p, jossa on alkiot(a, b, c, d, f , g, h).
a
b
c h
d
e
f
g
p
d'
f'
g'
p'
(b) Päivitetty puu p′, jossa on edellisten li-säksi uusi elementti e.
Kuva 12: Kaksi puuta, joiden alkiot sisäjärjestyksessä luettuna ovat järjestyksessä.(Okasaki, 1999)
ja niin edelleen. (Okasaki, 1999)
Alkion muuttaminen ja poistaminen vastaavalla tavalla johtavat kaikkien alkioiden ko-
pioimiseen listan alusta aina poistettavaan tai muutettavaan alkioon asti. Loput listasta
säilyy käyttökelpoisena sellaisenaan, ilman kopiointia. Toinen seuraus on, että molem-
piin suuntiin linkitetty lista ei ole mielekäs ratkaisu pysyvän listan pohjaratkaisuksi,
koska joutuisimme käytännössä kopioimaan koko listan alusta loppuun muuttuvien
osoitteiden takia. Listan alkiolle tehtävät operaatiot ovat siis näin kuvatussa algorit-
missa aikavaativuusluokkaa O(n), missä n on kohteena olevan alkion sijainti, tai ylei-
semmin johdettuna listan koko.
Pysyvä puu on Clojuressa 32-haarainen hakupuu, mutta käsitelkäämme tässä binääri-
puun tapaus yksinkertaisemman esityksen takia. Oletetaan puu, jossa on alkiot sisäjär-
jestyksessä (a, b, c, d, f , g, h) niin, että arvot ovat myös samassa suuruusjärjestyksessä
kirjaimen mukaan. Listaesityksenä kyseinen puu voidaan esittää esimerkiksi seuraa-
valla tavalla: (d (b (a c)) (g ( f h))), tai visuaalisena puugraafina kuten kuvassa 12a.
Elementin e, d < e < f , lisääminen puuhun siten, että järjestys säilyy, tapahtuu aset-
tamalla se alkioiden d ja f väliin: sisäjärjestetyssä hakupuussamme solmun f vasem-
maksi lapsisolmuksi. Puun solmuelementti tietää sisältönsä lisäksi vasemman ja oikean
lapsisolmunsa osoitteet. Muuttuvan puun kanssa lisäystä varten meidän siis tarvitsisi
muuttaa vain solmua f . Pysyvän puun tapauksessa meidän täytyy korvata solmu f
uudella versiolla, samoin kuten kaikki solmun f esivanhemmat juureen saakka. Jos
aloitamme juuresta p, niin korvaamme sen uudella solmulla p′, jossa voimme uudel-
44
leenkäyttää kokonaan vasemman lapsipuun, koska muuttunut solmu sijaitsee oikean
lapsipuun alla. Toistamme tätä kunkin juuren kanssa uudelleenkäyttäen muuttumatto-
mia lapsipuita. Okasaki (1999) mainitsee, että Sarnak ja Tarjan (1986) nimesivät tämän
menetelmän polun kopioinniksi (path copying).
Kuva 12b edustaa tätä päivitettyä puuta p′ siten, että tummennetut solmut ovat uusia
ja vaaleammat ovat uudelleenkäytettyjä tai käyttämättömiä solmuja vanhasta puusta.
Näemme, että tässä binääripuun tapauksessa voimme jakaa vanhan puun rakenteesta
aina yhden lapsisolmun kultakin muuttuvalta solmulta. Kun haaroja on kahden sijaan
32, 31 haaraa voidaan uudelleenkäyttää ja yksi pitää uusia. Näin tehty muutos, kuten
tässä, tietää alkion lisäämiselle aikavaativuutta O(loga n), missä parametri a on puun
haarautumiskerroin, eli “leveys”.
Elementin poistaminen puusta seuraa kuten lisäyskin, ja elementin vaihtaminen uuteen
on järjestetyn hakupuun tapauksessa vanhan poistamisen ja uuden lisäämisen yhdistet-
ty operaatio. Kaikkien perusoperaatioiden aikavaativuus on siis luokkaa O(log n).
Pysyvä vektori seuraa Clojuren pysyvästä puuratkaisusta, eikä ole käytännössä suora-
saantinen. Vektoria tai taulukkoa voi ajatella kuvauksena indeksinumeroista arvoihin.
Clojure käyttääkin 32-haaraisia hakupuitaan vektoreina samoilla algoritmeilla kuin
puiden tapauksessa. Vektorit eivät ole tässä mielessä suorasaantisia, mutta käytännön
rajallisilla, lokaaleilla resursseilla haku on vakioaikaista: esimerkiksi 230 elementin
vektorista, jollainen vaatii pienimmillään 8 gigatavua muistia, tarvitsee tehdä vain 6
vertailua päästäkseen halutun indeksin osoittaman arvon lähettyville.
Arvot
Rich Hickey (2012) määrittelee arvon asiana, jolla on suuruus tai merkitys itsessään.
Muuttumattomuus yksinään johtaa datan arvopohjaisuuteen. Muuttuva tieto ei omaa
tällaista merkitystä, koska se on sidottu aikaan ja paikkaan, ja voi olla saavuttamatto-
missa ilman erityisen toiminnallisuuden käyttämistä. Paikkakeskeinen ohjelmointi joh-
taa puolustavaan kopiointiin, (defensive copying) jota tehdään välttääksemme näyt-
tämästä muutoksia, jotka näkyisivät kutsuvissa funktioissa tai muissa säikeissä. Sii-
nä missä muuttumaton data ja arvojen käyttäminen kannustaa funktionaaliseen ohjel-
mointiin ja puhtaisiin funktioihin, Hickey jatkaa, että kääntäen muuttuva data kannus-
taa epäpuhtaaseen, imperatiiviseen ohjelmointiin.
Hickeyn (2012) mukaan arvot ovat niin paikka-, ohjelma- kuin kieliriippumaton tapa
45
välittää tietoa. Ilman erityisiä metodeja, joita olisi välttämättä käytettävä tiedon käsit-
telemiseksi, tieto on vapaampaa ja universaalimpaa. Muuttuva data johtaa sovellusten,
kielten ja logiikan sitoutumiseen. Hickey sanookin, että arvot ovat paras rajapinta, mi-
tä API:lla voi olla. Toisin kuin älykäs, metodein kiedottu data, arvot myös aggregoi-
tuvat arvoiksi: lista lukuja on edelleen arvo, siinä missä kuvitteellinen LukuLista ei
ole. Arvoilla on lisäksi vakaa esikuva fysikaalisessa maailmassa, mistä voimme hakea
intuitiota.
Faktat ja “tämänhetkiset asianlaidat” ovat arvoja, koska aikaleimatut faktat eivät muu-
tu. Fakta, joka on pitänyt paikkansa jonain vuonna ja ei ehkä pidä nykypäivänä, on
edelleen samaa tietoa kuin syntyhetkellään – korkeintaan vanhentunutta sellaista. Kun
siirrämme tiedonjyväsen vanhentuneena syrjään ajantasaisemman tiedon edeltä, alam-
me käsitellä vanhaa faktaa historiana. Se ei muutu tai sitä ei poisteta. Tiivistetysti Hic-
keyn mukaan faktat eivät muutu; syntyy vain uusia faktoja.
6.3 Tilanhallinta ja STM
Clojuressa toimitaan pääsääntöisesti ja oletuksena pysyvillä arvoilla, mutta kielen
suunnittelussa on tunnustettu tarve muutokselle. Muutoksen oikeaoppinen ja ekspli-
siittinen hallinta johtaa samalla hyvään, säieturvalliseen ohjelmointiin. (Fogus et al.,
2011)
Clojuressa on useita viitetyyppejä (reference types), jotka vastaavat identiteettejä. Vii-
tetyyppi sekä sen viittaama arvo ovat edelleen pysyviä arvoja; pikemminkin viitteen
seuraaminen (dereferencing) on sidottu kutsuaikaan ollen siten epäpuhdas operaatio.
Erilaisia seurattavia viitetyyppejä Foguksen ja Houserin (2011) esityksen perusteella
Clojuressa ovat esimerkiksi seuraavat:
Atomi (Atom) on atomisesti päivitettävä ja haettava viitetyyppi. Atomia päivitetään
synkronisesti vertaa-ja-vaihda (compare-and-swap; CAS) -semantiikalla kutsu-
vassa säikeessä.
Viite (Ref) on koordinoitu viitetyyppi, eli useita viitteitä voi päivittää yhden tapahtu-
man sisällä siten, että kaikki päivitykset tapahtuvat atomisesti yhtenäisenä.
Agentti (Agent) on asynkroninen päivittäjä. Agentin semantiikka muihin viitetyyp-
peihin verrattuna on enemmän viestinlähetyksellistä ja toisessa prosessissa ta-
46
pahtuvaa. Agenteilla voidaan hallita esimerkiksi IO-rajoitettuja resursseja luon-
tevammin kuin muilla viitetyypeillä.
Muuttuja (Var) on säikeen paikallinen (thread-local) viitetyyppi: säieturvallisuus taa-
taan estämällä viitteen jakaminen muihin säikeisiin. Muuttujia voi tehdä sekä
leksikaalisella että dynaamisella tasolla.
Keskitymme tässä kohdassa käsittelemään atomeita ja viitteitä: nämä kaksi viitetyyp-
piä auttavat hajautettujen muutosten hallinnassa; agentit ovat lähinnä tapa siirtää las-
kentaa muille säikeille ja muuttujat ovat pääasiassa suorituskykyoptimointia varten.
Clojuren käytettävissä on myös kaikki Javan vakiokirjastosta löytyvät hajautetun las-
kennan työkalut, mukaanlukien atomiset lukot ja semaforit. Clojuren vakiokirjastossa
on myös toteutukset futuureille ja lupauksille (promises).
Useasta säikeestä saatavilla olevat viitetyypit toimivat Clojuren toteuttamassa STM
(Software Transactional Memory) -pinossa, joka toteuttaa niinsanotuista ACID-
ominaisuuksista kolme ensimmäistä: atomiset (atomic), yhtenäiset (consistent) ja eris-
tetyt (isolated) päivitykset. Ominaisuuksista neljäs, tiedon pysyvyys, (durability) ei on-
nistu määritelmällisesti vain keskusmuistissa pidettävän tiedon kanssa. (Emerick et al.,
2012)
Tapahtuman atomisuus tarkoittaa, että muutokset menevät läpi joko kaikki tai ei yk-
sikään. Keskeneräisiä tuloksia ei anneta viedä tapahtuman ulkopuolelle. Tapahtuman
yhtenäisyys tarkoittaa sitä, että tapahtuman jälkeinen tila on yhtenäisessä muodossa.
Tapahtuman eristyneisyys liittyy samanaikaisten tapahtumien keskinäiseen tiedonjaka-
mattomuuteen: eristetty tapahtuma suoritetaan kuin se olisi ainut suorituksessa oleva
rutiini. Tiedon pysyvyys takaa sen, että tapahtuman ulostulo on varmistettu pysyvään
muistiin, eikä onnistuneen tapahtuman jälkeen voi enää hävitä. (Harris et al., 2010)
Tapahtumat Clojuren mallissa toimivat ilman viitteiden erityistä lukitsemista, ja tämän
mahdollistamiseksi tapahtumat toimivat MVCC (multiversion concurrency control) -
mallin mukaisesti. MVCC on mekanismi, jolla tapahtuman käyttämät viitteet eriste-
tään muusta maailmasta tapahtuman ajaksi. Malliin kuuluvat tapahtuman lopuksi teh-
tävät jälkiehdot arvojen säilyttämisessä: jos tapahtumassa käytetty viite on muuttunut
toisessa säikeessä tapahtuman aikana, toistetaan tapahtuma uudelleen tuoreilla arvoil-
la. (Fogus et al., 2011, luku 11.1)
Käytettyjen mekanismien ansiosta keskeneräiset tapahtumat eivät koskaan estä luke-
47
function update-atomically(R, f):while forever:cur-value = deref(R)new-value = f(cur-value)if compare-and-set(R, cur-value, new-value) is Success:return
function compare-and-set(R, cur-value, new-value):if deref(R) == cur-value:set R = new-valuereturn Success
else:return Failure
Kuva 13: Perusidea vertaa-ja-vaihda -operaatioiden taustalla.
masta arvoja muilta lukijoilta, ja onnistuneen tapahtuman valmistuttua kaikki sen teke-
mät muutokset ulkomaailmaan ilmestyvät näkyville kerralla atomisesti. Toisaalta Clo-
jure olettaa, että tapahtumat ovat aina puhtaita funktioita. Kaikkia viitetyyppejä päi-
vitetään funktiokutsuilla kannustaen kehittäjiä kirjoittamaan lyhyitä ja atomisia päi-
vityksiä. Myös viitetyyppien viittaamien arvojen on oltava muuttumattomia. (Hickey,
2014)
Atominen päivitys atomille R vertaa-ja-vaihda-operaation avulla on loogisesti esitetty-
nä kuvassa 13. Päivittäminen tapahtuu niinsanotussa nopeassa silmukassa (spin loop),
ja päivitystä yritetään uudelleen niin pitkään, kunnes kaikki säikeet ovat yhtä mieltä
arvoista.
STM on yleisesti ottaen yksinkertainen ja kestävä idea säieturvalliseen ohjelmoin-
tiin. Tärkein yksinkertaistava seikka on tarjota yhtenäinen rajapinta muutoksille si-
ten, että muita idiomaattisia tapoja tehdä muutoksia ei ole. Muutoslogiikka kannuste-
taan kirjoittamaan puhtaisiin funktioihin, jotka mielellään riippuisivat vain annetuista
lokaaleista argumenteistaan, ei suljetuista arvoista. Koska muutoksia tehdään atomi-
sissa tapahtumissa, ja muuttumaton tieto voidaan jakaa säikeiden välillä sellaisenaan,
moni eksplisiittiseen lukitukseen liittyvistä ongelmista katoaa. Clojuren toteuttamassa
STM-ratkaisussa on potentiaalisesti vain kahdenlaista ongelmaa Foguksen ja Houserin
(2011) mukaan: lukittautumista (livelock) ja kirjoitusvääristymää (write skew).
Elävältä lukittautuminen tapahtuu, kun kaksi eri tapahtumaa yrittävät päivittää samaa
48
viitettä. Kumpikaan ei pääse ikinä päivityssilmukasta pois, koska toinen ehtii aina
muuttaa arvon tapahtumien aikana. Clojure määrittelee tapahtumien uudelleenkäyn-
nistymisille rajat, joiden tullessa vastaan annetaan virhe. Toinen, hieman teknisempi
tapa välttää lukittautumista ja virheitä on antaa ensimmäisenä aloitetun tapahtuman
suoriutua rauhassa, ja uudempien tapahtumien kokeilla päivittämistä silmukassa, käy-
tännössä odottaen ensimmäisenä aloittaneen valmistumista.
Kirjoitusvääristymä voi tapahtua MVCC-mallissa erityisesti silloin, kun tapahtuma
käyttää tietoa sellaisesta viitteestä, johon se ei itse kirjoita. MVCC varmistaa itse aina
päivitettävien viitteiden kohdalla, että samanaikaiset päivitykset eivät ole keskenään
ristiriitaisia. Tätä varmistusta ei tehdä automaattisesti viitteille, joista vain luetaan ta-
pahtuman sisällä. Tällöin viitteen muuttuminen tapahtuman aikana voi johtaa päivityk-
seen, joka tehdään vanhentuneen tiedon perusteella. Clojure tarjoaa tapahtuman sisäl-
lä käytettävän ensure-funktion, jolla voidaan varmistaa, että tapahtumassa käytettävä
viite ei muutu toisessa tapahtumassa suorituksen aikana. (Fogus et al., 2011, luku 11.1)
49
7 Yhteenveto
Tässä työssä tarkastelimme Clojure-kieltä ja muutamia kieleen liittyviä käsitteitä ja
ominaisuuksia. Työssä käsittelimme kielen syntaksia ja käyttöä, erilaisia sovellusalus-
toja, funktionaalista ja oliopohjaista ohjelmointia eroineen, ilmaisuongelmaa, makroja
ja sovellusaluekohtaisia kieliä, Clojuren STM-viitteitä, pysyviä tietorakenteita ja muut-
tumattomia arvoja.
Clojure on lisp-kieli, joka käyttää kaarisulkuihin käärittyjä S-lausekkeita syntaksinaan.
Syntaksiin kuuluu verraten vähän erikoistapauksia, joten se on helppo oppia. Nykyai-
kaiset perustietorakenteet – linkitetyt listat, vektorit, kuvaukset ja joukot – ovat hyvin
tuettuja, ja niille on annettu lyhyesti kirjoitettavat syntaksielementit. Clojuren tietora-
kenteet ovat pysyviä, eli ne ovat muuttumattomia ja ne jakavat rakenteensa keskenään
muistia säästääkseen. Clojuren funktiot ovat ensiarvoisia: niitä voidaan käyttää mui-
den arvojen tavoin osana tietorakenteita, muuttujia, muiden funktioiden argumentteja
ja funktioiden paluuarvoja. Clojuren tietorakenteet toteuttavat yhteisen jonoabstrak-
tion, jonka ansiosta kaikkea voidaan käsitellä polymorfisesti samoilla jonofunktioilla,
joita Clojuren vakiokirjastossa on noin sata. Clojuren lisp-kielisyys mahdollistaa myös
makrojen, eli funktioiden ohjelmakoodista ohjelmakoodiin, kirjoittamisen. Näin kieltä
voi laajentaa eteenpäin mielekkäästi omin voimin. Clojure toimii Java Virtual Machi-
nen päällä, ja Java-yhteensopivuus on hyvä.
Välitason sovellusalustat toimivat käyttöjärjestelmän ja korkean tason sovelluskoodin
välissä, ja näiden sovellustasojen hyödyntäminen on nykyisin erittäin yleistä. Sovel-
lusalustat, kuten Java Virtual Machine ja Common Language Runtime, tuovat alusta-
riippumattomuden ja yhtenäisten työkalujen lisäksi hyvät, optimoivat kääntäjät ja tul-
kit: nopeudessa JIT-tulkattu tavukoodi häviää natiivisti käännetylle ohjelmakoodille
useimmissa tapauksissa vain vähän, ja joskus se jopa voittaa natiivikoodin suoritus-
nopeudessa, koska ajonaikainen optimoija pystyy optimoimaan saman rutiinin useita
kertoja erilaisilla optimointiprofiileilla. Välitason alustalle kirjoitettua koodia ei välttä-
mättä tarvitse kääntää kuin kerran, ja tuloksena syntyvä tavukoodi käy kaikille alustan
tukemille ympäristöille sellaisenaan ilman uudelleenkääntöä.
Clojure toimii ensisijaisesti Java Virtual Machinen päällä, kuten moni muu suosittu uu-
den polven kieli, mukaanlukien Scala, Groovy, Jython ja JRuby. Java Virtual Machine
tukee dynaamista tyypitystä, reflektiota ja roskienkeruuta, ja alustan JIT-optimoijaa on
50
pitkään hiottu ja kehitetty. Toisaalta JVM käyttää tavukoodinaan hyvin oliopohjaista
kieltä, joka on suunniteltu lähinnä Javan kannalta. Esimerkiksi itsenäisten funktioiden
tukea, häntärekursion optimointia tai tukea sulkeumille ei ole. Nämä puutteet voi Clo-
juren kääntäjä ohittaa kääntämällä puuttuvat rakenteet sopiviksi oliopohjaisiksi käsit-
teiksi.
Common Language Runtime on hieman uudempi alusta kuin JVM, ja on teknisesti ke-
hittyneempi ja on suunniteltu tukemaan useita kieliparadigmoja. Clojuresta on CLR:lle
oma versionsa, ClojureCLR. Tämä versio on jäänyt vähemmälle kehitykselle kuin pää-
asiallinen JVM-versionsa.
JavaScript on hiljalleen noussut sovellusalustan asemaan verkkoselainten siivittämä-
nä viimevuosina, ja on potentiaalisesti eniten kirjoitettu kieli uusissa projekteissa. Se-
laimet kilpailevat keskenään sivunpiirron nopeudesta, ja sen seurauksena JavaScriptiä
tulkkaavat moottorit ovat muuttuneet nopeiksi, jopa JVM:n ja CLR:n veroisiksi. Ja-
vaScriptissä on kielenä omat ongelmansa, ja onkin syntynyt monia kielen puutteita
korjaavia kieliprojekteja, joiden päämääränä on kääntää omaa kieltään JavaScriptiksi,
jota onkin nyttemmin kutsuttu “webin konekieleksi”. Clojurella on oma ClojureScript-
projekti, joka kääntää Clojuren läheistä sisarkieltä JavaScriptiksi.
Luvussa 4 tarkastelimme funktionaalisen ja oliopohjaisen paradigman määritelmiä ja
paradigmojen lyhyttä historiaa kirjallisuudessa. Oliopohjaisen ohjelmoinnin teoria läh-
ti liikkeelle 50-luvulla, ja ideoita jalostettiin 60-luvulla. Smalltalk ja Simula edustavat
ensimmäisen aallon oliokieliä. Valtavirtaan oliopohjainen ohjelmointi nousi 90-luvun
alussa, kun ensin C++ ja sitten Java alkoivat kerätä suosiota.
Funktionaalinen ohjelmointi sitä vastoin syntyi matemaattisessa hengessä jo 30-luvulla
Alonzo Churchin lambdakalkyylin muodossa. John McCarthy suunnitteli kirjoitetta-
valle matematiikalle 50-luvulla syntaksin, josta syntyivät S-lausekkeet ja tietokoneilla
suoritettava lisp-kieliperhe. Milner suunnitteli ML-kielen 70-luvulla ja 80-luvulla sai-
vat alkunsa ensin kaupallinen Miranda ja sitten Mirandan avoin klooni, Haskell. Has-
kellista on kehittynyt vuosien saatossa vahva ja käytetty funktionaalinen kieli.
Oliopohjaisen ohjelmoinnin ohjenuora on mallintaa kaikki asiat olioina, jotka ovat
“substantiiveja”. Olioiden jäsenmetodit toimivat “verbeinä”. Valtakielissä tavataan
mallintaa metodit luokkien alaisiksi, ei suinkaan itsenäisiksi rutiineiksi. Tähän liittyy
älykkään datan käsite: kukin olio sisältää tarpeeksi toiminnallisuutta, jotta se osaa kä-
51
sitellä itse oman datansa yhtenäisessä ja kapseloidussa muodossa. Tämän vastakohta
on tyhmä data, tai paljas data, jolla ei ole piilotettua tilaa, eikä myöskään omia meto-
deitaan. Oliopohjaiseen paradigmaan kuuluu oleellisesti tiedon kätkentä eli kapselointi
niin, että muut oliot eivät pääse olion tietoon käsiksi. Tämä mahdollistaa kestävämmät
rajapinnat olioiden välillä, mikä johtaa ylläpidettävyyteen. Olioluokilla voi olla hierar-
kioita, ja toiminnallisuutta voidaan periä ylemmistä luokista. Alan Kay kuvaili oliopoh-
jaisen ohjelmoinnin ideoita ensimmäisenä 70-luvulla, ja määritteli, että oliopohjaisuus
on muuttumatonta tietoa ja polymorfismia.
Funktionaalinen paradigma on väljemmin määritelty käsite, ja määritelmä riippuu ko-
vasti määrittelijän koulukunnasta. Yhteisiä tekijöitä eri teoksista kuitenkin löytyy:
funktiot ovat ensiarvoisia entiteettejä ja ovat funktionaalisen kielen pienin rakennus-
osanen. Korkeamman tason funktiot ovat funktionaalisessa kielessä tuettuna. Lisäksi
funktionaalisissa kielissä tyypillisesti käsitellään enemmän arvoja kuin muistipaikkoja
sellaisenaan. Muuttumattomuus ja pysyvyys tulevat usein puhtaiden funktioiden suo-
simisen seurauksena. Puhdas funktionaalinen kieli pakottaa kielen tasolla puhtaiden
funktioiden kirjoittamiseen ja epäpuhdas funktionaalinen kieli vastaavasti ei pakota tä-
hän, vain kannustaa.
Funktionaalisissa kielissä funktioiden yhdisteet ovat luonnollisin tapa uudelleenkäyttää
rutiineja. Muuttumattomuus tuo kieleen arvosemantiikan, jonka edut näkyvät esimer-
kiksi säieturvallisessa ohjelmoinnissa ja ohjelmakoodin analysoinnin helpottumisessa.
Clojure on epäpuhdas funktionaalinen ohjelmointikieli, jossa on ensiarvoiset funktiot,
muuttumaton data ja pysyvät tietorakenteet. Clojure on lisäksi ahkerasti suorittava kie-
li, mutta laiskasti käyttäytyvien tietorakenteiden käyttö on idiomaattista ja läpitunkeva
käytäntö.
Luvussa 5 tutkimme ilmaisuongelmaa; kuinka se syntyy ja joitakin sen ratkaisuja. Il-
maisuongelma on laajenevien vaatimusten ongelma, joka syntyy kun haluamme sovit-
taa uusia tietotyyppejä tarvittavien operaatioiden kanssa yhteen, kun järjestelmässä on
jo toteutettu joukko tyyppejä ja operaatioita. Ilmaisuongelma kysyy, kuinka helppoa
on tuoda uusi operaatio tai uusi tietotyyppi ilman olemassaolevan koodin muokkaa-
mista. Ilmaisuongelman ratkaisun tulisi sallia se, että olemassaolevaa koodia ei muo-
kata lainkaan tai käännetä uudelleen. Kolmannen osapuolen toimittamissa binäärisissä
kirjastoissa ei esimerkiksi ole mahdollisuutta muokkaamiseen tai kääntämiseen.
52
Tietotyyppejä ja operaatioita voidaan kirjoittaa kahdella tavalla järjestelmään niin, että
ilmaisuongelma syntyy herkästi. Ensimmäinen tapa on kirjoittaa tietokeskeinen järjes-
telmä, joka muistuttaa arkkitehtuuriltaan perinteistä oliopohjaista hierarkiaa: meillä on
joukko luokkia, joilla on omat metodinsa. Uusi luokka on helppo tuoda järjestelmään:
riittää toteuttaa kaikki operaatioiden tarvitsemat rajapinnat. Sen sijaan uuden operaa-
tion tuominen aiheuttaa sen, että potentiaalisesti jokaiseen luokkaan tulee kirjoittaa
uusia rajapintatoteutuksia. Toinen tapa tuottaa ilmaisuongelma on kirjoittaa operaatio-
keskeinen järjestelmä, joka on kuin funktionaalinen arkkitehtuuri. Tässä järjestelmässä
meillä on funktioita tai operaatioita, jotka toimivat vapaan datan kanssa. Uusia operaa-
tioita on helppoa tuoda mukaan, mutta uudet tietotyypit voivat aiheuttaa funktioiden
uudelleenkirjoittamista.
Ilmaisuongelman yleisiä ratkaisuja ovat muunmuassa sovitinmalli, paikkakoodi ja ylei-
set funktiot. Sovitinmalli toimii olioparadigman maailmassa niin, että uusi tietotyyppi
kääritään olemassaolevaan, järjestelmän jo tuntemaan luokkaan, joka toteuttaa operaa-
tioiden tukemat rajapinnat. Tämän ongelmana on uuden datan olioidentiteetin rikkou-
tuminen ja koodin toistaminen. Paikkakoodi sen sijaan antaa kirjaston käyttäjän lisätä
uutta toiminnallisuutta ulkoiseen koodiin niin, että se käyttäytyy kuin osa alkuperäis-
tä koodia. Paikkakoodin käyttäminen johtaa kompleksisuuteen ja huonoon ylläpidet-
tävyyteen. Lisäksi päällimmäinen ongelma on nimiavaruuksien mahdolliset konfliktit:
nämä johtavat hienovaraisiin ongelmiin ja voivat olla hankalia todeta.
Kolmas ratkaisu ovat niinsanotut yleiset funktiot, eli funktiot, joille oliojärjestelmä
osaa valita oikean toteutuksen kutsuargumenttiensa perusteella. Tämä vastaa käytän-
nössä samaa asiaa kuin luokkien omat metodit, mutta nyt metodit kirjoitetaankin luo-
kan toteutuksen ulkopuolella, ja yleiset funktiot täten antavat kenen tahansa laajentaa
olemassaolevia operaatioita tukemaan uusia tietotyyppejä.
Ilmaisuongelman ratkaisuiksi Clojure tarjoaa sekä edelläkuvatut yleiset funktiot että
hieman yleisemmän idean, monimetodit. Monimetodit voivat lähettää suorituksen eri
toteutuksille ei pelkästään argumenttien tyyppien mukaan, vaan lisäksi mielivaltaisesti
määritellyn funktion – niinsanotun lähettäjäfunktion – paluuarvon mukaan.
Luvussa 6 tarkastelimme muutamia tapoja välttää liiallista monimutkaisuutta sovel-
lusarkkitehtuurissa. Nämä tekniikat ovat suoraan tuettuna Clojuressa. Monimutkainen
koodi tarkoittaa kietoutunutta koodia, eli koodia, joka riippuu liian paljon muusta koo-
dista ja sen toiminnasta. Monimutkaisen koodin vastakohta on yksinkertainen koodi:
53
komponentit eivät ole keskenään toivottoman kietoutuneita, vaan osaavat toimia ver-
raten itsenäisesti. Yksinkertainen arkkitehtuuri ei ole aina helppoa saavuttaa, mutta
Clojure yrittää tarjota hyviä työkaluja tätä päämäärää varten.
Yksi tapa hajauttaa kietoutunutta koodia yksinkertaisiksi komponenteiksi on kirjoit-
taa sovellusaluekohtaisia kieliä (DSL), jotka soveltuvat ongelman ratkaisujen kuvaile-
miseen varsinaista ohjelmointikieltä paremmin. Sovellusaluekohtaiset kielet ovat siis
käytännössä yksi tai useampi uusi abstraktiokerros. Clojure tukee lisp-kielenä mak-
rofunktioita, joita on perinteisesti käytetty hyvin apuna sovellusaluekohtaisten kielten
toteuttamisessa. Makrofunktiot eli makrot ovat funktioita, jotka kääntäjä suorittaa koo-
din lukemisen aikana käännösvaiheessa: makrot ovat siis funktioita ohjelmakoodista
ohjelmakoodiksi.
Koska makrot katoavat lopullisessa käännöksessä, eivät ne ole Clojuressa funktioi-
den kanssa yhdenvertaisia olioita: yleensä käytäntö on ollut käyttää makroja juuri sen
verran kuin on välttämätöntä. Clojure ei ahkerana kielenä esimerkiksi salli laskennan
viivästyttämistä ilman että käytettäisiin sopivaa makroa, joka käärii suoritettavan ru-
tiinin uudeksi funktioksi. Tyypillisesti makroja käytetään uusien ehtorakenteiden tai
uusien deklaratiivisten lausekkeiden muodostamiseksi. Nämä makrot puretaan kään-
nösvaiheessa olemassaoleviksi ehtolauseiksi tai funktiomääritelmiksi. Toisaalta dekla-
ratiiviset sovellusaluekohtaiset kielet on mahdollista kirjoittaa usein ilman makrojakin,
mikä on toivottava tilanne.
Muuttumaton tieto ja pysyvät tietorakenteet sallivat tietorakenteen jakavan yhteisiä
osia edellisten versioiden kanssa niin, että päivitysoperaatioiden tuloksena saadaan se-
kä pysyviä tietorakenteita että hyviä tila- ja aikavaativuusluokkia operaatioille. Töiden
pohjalla on Chris Okasakin (1999) väitöskirja. Clojure toteuttaa vektorit, kuvaukset
ja joukot 32-haaraisina puina, jotka ovat tyypillisissä käyttötilanteissa erittäin matalia
puita. Vektorit on rakenteellisen jakamisen takia toteutettu puina, mikä johtaa siihen,
että haku vektorista indeksin mukaan ei teknisesti ottaen ole vakioaikaista, mutta ma-
talasta puusta tehtävä haku on käytännössä hyvin rajattu tehtävä.
Muuttumattomuuden laajentaminen tietorakenteisiin sallii arvosemantiikan hyödyntä-
misen laajemmalti, Clojuren tapauksessa koko koodikannassa. Kun ulkopuolisien ta-
hojen, kuten säikeiden, muutoksia ei tarvitse pelätä, on datan käsittely huolettomam-
paa ja koodin pohtiminen analyyttisesti mielekkäämpää. Muuttumatonta dataa uskaltaa
myös jakaa säikeiden välillä.
54
Silloin kun tilaa on välttämättä muutettava, on Clojuressa sisäänrakennettu STM-
moottori, jossa on MVCC-yhteensopivat viitteet (ref) ja CAS-semanttiset atomit. Näin
muutoksen teko on keskitetty tarkkaan rajatulle rajapinnalle, johon on tarjolla säie-
turvalliset operaatiot. Tämä järjestelmä on soveltuvin osin ACI-yhteensopiva, eli ato-
minen, yhtenäinen ja eristetty. Clojure tarjoaa useita viitetyyppejä kuvaamaan jaetun
tiedon identiteettiä. Nämä viitteet osoittavat itsessään muuttumattomaan dataan, mut-
ta viitteen päivitys ja viitteen osoittaman arvon hakeminen ovat aikaan sidottu ope-
raatio. Clojuren käyttämässä MVCC-päivitysmallissa on ref-viitteiden käytössä vain
kaksi isompaa ongelmaa: elävältä lukittautumisen uhka ja kirjoitusvääristymä.
Clojure on vähäisten vuosiensa aikana ehtinyt kasvaa varteenotettavaksi järjestelmäta-
son kieleksi, jolla on helppoa kirjoittaa yksinkertaisia ratkaisuja kaikenlaisiin ongel-
miin. Nopeata kasvua selittävät niin onnistuneesti valitut oletukset ja suunnitteluläh-
tökohdat kuin JVM-integraatiokin, jonka ansiosta uuden kielen ei tarvitse lähteä aivan
puhtaalta pöydältä liikkeelle.
55
Viitteet
Abelson, H., Sussman, G. J. ja Sussman, J. (1996). Structure and Interpretation of
Computer Programs, 2nd Edition. MIT Press, Cambridge, MA, USA.
Atwood, J. (2007). The Principle of Least Power. url: http : / / blog .
codinghorror . com / the - principle - of - least - power/ (viitattu
20. 05. 2014).
Barendregt, H. (1997). The impact of the lambda calculus in logic and computer
science. Bulletin of Symbolic Logic 3 (2), s. 181–215.
Bloch, J. (2008). Effective Java (2nd Edition) (The Java Series). 2. painos. Prentice
Hall PTR, Upper Saddle River, NJ, USA.
Bres, Y., Serpette, B. P. ja Serrano, M. (2004). Compiling Scheme programs to .NET
Common Intermediate Language. Teoksessa: .NET Technologies 2004 Workshop
proceedings, s. 25–32.
Clojure (2014). Clojure-version 1.6.0 lopullisen lähdekoodin tilannevedos. url:
https://github.com/clojure/clojure/tree/clojure-1.6.0 (viitat-
tu 01. 07. 2014).
CoffeeScript (2014). CoffeeScript. url: http : / / coffeescript . org/ (viitattu
26. 05. 2014).
Crockford, D. (2008). JavaScript: The Good Parts. O’Reilly Media Inc., Sebastopol,
CA, USA.
ECMA-262 (2011). ECMAScript language specification. url: http : / / ecma -
international.org/publications/standards/Ecma-262.htm.
Emerick, C., Carper, B. ja Grand, C. (2012). Clojure Programming. O’Reilly Media
Inc., Sebastopol, CA, USA.
Fogus, M. ja Houser, C. (2011). The Joy Of Clojure. Manning Publications Co., Stan-
ford, CT, USA.
Freeman, E. et al. (2004). Head First Design Patterns. O’Reilly Media Inc.
Google (2014). Google Web Toolkit. url: http://www.gwtproject.org/ (viitattu
26. 05. 2014).
Gorschek, T., Tempero, E. ja Angelis, L. (2010). A Large-scale Empirical Study of
Practitioners’ Use of Object-oriented Concepts. Teoksessa: Proceedings of the
32nd ACM/IEEE International Conference on Software Engineering - Volume 1.
ICSE ’10. ACM, Cape Town, South Africa, s. 115–124. url: http://doi.acm.
org/10.1145/1806799.1806820.
56
Graham, P. (1994). On Lisp. Prentice Hall PTR, Upper Saddle River, NJ, USA.
Groovy (2014). Groovy. url: http : / / groovy . codehaus . org/ (viitattu
27. 06. 2014).
Halloway, S. (2009). Programming Clojure. The Pragmatic Bookshelf.
Hanselman, S. (2011). JavaScript is Assembly Language for the
Web. url: http : / / www . hanselman . com / blog /
JavaScriptIsAssemblyLanguageForTheWebPart2MadnessOrJustInsanity.
aspx (viitattu 22. 05. 2014).
Harris, T., Larus, J. ja Rajwar, R. (2010). Transactional Memory, 2nd edition. Morgan
& Claypool.
Haste (2014). The Haste Compiler. url: http : / / haste - lang . org/ (viitattu
27. 05. 2014).
Hickey, R. (2011). Simple Made Easy. Strange Loop -esitys. 20.10.2011.
— (2012). The Value of Values. GOTO Copenhagen -esitys. 22.5.2012.
— (2014). Clojure refs. url: http://clojure.org/refs (viitattu 26. 09. 2015).
Holmevik, J. R. (1994). Compiling SIMULA: A historical study of technological ge-
nesis. Annals of the History of Computing, IEEE 16 (4), s. 25–37.
Houser, C. (2010). Clojure’s Solutions to the Expression Problem. url: http : / /
www.infoq.com/presentations/Clojure-Expression-Problem (viitattu
08. 01. 2015).
Hughes, J. (1989). Why functional programming matters. Computer Journal 32 (2),
s. 98–107.
Hutton, G. (2007). Programming in Haskell. Cambridge University Press Cambridge.
JRuby (2014). JRuby. url: http://jruby.org/ (viitattu 27. 06. 2014).
Jython (2014). The Jython Project. url: http : / / www . jython . org/ (viitattu
27. 06. 2014).
Kay, A. C. (1993). The Early History of Smalltalk. Teoksessa: The Second ACM
SIGPLAN Conference on History of Programming Languages. HOPL-II. ACM,
Cambridge, Massachusetts, USA, s. 69–95. url: http://doi.acm.org/10.
1145/154766.155364.
McNamara, B. ja Smaragdakis, Y. (2000). Functional programming in C++. ACM
SIGPLAN Notices 35 (9), s. 118–129.
Microsoft (2014). TypeScript. url: http://www.typescriptlang.org/ (viitattu
26. 05. 2014).
57
Microsoft (2015). What is .NET Core. url: http://docs.asp.net/en/latest/
conceptual- overview/dotnetcore.html#what- is- net- core (viitattu
05. 09. 2015).
Mono Project (2015). Mono Compatibility. url: http://www.mono-project.com/
docs/about-mono/compatibility/ (viitattu 05. 09. 2015).
Monteiro, M. et al. (2005). Compiling Non-strict Functional Languages for the. NET
Platform. J. Universal Computer Science 11 (7), s. 1255–1274.
Mozilla (2014). Are We Fast Yet. url: http : / / arewefastyet . com (viitattu
01. 09. 2015).
Nierstrasz, O. (1989). A Survey of Object-Oriented Concepts. Teoksessa: Object-
Oriented Concepts, Databases and Applications. ACM Press ja Addison Wesley,
s. 3–21.
Odersky, M. et al. (2014a). Scala Language Specification, version 2.11. url: http:
//www.scala-lang.org/files/archive/spec/2.11/ (viitattu 23. 02. 2015).
Odersky, M. (2014b). What is Scala? url: http://www.scala-lang.org/what-
is-scala.html (viitattu 27. 06. 2014).
Okasaki, C. (1999). Purely Functional Data Structures. Cambridge University Press,
New York, NY, USA.
Oracle (2015). Java 8 java.util.function-paketin referenssidokumentaatio. url: https:
/ / docs . oracle . com / javase / 8 / docs / api / java / util / function /
package-summary.html (viitattu 18. 06. 2015).
Peyton Jones, S. L. ja Wadler, P. (1993). Imperative functional programming. Teokses-
sa: Proceedings of the 20th ACM SIGPLAN-SIGACT symposium on Principles of
programming languages. ACM, s. 71–84.
Sarnak, N. ja Tarjan, R. E. (1986). Planar point location using persistent search trees.
Communications of the ACM 29 (7), s. 669–679.
Schärli, N., Black, A. P. ja Ducasse, S. (2004). Object-oriented Encapsulation for
Dynamically Typed Languages. Teoksessa: Proceedings of the 19th Annual ACM
SIGPLAN Conference on Object-oriented Programming, Systems, Languages, and
Applications. OOPSLA ’04. ACM, Vancouver, BC, Canada, s. 130–149. url:
http://doi.acm.org/10.1145/1028976.1028988.
Seibel, P. (2005). Practical Common Lisp. Apress. url: http://gigamonkeys.com/
book/.
58
Serpette, B. P. ja Serrano, M. (2002). Compiling Scheme to JVM bytecode: a perfor-
mance study. Teoksessa: Proceedings of the 7th International Conference on Func-
tional Programming, s. 259–270.
Singer, J. (2003). JVM versus CLR: a comparative study. Teoksessa: Proceedings of
the 2nd international conference on Principles and practice of programming in
Java. Computer Science Press, Inc., s. 167–169.
Sun Microsystems (1996). JavaSoft Ships Java 1.0. url: http://tech-insider.
org/java/research/1996/0123.html (viitattu 28. 04. 2014).
Torgersen, M. (2004). The expression problem revisited. Teoksessa: ECOOP 2004–
Object-Oriented Programming. Springer, s. 123–146.
Turner, D. A. (1995). Elementary strong functional programming. Teoksessa: Functio-
nal Programming Languages in Education. Springer, s. 1–13.
Weiss, T. (2014). Compiling Lambda Expressions: Scala vs. Java 8. url: http://
www.takipiblog.com/2014/01/16/compiling-lambda-expressions-
scala-vs-java-8/ (viitattu 14. 05. 2014).
Wirfs-Brock, A. (2013). JavaScript: The Machine Language of the Ambient Computing
Era. url: http://www.slideshare.net/allenwb/fronttrends-awb (viitattu
22. 05. 2014).
Yegge, S. (2006). Execution in the Kingdom of Nouns. url: http://www.eecis.
udel . edu / ~decker / courses / 280f07 / paper / KingJava . pdf (viitattu
20. 03. 2014).
59