1. joulukuu 2010, 18:51

Clojure ja Haskell

Funktionaalinen ohjelmointi; Haskell. Niinhän se on yleensä lukenut, mutta nyt olen tainnut päästä Clojuresta kiinni. Clojure on vain käytännönläheisempi ja jopa helpommin lähestyttävä kuin tuo modernien funktionaalisten kielien kingi. Olisi monia syitä kumpaakin kieltä vastaan ja puolesta.

Haskell

Matematiikkaa lukeneena ihastuin funktionaalisiin ohjelmointikieliin Haskellin kautta, koska Haskell osaa hienosti johtaa ja vedota matemaattisiin ominaisuuksiin ohjelmissa. Funktiokutsuissa on pääpainotus, ja Haskell tekee sen kuten pitääkin. Ja kaikki hienot, vahvasti rekursiota hyödyntävät listafunktiot ja tietorakenteet, kaikki ovat kaikin puolin matemaatikon näkökulmasta upeita asioita. Puhtaita, upeita asioita.

Haskell on myös varsin tehokas kieli. GHC kääntää hyvää natiivista jälkeä, vaikka ihan Ocamlin tasolle ei päästäisikään. TCO, staattinen tyypitys ja muut optimoinnit tekevät koodauksesta parhaimmillaan huoletonta — voin huoletta tehdä tämän asian list comprehensionilla, koska kääntäjä osaa purkaa sen auki käännösvaiheessa.

Miksi sitten Haskell pölyttyy hyllyssä, ja annan itseni valloittua Clojuren kanssa?

Clojure

Tämä varsin uusi kieli on ollut minunkin blogissani monta kertaa hehkutettavana, joten kerron vain uusimman valaistukseni johdosta tulleen pohdinnan tänne.

Ensinnäkin olin pitkään siinä uskossa, että Clojure olisi jotenkin erityisen hidas Javaan verrattuna. Syynä oli kai joku blogikirjoitelma jonkun algoritmin Python-toteutuksen ylivertaisuudesta Clojureen nähden. Sen jäädessä kummittelemaan mieliin kuitenkin kaikki muut tulokset tuntuvat puhuvan päinvastaista jälkeä. Java itsessään nopeutuu alvariinsa, ja jos kirjoittaa Clojurensa ilman hidastavaa (tai niin he sanovat) reflektiota, se yltää melko tarkasti samoihin nopeuslukemiin kuin Java. Puhutaan siis nopeuksien kärjestä! No, kielien nopeustestailut sanovat muuta. Näkisin kuitenkin Javan tehostumisen heijastuvat Clojuren tehokkuustumisessa samaan tapaan. Erittäin hyvältä vaikuttaa.

Mikä sitten tekee Clojuresta oikeasti Haskellia mielenkiintoisemman (se ei ole puhdas tehokkuus. Haskellista (GHC) huokuu tehokkuutta.) ja ehkäpä helpommin lähestyttävän on sen syntaksin helppous. Luitte muuten aivan oikein: lisp-syntaksin on yksinkertaisuudessaan oltava eräs helpoimpia kielioppeja, mitä tulee mieleen. Haskell ja C++ molemmat tulevat välttämättä mieleen, kun puhutaan monimutkaisista ja hyvinkin kaoottisista syntakseista. Molemmat ovat massiivisia ja helposti kustomoitavissa operaattorien ylikuormituksen avulla. Lisp-murteena Clojure on vain kasa funktiokutsuja, joita pitää ryhmitellä epäselvyyksien välttämiseksi.

Asia ei ole sen monimutkaisempaa. Useimpia tuntuu vaivaavan myös operaattorien esilaitto, prefiksaus tuon C-kielen valitseman infiksaamisen sijaan. Mutta kuinka usein koodissa sitten pitää käytellä noita aritmeettisia perusoperaatioita, kuten +,-,*,/? Ylivoimaisesti suurin massa koodista koostuu pitkistä funktiokutsuista parametreineen, ja ne ovat kaikissa kielissä prefix-notaatiossa. Täytyy myöntää, että joskus esimerkiksi lispin cons ja Clojuren conj menevät vähän sekaisin, koska listan käsittelyssä (lähinnä konkatenaatiossa) olisi kiva käytellä infix-notaatiota. Tässä taistelussa, esteettisyydessä, Haskell voittaa kivojen merkintöjensä ansiosta:

