söndag 13 december 2009

En första clojure-betraktelse

Jag har börjat kika på Clojure och blir alldeles tagen. Jag har tittat på LISP och lambdakalkyl tidigare och blivit begeistrad över skönheten men bedrövad över att jag inte skulle kunna få något gjort med det. Det verkar vara annorlunda med Clojure. Trots att det tillåter vacker struktur och ren logik är det användbart.
För er som inte vet det är Clojure en LISP-dialekt som lever i en JVM. Det är utvecklat för att vara funktionellt och ha stöd för samtidighet och parallellisering. Dessutom är det designat för att friktionsfritt kunna kommunicera med Java. Det finns mängder av blogartiklar som förklarar grunderna och flera bra inspelade föredrag av skaparen Rich Hickey. Om du inte känner till Clojure, kolla upp det!
Jag har tänkt att prova Clojure och vill kunna göra fler uppdateringar på en datastruktur. Var ändring skulle beskrivas med en path och en ändringsfunktion eller ett nytt värde. Kanske finns exakt vad jag behöver i clojure.contrib men jag ville prova själv.
Sagt och gjort. Så här blev funktionen:
(defn multi-update
([coll path update]
(if (empty? path)
(if (fn? update) (update coll) update)
(assoc coll (first path)
(multi-update (coll (first path)) (rest path) update))))
([coll path update & more]
(apply multi-update (cons (multi-update coll path update) more))))
Funktionen anropas som följer:
(multi-update {:a 12 :b {:c [1 2 3]}}
[:a] inc
[:b :c 0] "Hupp"
[:b :c 1] inc)
Precis som önskat ger den: {:a 13 :b {:c ["Hupp" 3 3]}}. Det inc som skickas in är en funktion som ökar argumentet med ett.
I funktionalistisk anda löses problemet med rekursion. Om man anropar funtionen med bara data, path och update gör den följande:
Om path är tom har hittat elementet som ska uppdateras. Den ersätter värdet (coll) med (update coll) om update är en funktion och med update i annat fall.
Om path har element kvar arbetar den sig ner genom att ersätta datans element som svarar mot första nyckeln i path och ersätter det uppdaterat med resten av path.
Om funktionen anropas med coll, path, update och mer ropar den på sig själv med den uppdaterade informationen (multi-update coll path update) och de resterande argumenten.
Allt detta kanske låter bökigt men hela problemet är faktiskt ganska komplicerat. Hade samma problem lösts traditionellt objektorienterat hade en mängd klasser behövts för att klara av typsäkerheten (BaseNode, MapNode, ListNode, ValueNode, Path, BaseUpdate, SetUpdate och LambdaUpdate). Dessa nodklasser skulle ha var sin metod Update(Path path, Update update) som skulle innehålla den faktiska koden som gjorde något. Grymt mycket boiler plate-kod. Otypat skulle man kunna göra en rekursiv static-metod som jobbar på Dictionary<object, object> och List<object>, men den hade inte varit så särdeles elegant eller användbart.
Den objektorienterade varianten skulle dessutom få välja mellan att ändra hela listan eller kopiera hela listan. En ändring hade varit helt omöjligt att backa, att kopiera allt ett enormt resursslöseri. I Clojure-fallet får man ett nytt objekt men bara de ändrade noderna måste kopieras. De oförändrade noderna refereras till då de ju ändå aldrig kan ändras. Allt detta sköter Clojure om i bakgrunden.
Clojure vinner på alla fronter!