Parenteser

Betraktninger fra Mat-teamets grønne enger

Lange flate trær

Hjembyen min Fredrikstad er også kjent som Plankebyen, så du kan si at jeg har alle forutsetninger for å forstå tree-seq – funksjonen som lager lange lister av trær.

Forskjellen mellom postwalk og tree-seq

For et par uker siden så vi på postwalk og hvordan det var et nyttig verktøy for å gjøre oppdateringer i en trestruktur. Vi brukte den til å lage en interpolate-funksjon:

(interpolate
 [:div
  [:h1 "Hallo " :navn "!"]
  [:p "Fint å se deg."]]
 {:navn "Christian"})

;; => [:div
;;     [:h1 "Hallo " "Christian" "!"]
;;     [:p "Fint å se deg."]]

Observer at vi fikk beholde trestrukturen slik den var - med enkelte modifikasjoner. Det er også nettopp her den store forskjellen ligger. Funksjonen tree-seq bryter trestrukturen fra hverandre og gir oss en lang liste med alle elementene i treet. Slik:

(def hiccup
  [:div
   [:h1 "Hallo " :navn "!"]
   [:p "Fint å se deg."]])

(tree-seq coll? identity hiccup)

;; => ([:div [:h1 "Hallo " :navn "!"] [:p "Fint å se deg."]]
;;     :div
;;     [:h1 "Hallo " :navn "!"]
;;     :h1
;;     "Hallo "
;;     :navn
;;     "!"
;;     [:p "Fint å se deg."]
;;     :p
;;     "Fint å se deg.")

Som du ser får vi en salig blanding av nivåer i trestrukturen. Ja, vi får endatil hele det opprinnelige treet tilbake i første posisjon. Det oppleves kanskje ikke så veldig hendig, men se nå: Vi kan slenge på et filter.

(->> (tree-seq coll? identity hiccup)
     (filter string?))

;; => ("Hallo "
;;     "!"
;;     "Fint å se deg.")

Nå begynner det å ligne på noe. Her er alle rå tekststrenger i dom-strukturen. Det kan være nyttig hvis vi vil sikre at vi alltid bruker [:i18n]-tagger, for eksempel.

Eller se her:

(->> (tree-seq coll? identity hiccup)
     (filter keyword?))

;; => (:div :h1 :navn :p)

Nå fant vi alle keywords som var i bruk. Vi kunne for eksempel brukt det til å sjekke at det alltid er en og bare én :h1 på siden:

(->> (tree-seq coll? identity hiccup)
     (filter #{:h1})
     count)

;; => 1

Så ble det hendig likevel.

Her kan du i grunn slutte å lese for 95% av all bruk av tree-seq, men hvis du er ille nysgjerrig på detaljene så har jeg litt mer rare greier på lager.

Hva er greia med coll? og identity?

Første parameter til tree-seq (normalt sett coll?) er et predikat som avgjør om en node i treet skal manøvreres ned i - eller om den skal behandles som en løvnode.

Andre parameter (normalt sett identity) er en funksjon hvis jobb er å returnere en liste med nye noder, dersom predikatet slo til.

Vi kunne for eksempel driste oss til å manøvrere ned i strenger også:

(->> (tree-seq
      #(or (coll? %)
           (string? %))
      seq
      hiccup)
     (filter char?))

;; => (\H \a \l \l \o \space \!
;;     \F \i \n \t \space \å
;;     \space \s \e \space
;;     \d \e \g \.)

Om det ble så veldig nyttig, vet jeg ikke, men det demonstrerer at man selv kan bestemme hvordan tree-seq ser på trestrukturene vi gir den.

Mer nyttig er eksempelet fra Christian sin bloggpost om Kode som skriver kode:

(defn get-syms [xs]
  (->> xs
       (tree-seq coll? (fn [x]
                         (cond-> x
                           (and (map? x) (:as x))
                           (select-keys [:as]))))
       (filter symbol?)
       (remove #{'&})))

Her erstatter vi identity med vår egen funksjon, for å snevre ned hva som teller som noder på vår vei nedover i treet:

(cond-> x
  (and (map? x) (:as x))
  (select-keys [:as]))

Det meste får flyte gjennom som vanlig (x), men dersom det er map som inneholder nøkkelen :as, så trår vår select-keys til.

Resulatet er at når vi har en destrukturering av denne typen:

(defn test [{:keys [bar baz] :as foo}])

Så blir symbolet foo valgt ut, mens bar og baz blir droppet. Akkurat det vi ønsket oss for loggingen vår.

Til slutt

Ingen av oss har noe særlig lyst til å drive med tømmerfløting nedover Glomma. På samme måte prøver vi å unngå å jobbe med dypt nøsta data. Aller helst jobber vi med flate, møre data, men når vi har trestrukturer i hendene er postwalk og tree-seq fine verktøy å ha i kassa.

Magnar

Om Clojure

clojure.core er en serie med bloggposter om alle de nyttige verktøyene i `clojure.core`, hjørnesteinen i det rikholdige standardbiblioteket til Clojure. Gikk du glipp av starten? Her er det første innlegget i serien:

Data i passe porsjoner

Noen datamengder kan ikke spises i én jafs, men må heller porsjoneres ut i passende mengder. Heldigvis er det lekende lett i Clojure.