A ++ B       --haskell
a:A          --haskell, liitetään alkio listan alkuun
(concat A B) ;Clojure
(conj A a)   ;Clojure, liitetään alkio joukkoon
(cons a A)   ;Clojure, liitetään alkio listan alkuun

Pieni juttu, iso merkitys. Edellä oli Clojurelle ominainen conj, joka ottaa vastaan ensin joukon ja sitten alkion. Koko funktio on vähän ikävä aloittelijan kannalta, sillä sen toiminta vaihtelee käytetyn kokoelman tyypin mukaan. Ja ikävältä se tuntuu muutenkin, sillä moni Clojuren funktio tuntuu ottavan mielellään mitä tahansa kokoelmia vastaan, ja sitten palauttavat vain listoja. Pitäisi varmaan tyytyä aina listoihin silloin, kun käsiteltävät tietomäärät tiedetään pieniksi.

Mutta koko S-lausekkeisiin liittyvä kielioppi on idioottivarmaa muuten. Jahka siihen tottuu, on se niin vesiselvää ja helppoa. Kun noita sulkuja pitää käytellä ahkerasti, jokainen sulkupari ja sen sisälle jäävä data on todellakin yksi yksikkö, jonka voi siirtää koodissa melko huoletta mihin tahansa paikkaan, eikä tarvitse pelätä, että jokin syntaksipoikkeus pomppaisi pensaan takaa. Samaan huolettomuuteen perustuu myös LISPien makrot. Mitään ei voi mennä pieleen, jos makro käsittelee vain omien sulkujensa sisällä olevaa dataa. Makrojen hyvistä puolista en ole vielä 100-prosenttisen vakuuttunut, mutta kyllä ne ainakin koodia lyhentävät.

Tarvitsisikohan Clojure edes makroja, jos se olisi Haskellin tapaan oletuksena täysin laiska? Clojuressa on laiskoja tietorakenteita (lazy-seq) mutta silti oppaissa sanotaan, että funktiokutsua varten kaikki argumentit evaluoidaan läpi. Tässä ollaan valittu tehokkuuden ja käytännöllisyyden tie, eikä puhdasoppista matematiikan ja Haskellin tietä. Ikävä homma.

Se onkin ikävää. Minulla on Haskell, jonka syntaksia en osaa vielä kokonaan. Haskellissa on paljon upeita matemaattisia värkkejä ja leluja, sekä teoreettista pohjaa tehdä vaikka mitä. Clojuressa ei voi edes rekursiota toteuttaa ilman kikkoja: Javahan ei tue TCO:ta. Huoleton (mutuaalinen) rekursio tuntuu hyvältä kielessä. Jopa Ocamlin ylimääräistä avainsanaa rec olen katsonut kieroon, joten tämä Clojuren valitsema loop-recur -rakenne tuntuu kaikista jäykimmältä.

Ei Clojurea voi kuitenkaan jäykäksi sanoa. Ei missään nimessä. Olen tutkinut joitain Javan kirjastoja (mikä on taas yksi erittäin tärkeä syy Clojuren käytölle: valtava “ekosysteemi” erilaisia kirjastoja. Pythonit sun muut jäävät kirkkaasti toiseksi) Clojuren avulla, ja se on ensinnäkin aivan älyttömän helppoa ja joustavaa. Ei mitään tyypillistä “lue-apia -> koodaa kokeilu -> käännä -> aja” -sykliä, vaan onelinereitä suoraan APIsta kääntäen mielessään vauhdilla Clojureksi. Kokeilin MPD-klienttiä Clojurella REPLissä ja se lähtisi pienen kirjoittelun jälkeen varmaan 20-30 koodirivillä toimimaan kivasti komentoriviltä! Ei taatusti tunnu yhtään siltä kivikautisen kankealta Javalta, jonka me tunnemme.

