Macros

Es gibt in Clojure nur wenige Special Forms, mit neuen Kernfunktionen der Sprache

  • def
  • if
  • do
  • let
  • quote
  • fn
  • loop und recur
  • throw und try

Die Kurznotationen werden vom Reader automatisch übersetzt:

'(1 2 3) $\Rightarrow$ (quote 1 2 3)

#(+ % 4) $\Rightarrow$ (fn [p1__8130] (+ p1__8130 4))

Die meisten sonstigen Elemente sind Macros

Macros nehmen ihren Input und wandeln ihn zur Compile-Zeit um.

Dadurch können wir beliebigen Code generieren lassen.

Mit macroexpand können wir schauen, was der Output eines Macros ist.


          (macroexpand '(when (> 3 1)
                              (println "Bar")
                              (println "Foo")))

          ; => (if (> 3 1) (do (println "Bar") (println "Foo")))
        

Macros gibt es für viele low-level Funktionalitäten:


          (macroexpand '(and (+ 2 2) (+ 4 2) nil 5))

          ; => (let*
          ;     [and__8108__auto__ (clojure.core/+ 2 2)]
          ;     (if and__8108__auto__ (clojure.core/and (clojure.core/+ 4 2) nil 5) and__8108__auto__))
        

Ein Macro definieren wir mit defmacro


          (defmacro infix
            "Use this macro when you pine for the notation of your childhood"
            [infixed]
            (list (second infixed) (first infixed) (last infixed)))
        

Destructuring funktioniert auch bei Macros:


          (defmacro infix
            "Use this macro when you pine for the notation of your childhood"
            [[operand1 op operand2]]
            (list op operand1 operand2))
        

Multi-arity Macros sind auch möglich:


          (defmacro and
            "Evaluates exprs one at a time, from left to right. If a form
            returns logical false (nil or false), and returns that value and
            doesn't evaluate any of the other expressions, otherwise it returns
            the value of the last expr. (and) returns true."
            {:added "1.0"}
            ([] true)
            ([x] x)
            ([x & next]
            `(let [and# ~x]
                (if and# (and ~@next) and#))))
        

Die einzelnen Elemente schauen wir uns noch im Detail an.

Beim Schreiben von Macros muss man sehr darauf achten, welche Teile evaluiert werden sollen und welche nicht.

println gibt immer nil zurück. Wir wollen eine Variante, welche den zu druckenden Wert zurückgibt.


          (defmacro my-print
            [expression]
            (list let [result expression]
                  (list println result)
                  result))
        

Warum funktioniert das nicht?

Das Macro versucht den Wert des Symbols let zurückzugeben und nicht das Symbol selber.

Der Macro-Body verhält sich beim auswerten nicht anders als ein Function-Body.

Wir müssen die Symbole "quoten", welche nicht ausgewertet werden sollen:


          (defmacro my-print
            [expression]
            (list 'let ['result expression]
                  (list 'println 'result)
                  'result))
        

'let wird vom Reader durch (quote let) ersetzt.


            foo

            ; => Unable to resolve symbol: foo in this context
          

            (quote foo)

            ; => foo
          

Hier die definition des when Macro:


          (defmacro when
            "Evaluates test. If logical true, evaluates body in an implicit do."
            {:added "1.0"}
            [test & body]
            (list 'if test (cons 'do body)))
        

Neben ' gibt es auch noch den Syntax-Quote `

Der Syntax-Quote liefert den fully qualified name eines Symbols:


          '+

          ; => +
        

          `+

          ; => clojure.core/+
        

Das ist nützlich um Name Collisions zu vermeiden

Der Syntax-Quote bietet die Möglichkeit für einzelne Teile wieder abgeschaltet zu werden (unquote):


          `(+ 1 ~(inc 1))

          ; => (clojure.core/+ 1 2)
        

So können wir den Rückgabewert eines Macros einfacher zusammenbauen und nur einzelne Elemente evaluieren.

Name Collisions sind eine potentielle Gefahr bei Macros, da wir nicht wissen, welche Namen der Macro Nutzer in seinem Code verwendet:


          (defmacro with-mischief
            [& stuff-to-do]
            (concat (list 'let ['message "Oh, big deal!"])
                    stuff-to-do))

            (let
              [message "Good job!"]
              (with-mischief
                (println "Here's how I feel about that thing you did: "
                         message)))

          ; => Here's how I feel about that thing you did: Oh, big deal!
        

Um Name Collisions zu vermeiden, können wir innerhalb eines Syntax-Quotes eindeutige Symbole generieren lassen:


          `(let [foo# 42] (println foo#))

          ; => (clojure.core/let [foo__8184__auto__ 42] (clojure.core/println foo__8184__auto__))
        

Unser my-print Macro lässt sich nun etwas sauberer schreiben:


          (defmacro my-print
            [expression]
            `(let [result# ~expression]
              (println result#)
              result#))
        

Es ist oft eine gute Idee, Argumente nur einmal zu evaluieren und dem Ergebnis mit let dann einen Namen innerhalb des Macros zu geben, wenn wir es mehrfach brauchen.

Ein weiteres Feature innerhalb von Syntax-Quotes ist unquote splicing.

Damit können wir eine Liste beim unquoten "ausbreiten":


          (defmacro my-when
            [test & body]
            `(if ~test (do ~@body)))
        

Man sollte Macros nur verwenden, wenn eine Funktion wirklich keine Option ist

  • Die Argumente dürfen noch nicht evaluiert sein.
  • Es ist wichtig, dass der Code zur Compile-Zeit ausgeführt wird.

Mit Macros können wir die Sprache um "Syntactic Sugar" erweitern.

Beispiel:


            (defn increase-age [person]
              (update (assoc person :hair-color :gray) :age inc))

            (increase-age {:name "Jon" :age 42 :hair-color :brown})
            ; => {:name "Jon", :age 43, :hair-color :gray}
          

Sehr oft wollen eine Kette von Modifikationen machen, bei denen der Output einer Funktion der Input der nächsten ist.

Das kann zu sehr viel Nesting führen.

Mit den Threading Macros können wir Nesting vermeiden:


          (defn increase-age [person]
            (-> person
                (assoc :hair-color :gray)
                (update :age inc)))

          (increase-age {:name "Jon" :age 42 :hair-color :brown})
          ; => {:name "Jon", :age 43, :hair-color :gray}
        

    ->

  • started mit dem ersten Argument
  • setzt dieses anschließend als ersten Parameter ein.
  • nimmt den Rückgabewert und setzt diesen wieder als ersten Parameter ein.
  • ...
  • gibt den letzten Rückgabewert zurück.

Mit ->> können wir den aktuellen Wert jeweils als letzten Parameter einsetzen


          (->> (range 10)
               (filter odd?)
               (map #(* % %))
               (reduce +))
        

Allgemeines Pattern:

  • Funktionen auf Datenstrukturen (Maps) nehmen diese als ersten Parameter.
  • Funktionen auf Collections nehmen diese als letzten Parameter.

Mit as-> können wir jeweils angeben, wo der aktuelle Parameter eingesetzt werden soll:


          (as-> {:name "Jon" :age 42 :hair-color :brown} person
                (update person :age inc)
                (if (>= (:age person) 50)
                    (assoc person :hair-color :gray)
                    person))
          ; => {:name "Jon", :age 43, :hair-color :brown}