LINQ-macro i Clojure


mandag 23. august 2010 Clojure Macro

En av de tingene som gjør Clojure og andre Lisper unike i forhold til andre språk er makroer. Det finnes andre språk som har makro-features, men ikke så geniale som de i Clojure/Lisp. Makroer gjør det blant annet ganske enkelt å lage interne DSL'er. Her er et eksempel hentet fra en introduksjon til lisp-makroer på slideshare, hvor man legger tilrette for Linq-lignende spørringer med bare noen få linjer kode.

La oss først se på hvordan det tar seg ut i bruk:

(def names '("Burke" "Connor"
             "Frank" "Everett"
             "Albert" "George"
             "Harris" "David"))

(def query
     (from n in names
           where (= (. n length) 5)
           orderby n
           select (. n toUpperCase)))

(doseq [n query]
       (println n))
Output:
BURKE
DAVID
FRANK

Bortsett fra Clojure-koden for å spesifisere lengde på 5, og å gjøre navnene uppercase, er denne spørringen svært så leselig, og ikke ulik det man får i .net med Linq.

Og her følger alt vi trengte å definere for å kunne skrive spørringen. From-makroen tar imot argumentene og gjør det om til list-prosesserende kall som filter, sort-by og map. Nøkkelordene in, where, orderby og select ser den faktisk bare bort fra – de er der kun for lesbarhet i selve spørringen. Når jeg kjører programmet vil spørringen min bli byttet ut med den templaten som makroen definerer.

(defmacro from
  [var _ coll _ condition _ ordering _ desired_map]
  `(map (fn [~var] ~desired_map)
        (sort-by (fn [~var] ~ordering)
                 (filter (fn [~var] ~condition)
                         ~coll))))

Med makroer kan vi altså sømløst utvide språket med ting som i mange andre språk ville krevd en endring av selve kompilatoren. For en grei innføring i makroer kan du ta en titt her.

Flere eksempler

Jeg eksperimenterte litt videre med makroen fra demonstrasjonen, og lagde noen spørringer på en liste med storbyer for å illustrere kraften i DSL'en bedre. Her er først byene, opprettet som en liste av hashes (disctionaries):

(def cities '({:name "Beijing" :country "China" :population 19000000}
              {:name "Guangzhou" :country "China" :population 14000000}
              {:name "Shanghai" :country "China" :population 10000000}
              {:name "Bogota" :country "Colombia" :population 32000000}
              {:name "Delhi" :country "India" :population 4000000}
              {:name "Los Angeles" :country "USA" :population 9000000}
              {:name "London" :country "United Kingdom" :population 24000000}
              {:name "Tianjin" :country "China" :population 37000000}
              {:name "Wuhan" :country "China" :population 29000000}
              {:name "Lagos" :country "Nigeria" :population 25000000}))

Så kan jeg for eksempel lage en spørring som henter ut alle byene i Kina med 15 millioner innbyggere eller mer. Jeg spesifiserer også synkende sortering på innbyggertall ved å gange det med –1. Til slutt selekterer jeg kun ut navnet på byen og innbyggertallet.

(def chinese-cities-more-than-15-million
     (from c in cities
           where (and (= (:country c) "China")
                      (>= (:population c) 15000000))
           orderby (* (:population c) -1)
           select [(:name c) (:population c)]))

(doseq [c chinese-cities-more-than-15-million]
       (println c))
Output:
[Tianjin 37000000]
[Wuhan 29000000]
[Beijing 19000000]

I neste eksempel er jeg interessert i alle land som har byer med mer enn 25 millioner innbyggere. Jeg sorterer på land, og selekterer kun landet. I tillegg bruker jeg distinct, som er "innebygd" i clojure, for å unngå at Kina kommer med to ganger.

(def countries-with-extreme-cities
     (distinct (from c in cities
                     where (>= (:population c) 25000000)
                     orderby (:country c)
                     select (:country c))))

(doseq [c countries-with-extreme-cities]
       (println c))
Output:
China
Colombia
Nigeria

comments powered by Disqus