Javan valmiiden kirjastojen lukumäärä ei kuitenkaan lopullisesti sysää harrastelijaa pois Haskellista. Haskellissahan on vastaava, kuulema loistavasti toteutettu FFI -rajapinta, jonka välityksellä saa kaiken C-kielisen toimimaan osana funktionaalista Haskellia. Clojure tekee Java-yhteistyön vähintään yhtä kivasti, mutta toinen seikka on vielä tärkeämpi Javaan liittyen: JVM on porttautuva alusta. Jos kirjoitan kivan Clojureohjelman, joka käyttää vaikka Swingiä graafisena kaverinaan niin se kyllä toimii Windowsissa sitten heittämällä. Se nostaa panoksia kovasti. Haskellia joutuisin kääntelemään erikseen joka alustalle. Grafiikkakirjastoja kuten wxHaskellia käyttämällä selviäisin ongelmista helposti, mutta jotain siellä aina ilmaantuu. Clojure kaikkien näiden puoliensa avulla tuntuu ensisijaisesti huolettomalta:

  1. Ohjelmasi toimii melkein kaikkialla
  2. Jos funktionaalinen ratkaisu liian vaikea, voi turvautua Javan tarjoamaan suojaverkkoon ja kolmansien osapuolten kirjastojahan riittää
  3. Dynaaminen kehitys, dynaaminen muuttujisto
  4. Syntaksi on idioottivarma: ei epäselvyyksiä
  5. Clojurekoodi vastaavaan Javakoodiin verrattuna on murto-osia pitkä

Tästä blogiviestistä tulee hankalan kirjoitusympäristön takia vähän epäselvä sillisalaatti, mutta yritetään kuitenkin. Clojuressa toteutuu Lispin nätti suunnittelukonsepti: bottom-up -kehitysmalli. Staattisesti tyypitetyissä kielissä (omat suosikkini C++ ja Haskell ovat molemmat malliesimerkkejä siitä, miten staattinen tyypitys pitää toteuttaa) joudutaan uhraamaan aika paljon aikaa projektin suunnitteluun. Siksi, että koko ohjelmalogiikka rakentuu määriteltyjen tietotyyppien käsittelemiselle. Kumpainenkin näistä kielistä toteuttaa staattisen tyypityksen niin hyvin, että perinteiset kannanotot staattisuutta vastaan kuulostavat tietämättömiltä. Mutta se selvänäön määrä on suuri, jos haluaisi saada aikaan hienot tietotyypit, joiden päälle on kiva rakentaa funktiot, on valtaisa. Suunnittelu joudutaan pakostakin aloittamaan ylhäältä.

Miten Lispissä aikoinaan, ja nyt myös Clojuressa on tavattu tekemään, on aloittaa alhaalta (ei tietysti sovi isoihin projekteihin). Koodailija voi aloittaa tekemällä jonkun alhaisen tason funktion esimerkiksi käsittelemään syötettä, ja siitä nousta ylöspäin ryhmittelemään ja käsittelemään sitä. Sitten viimeisenä voi syntyä ulostulostus. Dynaaminen Clojure tuntuu uskomattoman modulaariselta tätä tehdessäni. Tuntuu kuin alhaalta-ylös -suunnittelumallia suorastaan kannustetaan käyttämään. Miltähän sekin tuntuisi, jos suurprojekti aloitettaisiin siten, että ohjelmoijille jaettaisiin vastuualueita, ja ensimmäinen päivä kulutettaisiin pyörittelemällä satunnaisia ideoita replissä. Ehkäpä vaikka toimisi? Kuulostaa ainakin sopivan hullulta. Dynaamista tyypitystä voi Clojuressa staattistaa jälkikäteen, mikä tuo oman mausteensa keskusteluun. Aloitetaan dynaamisella, huolettomalla heittelyllä, ja kun projekti kasvaa riittävän isoksi, voidaan sitä alkaa hallita staattisen tyypityksen avulla. Sehän jo yksinään ehkäisee suuren joukon erilaisia yksikkötestejä.

Pari Clojure-kirjaa pitää kyllä ostaa. Sain jo itseni vakuutettua tämän kirjoituksen kanssa, että vaikka Haskell tyydyttäisi mieltä enemmän kuin Clojure, siihen tyydytykseen tarvittaisiin enemmän henkistä pääomaa. Clojure tyydyttää jo nyt ja hartaasti.

Tageja: , ,

---
---

---

Aiheen vierestä