Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

15 Februar, 2016

Destructing in Clojure

Programmiersprachen

Destructing (auch Abstract Structural Binding genannt) ist eines von vielen Features in Clojure, die die Sprache für mich so unglaublich elegant und schön machen. Wer noch nie mit Clojure programmiert hat, sollte jetzt genau aufpassen :) (Achtung: Ich rede hier von Variablen, doch korrekterweise sind es lexikalische Bindungen. Alle Bindungen in Clojure sind unveränderbar (immutable), also keine Variablen. Dennoch ist es für das Verständnis einfacher sie Variablen zu nennen.)

In der Doku steht ganz unspektakulär:

Clojure supports abstract structural binding, often called destructuring, in let binding lists, fn parameter lists, and any macro that expands into a let or fn. The basic idea is that a binding-form can be a data structure literal containing symbols that get bound to the respective parts of the init-expr. The binding is abstract in that a vector literal can bind to anything that is sequential, while a map literal can bind to anything that is associative.

Es geht also darum, Variablen-Zuweisungen in Form von Listen zu machen. PHP bietet dafür eine ganz primitive Version davon an:

list($firstname, $lastname, $rest) = ['Aaron', 'Fischer', 'other', 'stuff'];

Dies wird oft Multiple Variable Assignment genannt. In Ruby gibt es zusätzlich den splat-Operator, der ähnliches kann.

def (firstname, lastname, *rest)
  ...
end

Das ist natürlich nur die Spitze des Eisbergs. In Clojure wurde diese Funktionalität komplett abstrakt umgesetzt und funktioniert mit jeder Art von Listen. Man muss sich das wie Pattern Matching vorstellen. Es ist unglaublich, wie mächtig und nützlich Destructing ist. Besser erklären lässt es sich anhand ein paar Beispielen:

Hier wird wie oben aus einem Vektor der Vorname, der Nachname und der Rest (other stuff) herausgezogen und mit let an die drei Variablen gebunden.

(let [[firstname lastname & rest] ["Aaron" "Fischer" "other" "stuff"]]
  ...)

Das ganze lässt sich auch bei Funktionen in den Parametern verwenden:

(defn add-vectors [[a1 a2] [b1 b2]]
  [(+ a1 b1) (+ a2 b2)])

(add-vectors [1 2] [5 6])

Das macht den Code viel lesbarer und verständlicher. In anderen Programmiersprachen hat man bei solcher Art von Funktionen oft zu Beginn eine Liste mit Variablenzuweisungen und viel Array-Fummelei.

Das schöne daran ist, es lässt sich beliebig verschachteln und funktioniert mit jeder Form von Listen -- also allem was irgend wie mit nthnext bedient werden kann. Hier ein weiteres Beispiel ({:keys [a b]} ist ein Shortcut für {a :a b :b}):

(defn foo [{name :full-name, [x y _] :coordinates, {:keys [n e]} :direction}]
 ...)

(foo {:full-name "Aaron Fischer"
      :coordinates [24 32 123]
      :other "stuff"
      :direction {:n 0, :e 60}})

So kann man beliebig komplexe Datenstrukturen ganz einfach zerlegen (= destruct), in dem man ein Muster definiert, das auf die Datenstruktur passt. So lassen sich alle relevanten Informationen schon im let bzw. fn extrahieren und danach direkt damit arbeiten.

Destructing wird auch ausgiebig von Bibliotheken verwendet, die mit großen verschachtelten Datenstrukturen umgehen müssen. Compojure zum Beispiel nutzt es, um durch den DOM-Baum zu navigieren und relevante Informationen herauszupicken/auszutauschen.

Hat man sich an dieses Luxus-Feature erst einmal gewöhnt, sucht man es schmerzlich bei anderen Programmiersprachen (Rust hat ebenfalls Destructing, in Haskell ist es zentraler Bestandteil der Sprache). Die hier gezeigten Beispiele umspannen allerdings noch nicht den vollen Umfang von Destructing. Es gibt noch einige Features wie Default-Werte, eine Art erweiterter splat-Mechanismus, usw. Es lohnt sich also, mal in der eigenen Lieblingssprache nach diesem Feature zu suchen.