18. joulukuu 2010, 18:59

Clojuren kuvaukset ovat tyylikkäitä

Tällä kertaa olen ihmetellyt Clojuren erilaisia kuvauksia. Kukaan ei ole valehdellut sanoessaan, että kuvaukset (maps) ovat Clojuren keskeinen tietorakenne. Pelkkä tavallinen kuvaus, syntaksiltaan {}, on todella yleiskäyttöinen, kun käytellään sopivasti avainsanoja. Nykyinen parserointiskriptinikin hyödyntää melko sulavasti kuvauksia ja sisäkkäisiä sellaisia, kaikki identifioituna avainsanoilla. Käytännössä kyse on staattisten systeemien struct-rakenteista, mutta nyt muuttujanimet tietueiden sisällä ovat varsin vapaasti käsiteltävissä.

Pelkkien tavallisten kuvausten lisäksi Clojure tuo huisin helpossa muodossa koodaajiensa eteen myös paljon käytetyt järjestetyt kuvaukset. Nämä kuvaukset (sorted-map) pitävät sisältönsä avaimen mukaisessa järjestyksessä. Erittäin hyviä. Avainsanat (jotka ovat siis muotoa :foo) käsitellään merkkijonoina, ja laitetaan leksikograafiseen järjestykseensä.

Viikonpäivät järjestykseen

Nytpä tuli tarvetta saada kuvaus mielivaltaiseen järjestykseen. Kuvauksen avaimina ovat järjestetyt parit (ja Clojuren tapauksessa vektorit), jotka koostuvat viikonpäivästä ja tunnista. Koska viikonpäivät eivät ole ainakaan Suomessa leksikograafisessa järjestyksessä, pitäisi keksiä funktio, joka järjestelee ne siten.

Naiivi, toimiva pohja järjestellä viikonpäivät oikeassa järjestyksessä on seuraava:

(defn compare-weekdays
  [k1 k2]
  (let [weekday (zipmap [:ma :ti :ke :to :pe :la :su] (range 8))]
    (compare (weekday k1) (weekday k2))))

Nyt Clojuren oma compare-funktio hoitaa vaativan yhtäsuuruuspuolen alta pois. Se vastaa ikään kuin Javan compareTo-metodia. Koska kuvaukset ovat omien avaintensa funktioita, esimerkiksi weekday :ke palauttaa kakkosen ja weekday :su kuutosen. Hmm, olenkin vahingossa jättänyt range-funktiolle yhtä liian ison lukuvälin. No, onneksi zipmap lopettaa lyhimmän joukon osuessa loppuunsa. Muissa kielissä, kuten Haskellissa zip-tyyppiset funktiot yleensä palauttavat listan järjestettyjä pareja, mutta Clojuren tapauksessa kuvauksen palauttaminen on enemmän kuin loogista. Sitä voi sitten käyttää funktiona välittömästi!

Mutta en ollut tyytyväinen funktioomme. Haluaisin yleistetyn version, joka ottaisi vastaan listan avainsanoja ja palauttaisi sitten sopivan vertailufunktion. Ja, koska Clojure on funktionaalinen kieli, ei ensimmäisen esimerkin muokkaaminen sopivaksi ollut liian työlästä. Lisäksi pieni Haskell-tuntemus auttaa:

(defn keywords-in-order
  [keywords] 
  (let [k (zipmap (seq keywords) (iterate inc 0))]
    (fn [k1 k2]
      (compare (k k1) (k k2)))))

Edelleen samaa vanhaa, mutta nyt keywords-lista voi olla miten pitkä tahansa, meidän pitää saada naitettua jokaiselle oma kasvava lukuarvonsa. (iterate inc 0) tuottaa meille äärettömän listan lukuja alkaen nollasta, mutta edelleen zipmap lopettaa uusien lukujen ottamisen, kun keywords ehtyy.

Ahhh. Funktionaalinen ohjelmointi. Clojure on juuri oikeata myrkkyä mielelleni. Clojure on Java ei vain steroideissa, vaan myös hapoissa (mistä S-lausekkeet). Uusi funktiomme keywords-in-order ottaa vastaan vain joukon alkioita (mielellään avainsanoja, muista atomeista en ole varma) ja se palauttaa uuden funktion, joka toimii vertailijana. Nyt funktion compare-weekdays pitäisi olla täsmälleen sama kuin (keywords-in-order [:ma :ti :ke :to :pe :la :su]). Siis kummatkin funktioita, jotka tekevät saman samoilla syötteillä.

Entäs ne kuvaukset?

Niin, nyt meillä on ainakin vertailufunktio, joka sanoo, että :ma < :to. Meidän pitänee (en ole aivan varma) kirjoittaa oma vertailufunktio myös kattamaan käyttämämme kuvauksen vektorit [:day time]. Clojuren oma comparator osaa kyllä vertailla, jos vektorin kaikki alkiot vastaavat toisiaan pareittain, ja ovat vertailtavissa. Koska meillä on nyt oma kustomoitu vertailija, on luultavasti tarpeen kirjoittaa oma vektorien vertailija. Sen pidemmittä puheitta:

(defn compare-times
  [[day1 time1]
   [day2 time2]]
  (let [comparator (keywords-in-order [:ma :ti :ke :to :pe :la :su])]
    (if-not (= day1 day2)
      (comparator day1 day2)
      (compare time1 time2))))

Nyt tärkeintä on huomata, että olen määritellyt comparator-funktion edellisessä kappaleessa määritellyllä keywords-in-order-funktiolla, joka palauttaa meille sopivan vertailijan. Iffin laitoin näemmä “väärinpäin”, mutta muuten ok. Tämä funktio saa vain kaksi arvoa, mutta käytän destructure -menetelmää purkamaan nämä parametrit auki. Viimeisellä rivillä voi käyttää Clojuren omaa compare-funktiota, koska time1 ja time2 ovat kokonaislukuja.

Ja mitä näistä saa auki? No:

(into (sorted-map-by compare-times)
       {[:pe 10] "tunti"
        [:ti 12] "demo"
        [:ti 13] "lounas"
        [:ma 8] "herätys"})
; tulos:
   {[:ma 8] "herätys",
    [:ti 12] "demo",
    [:ti 13] "lounas",
    [:pe 10] "tunti"}

Kaikki on nyt nätisti järjestyksessä!

Tageja:

---
---

---

Aiheen vierestä