Pattern Matching & Destructing in eLisp
ProgrammiersprachenIn Clojure stößt man
unweigerlich auf Destructing, was man -- einmal
verstanden -- nicht mehr missen will. Dieses Sprachfeature macht
die Zuweisung von Daten in einer Datenstruktur in Variablen
sehr einfach. Dabei werden Muster mit Platzhaltern definiert, auf
die dann die eigentliche Datenstruktur aufgelegt
wird. Hört sich
kompliziert an, ist es aber nicht.
Folgenden Code kennt jeder Programmierer (PHP):
function doSomething(options={}) {
$name = isset(options['name']) ? options['name'] : false;
$age = isset(options['age']) ? options['age'] : false;
...
}
Aus der Datenstruktur $options
werden die Werte für die Variablen $name
und $age
ausgelesen, um dann anschließend damit zu arbeiten. In jeder Programmiersprache, die Variablen unterstützt, schreibt man zwangsläufig diesen Boilercode. Das Beispiel in Clojure:
(let [name (:name options)
age (:age options)]
...)
Mit Destructing lässt sich dies nun viel einfacher abbilden:
(let [[:name :age] options]
...)
Es wird die Datenstruktur definiert und Platzhalter
mit Namen festgelegt, auf die dann die Daten matchen. Daher wird dieser Mechanismus auch Pattern Matching genannt. Die Datenstruktur mit den Platzhaltern (also das Pattern) kann in Clojure beliebig komplex sein. Das macht den Code nicht nur einfacher zu lesen, sondern auch noch robuster gegen ungewünschten Input. Passt die Eingabe nicht auf das Muster, bricht der Code direkt an dieser Stelle ab.
Emacs/eLisp bietet mit dem Paket pcase.el auch die Möglichkeit an, Pattern Matching zu verwenden. (Ist das nicht toll, in Lisp kann man sich Sprachfeatures einfach dazubauen ohne den Kern zu verändern!) Die Funktion pcase
wird automatisch geladen, man benötigt also kein (require ...)
.
Hier ein Beispiel in eLisp mit pcase-let
:
(pcase-let ((`(,name ,age) options))
...)
Das Muster definiert wieder Platzhalter (Bindings), die dann im Body verfügbar sind. pcase kann aber noch mehr. Ein anderes Beispiel:
(pcase-let ((`(,_ 2 ,c) (list 1 2 3)))
...)
Mit ,_
wird das erste Element ignoriert (muss aber da sein), das zweite Element der Liste muss eine 2
sein und das dritte Element wird an c
gebunden. Auch lassen sich Abhängigkeiten innerhalb des Musters definieren. Hier bspw:
(pcase-let ((`(,x ,x) (list 1 1)))
...)
Hier muss das erste und das zweite Element der Liste gleich sein (der Inhalt ist nicht relevant, muss aber gleich sein).
Interessant wird es mit der pcase
-Funktion, die sich wie ein normales case
verhält, nur mit dem Pattern Matching Mechanismus. Das macht den Code extrem übersichtlich und einfach verständlich:
(pcase (list 1 2 2)
(`(,a ,b) (+ a b))
(`(,a 2 ,b) 5))
Hat die Liste zwei Elemente, werden sie addiert. Hat die Liste an zweiter Stelle eine 2, dann wird 5 ausgegeben.
Clojure hat das Spezialsymbol &
, mit dem man auch dynamische Datenstrukturen handhaben kann. Ein Catch all
für den Rest sozusagen. In pcase ist das ein Built In Feature der Sprache. (Schon wieder diese Eleganz von Lisp :).
(pcase (list 1 2 3)
(`(,a . ,rest) (apply '+ rest)))
Das Muster wird einfach als ein Paar definiert. car
wird an a
gebunden und cdr
(also der ganze Rest, egal wie groß dieser sein mag) an rest
. In rest
steckt dann logischerweise die Liste (2 3)
, die dann mit apply
auf der Funktion +
angewendet wird und zu 5 evaluiert.
Was im Hintergrund passiert, lässt sich gut mit pp-macroexpand-expression
nachvollziehen:
(let*
((#:val
(list 1 2 3)))
(if
(consp #:val)
(let* ((#:x10 (car #:val))
(#:x11 (cdr #:val)))
(let ((rest #:x11)
(a #:x10))
(apply '+ rest)))
nil))
Das Makro übernimmt für uns den ganzen lästigen Boilerplate Code mit der Zuweisung der Variablen und stellt gleichzeitig sicher, dass die Struktur die ist die wir erwarten.
Zu pcase
gibt es noch einiges mehr, wie Guards oder Prädikate. Es lohnt sich also mal in das Manual zu schauen. Wer mit eLisp nichts anfangen kann, sollte dennoch einmal in seiner Lieblingssprache suchen, ob sie Destructing bzw. Pattern Matching Features bereithält.