Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

18 Februar, 2019

Pattern Matching & Destructing in eLisp

Programmiersprachen

In 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.