Clojure, funktionaalinen Lisp murre Principles of Programming Languages, S2015 Jukka Pekka Venttola & Pietari Heino Taustaa Clojuren pohjana on käytetty Lisp ohjelmointikieltä, jonka historia ulottuu 1950 luvulle asti. Lisp on monen lähestymistavan ohjelmointikieli, jossa pääasiallisena tietorakenteena toimivat linkitetyt listat. Eräs toinen Lispin erikoisemmista ominaisuuksista, varsinkin C/C++ taustaiselle ohjelmoijalle, on mahdollisuus käsitellä ohjelmakoodia datana. Lisäksi Lisp on hyvin helposti laajennettavissa ja muokattavissa, mikä johtaa siihen, että Lispiä pystyy muokkaamaan ohjelmointitarpeen mukaan sekä rakentamaan sisäkkäisiää ohjelmointikieliä. Perusajatukseltaan Lisp perustuu Alonzo Churchin lamdakalkyyliin. Suunnitteluperiaatteet Clojuren ensimmäinen versio julkaistiin vuonna 2007, pääkehittäjänään Rich Hickey. Omien sanojensa mukaan hän alkoi kehittää Clojurea, sillä hän halusi funktionaalisen Lisp murteen, joka on kiinteästi yhteydessä toteutusalustaan ja on suunniteltu vahvasti rinnakkaisuutta varten. Yleisimmistä Lisp murteista (Common Lisp ja Scheme) puuttui standardoitu tuki rinnakkaisuudelle ja tietorakenteet ovat lähtökohtaisesti muuttuvia. Funktionaalisuuden perusajatukseen kuuluu kuitenkin tietorakenteiden muuttumattomuus sekä sivuvaikutuksettomat funktiot. Funktionaalisuutta olisi ollut mahdollista noudattaa Lispissä käytäntöjen ja sopimusten kautta, mutta tietorakenteita olisi silti ollut mahdollista muuttaa. Clojuressa tietorakenteet ovat lähtökohtaisesti muuttumattomia, jolloin rinnakkaisuuden toteuttaminen muuttuu likipitäen triviaaliksi. Jos dataa ei pysty muuttamaan, ei ole mitään väliä sillä, koska ja kuka siihen pääse käsiksi. Muuttumattomuuden lisäksi tiukka yhteensovitus Javan virtuaalikoneen kanssa johtaa siihen, että Clojure ei ole suoraan taaksepäin yhteensopiva, mikä vapauttaa Clojuren useista standardoitujen Lisp murteiden asettamista rajoitteista. Puhtaat funktionaaliset kielet (esim Haskell) ovat usein vahvasti tyypitettyjä, mutta Hickey loi Clojuren tyypitykseltään dynaamiseksi. Clojurea käytetään pääsääntöisesti yhdessä Javan virtuaalikoneen (JVM) kanssa kääntämällä lähdekoodi Javan tavukoodiksi. Käännöksestä huolimatta Clojure pysyy täysin dynaamisena(tähän lisäinfoa tai tarkennusta) Mahdollista on lisäksi Java koodin kutsumisen suoraan lähdekoodista mahdollisten tyyppivinkkien avulla. JVM:n lisäksi Clojurelle löytyy JavaScript kääntäjä ClojureScript, joka tuottaa Google Closure kääntäjälle sopivaa koodia optimointia varten. Myös Microsoftin.Net Frameworkin Common Language Runtimelle löytyy toteutus nimeltä ClojureCLR.
Ominaisuudet Malli ja identiteetti Siinä missä C++:n Object Oriented Programming (OOP) mallissa on olio, jolla on jäsenmuuttujia, jotka muuttuvat, Clojuressa on malli ja identiteetti. OOP:ssa olion jäsenmuuttuja on jokin muistiosoite, jonka kuvastama data muuttuu, esimerkiksi ihmisen ikä kasvaa. Se kuitenkaan mielletään samaksi jäsenmuuttujaksi, joka vain kokee mutaation ja muuttuu ohjelman suorituksen edetessä. Clojuressa ajattelu on toisenlainen: on identiteetti (esim. luennoitsija), jolla on tila (esim. nimi), joka on arvo (esim. Matti ), joka vaihtuu (ei muutu) ajan kuluessa toiseen arvoon (esim. Bitti ). Toisin sanoen siis identiteetillä on (=> identiteetti omistaa) tila, joka voi vaihtua toiseksi tilaksi tilanteesta riippuen, mutta vaihdos ei muuta aiemmin omistetun tilan arvoa. Nämä tilat Clojuressa on samanlaisia arvoja kuin imperatiivisten kielten perusarvot. Siinä missä 42 on kaikissa ohjelmointikielissä numero, joka itsessään ei muutu, mutta joka voidaan korvata toisella numerolla, jos numero on liitetty muuttujaan, ovat Clojuren tilat samalla tavalla muuttumattomia arvoja. Nimet Bitti ja Matti ovat tiloja, jotka joko ovat tai eivät ole identiteetillä luennoitsija, mutta itsessään ne eivät koskaan muutu niiden luomisen jälkeen. Jos Clojuressa vaikuttaa siltä, että identiteetti muuttuu tavalla tai toisella, niin se on seurausta identiteetin tilojen muutoksista (vaihdoksista). Clojure siis haluaa, että voidaan puhua identiteetistä Yhdysvallat, mutta se voi tarkoittaa eri asiaa eri tilojen vuoksi ihmisille, jotka ovat eläneet eri vuosikymmenillä ja kokeneet eri Yhdysvallat. Ihmisten kokemukset eivät toki muuta Yhdysvaltoja tai eri historian tapahtumat toisiaan. Samaa logiikkaa noudattaen myös muuttujien arvot ovat muuttumattomia funktionaaliseen tapaan. Var ja muutokset muuttujissa Clojuressa on mahdollista käyttää viitettä muuttujaan, jonka tila vaihtuu toiseksi. Se tehdään Var tyyppisen muuttujan avulla, jolle alustetaan jokin alkuarvo. Alkuarvo (eli tila) ei itsessään koskaan muutu, mutta Var voidaan muuttaa osoittamaan toiseen arvoon (tilaan), joka toimii käytännössä samoin kuin jos arvo olisi muuttunut toiseksi. Jokaisella Varilla on globaali kiinnitys (binding) johonkin alkuarvoon, mutta ohjelman elinkaaren aikana jokaisella säikeellä voi olla oma kiinnitys samalle Varille. Toisin sanoen eri säikeet voivat alkaa käsitellä samaa aluksi tietyllä tavalla alustettua Varia, mutta myöhemmin tehdä muutoksia siihen kiinnittäen Varin osoituksen säikeelle sopivaan paikkaan. Vain se säie, joka on tehnyt kiinnityksen Varille (ja niin muuttanut sen muun laiseksi kuin alkuperäinen tila), näkee muokkaamansa Varin. Toiset säikeet näkevät vain omat versionsa tai alkuperäisen globaalin Varin. Viittaukset ja agentit Kun usean säikeen halutaan käyttävän samaa dataa, voidaan sitä käsitellä Refs viittauksen avulla. Refs tyypin muuttujaa käsitellään tietokantajärjestelmistä tutuin transaktioin, joissa varmistetaan, että jos jotakin muutosta yritetään, niin se joko onnistuu täydellisesti tai mitään muutosta ei tapahdu. Clojure pitää tästä huolen eikä ohjelmoijan tarvitse välittää transaktioista tai kirjoittaa niitä ohjelmakoodiin mitenkään. Refs itsessään on viite tiettyyn
paikkaan ohjelman sisällä, jossa on käsiteltävää dataa. Refs voidaan muuttaa transaktion avulla osoittamaan toiseen paikkaan, jolloin näennäisesti myös data muuttuu. Näin voidaan usealle säikeelle antaa käsiteltävä data, jonka muutokset nähdään aina uutena kirjoitus /lukualueena ja vanhan hylkäämisenä. Agentti puolestaan on viite tiettyyn paikkaan ohjelman sisällä, mutta eroaa Refsistä siten, että sen paikka ei muutu Agentin muuttuessa. Refsin muutos aiheutti transaktion ja osoituksen uuteen paikkaan, jossa uusi, muutettu data sijaitsee. Agentin muutos taas aiheuttaa alkuperäisen data alueen sisällön korvaamisen uudella datalla. Kaikki Agentin muutokset tehdään toimintojen (Actions) avulla, jotka on määritelty Agenteille. Niiden paluuarvo on Agentin uusi tila, joka kirjoitetaan vanhan päälle. Roskienkeruu Clojuressa on käytössä Javan roskienkeruu, koska Clojure ohjelma ajetaan käännetystä tavukoodista Javan virtuaalikoneella (JVM). Makrot Clojure tukee makroja, joilla voi lisätä uusia toiminnallisuuksia ohjelmaan ajonaikaisesti. Yleinen esimerkki on Clojuren varattu sana when, joka on oikeasti vain do :sta ja if: istä koostuva makro. Myös unless on oikeasti makro, joka perustuu if: iin. (macroexpand '(when boolean expression expression 1 expression 2 expression 3)) ; => (if boolean expression (do expression 1 expression 2 expression 3)) Rinnakkaisuus Clojuressa rinnakkaisuus on toteutettu säikeiden avulla. Datan jakaminen säikeiden kesken on melko vaivatonta datan muuttumattomuudesta johtuen.tämän lisäksi Clojure tarjoaa kehittäjän käyttööön työkaluja säikeiden ja niiden datan synkronoimiseksi. Multimetodit Multimetodit ovat tapa toteuttaa ajonaikaista polymorfismia funktionaalisissa ohjelmointikielissä. Clojuren tapauksessa. Se muistuttaa hieman olio ohjelmoinnissa käytettyä funktioiden ylikuormitusta, joka tapahtuu käännösaikana. Multimetodeja ovat sellaiset funktiot, joiden paluuarvon ja parametrien tyypit sidotaan vasta suoritusvaiheessa riippuen metodin kutsutavasta. Clojuren laajennukset Lispiin Clojure laajentaa hieman Lispin ajatusta ohjelmakoodista datana laajentamalla code as data systeemiä käsittämään listojen lisäksi vektorit ja mapit.
Tietotyypit Kuten mainittua, Clojure on dynaamisesti tyypitetty kieli. Vaikka suurin osa funktionaalisista kielistä on vahvasti tyypitettyjä, Clojure on dynaaminen siinä missä muutkin Lisp kielet. Dynaamisuutensa ansiosta Clojure koodia on mahdollista muokata ajon aikana luomalla esimerkiksi uusia funktioita ja makroja. Clojuresta löytyy yleisimmät tietotyypit, kuten int, float, vector, set ja map, mutta myös rationaaliluku, rational. {:foo "bar" 3 4} // map, jossa avaimina :foo ja 3 (/ 7 49) // rational, evaluoituu 7/49 = 1/7 (/ 7.0 49) // float, evaluoituu 0.14285714285714285 Suoritustehokriittisissä ohjelmakohdissa on mahdollista käyttää tyyppivihjeitä (type hints), joiden avulla kääntäjälle kerrotaan käsiteltävän muuttujan tyyppi. Käännösaikana kääntäjä käy läpi (resolve) ne kaikki kohdat, joissa on käytetty tyyppivihjeitä, eikä jätä toimenpidettä ajonaikaiseksi. Käytännössä pienellä määrällä tyyppihvijeitä ohjelmasta saadaan helposti lähes kokonaan tyypitetty, koska kääntäjän on ratkaistava myös kaikki ne ohjelmakoodin kohdat, joissa viitataan tyyppivihjeitä käyttäviin osioihin. Tyyppivihjeet annetaan [tägeinä] ohjelmakoodin seassa. Dynaaminen vs. vihjeistetty tyypitys (defn len [x] (.length x)) (defn len2 [^String x] (.length x)) // funktion määrittely // määrittely tyyppivihjeellä user=> (time (reduce + (map len (repeat 1000000 "asdf")))) "Elapsed time: 3007.198 msecs" 4000000 user=> (time (reduce + (map len2 (repeat 1000000 "asdf")))) "Elapsed time: 308.045 msecs" 4000000 Yllä olevat funktiot yhdistelevät merkkijonoja, laittavat niitä tietorakenteisiin ja lopulta laskevat merkkien määrää. Alempi versio, jossa on käytetty tyyppivihjettä, on suoritusajaltaan 10 % tyyppivihjeettömästä versiosta.
Syntaksi Clojuressa kaikki koostuu listoista. Listojen alussa kerrotaan, mikä operaatio halutaan suorittaa, minkä jälkeen listassa annetaan parametrit. Listoja voi myös olla sisäkkäin ja ne voivat olla mielivaltaisen pituisia. ( * 4 5 ) laskee 4*5 ( + 4 5 6 7 8 9 ) laskee 4+5+6+7+8+9 ( 4 ( * 2 2 ) ) laskee 4 (2*2) Myös funktiot määritellään Clojuressa listojen avulla. Neliöjuuren laskeminen sq nimisellä funktiolla: ( defn sq [ x ] ( * x x ) ) sq kertoo parametrinsa itsellään ( sq 4 ) laskee 4*4 Clojuressa on myös anonyymejä eli nimettömiä funktioita. Kun määrittelee anonyymin funktion, Clojure suorittaa sen välittömästi tulostamalla funktion sisällön. ( fn [ x ] ( * x x ) ) $eval30463$fn 30464@86a8fa1> Nimetöntä funktiota voi kutsua Clojuren listaesitystapaa käyttäen: ( ( fn [ x ] ( * x x ) ) 10 ) suorittaa: ( operaatio argumentti ) eli 10 * 10 Aiemmin käytetty defn funktion määrittelyssä on oikeasti lumetta ja konepellin alla suoritetaan def anonyymille funktiolle. Toisin sanoen: ( defn sq [ x ] ( * x x ) ) <==> ( def sq ( fn [ x ] ( * x x ) ) ) def määrittelee siis nimen sq ja yhdistää sen listaan, joka tässä tapauksessa on anonyymi funktio, joka laskee neliön annetulle parametrille. Silmukassa operointi: ( dotimes [ i 3 ] ( println i ) ) // 0 // 1 // 2
Sovelluskohteita Clojure ei ole lainkaan käyttöjärjestelmäriippuvainen, johtuen sen tiukasta integraatiosta JVM:n kanssa. Käytännössä mikä tahansa ympäristö, jossa JVM on mahdollistaa saada toimimaan, mahdollistaa Clojurella toteutetun ohjelmiston ajamisen. Lisää käyttömahdollisuuksia saadaan ClojureScriptin sekä ClojureCLR:n kautta. Vahvan rinnakkaisuustukensa takia Clojure soveltuu hyvin myös suurten datamäärien käsittelemiseen sekä analysointiin. Nykyisin Clojurea käyteyään lisäksi jonkin verran myös webbipuolella sekä back end että front end kehityksessä.
Lähteet: http://clojure.org http://www.braveclojure.com/writing macros/