Algorithmische Komposition mit Common Lisp
Exkurs: Reihenfolge von Bindungen und destructuring-bind in loop Ausdrücken
Die loop Form in der Funktion collect-part-events enthält einige Besonderheiten, auf die hier näher eingegangen werden sollte:
(loop for start-time = start then (+ start-time section-duration) for start-shifts = '(0 0) then (calc-shift start-shifts shifts) for (num-repeats shifts) in part for section-duration = (section-duration num-repeats pattern dtime) append (shift-collect-section-events pattern num-repeats start-time dtime start-shifts shifts))
Reihenfolge der Bindungen von start-time und section-duration im loop
Bei genauerem Hinschauen fällt auf, dass in dem Ausdruck (+ start-time section-duration) die Variable section-duration referenziert wird, bevor ihr zwei Zeilen später in for section-duration = (section-duration num-repeats pattern dtime) ein Wert zugewiesen wird.
Diese Reihenfolge ist wichtig! Die Startzeit einer Sektion entspricht der Startzeit der vorangegangenen Sektion plus der Dauer der vorangegangenen Sektion. Im loop body wird das dadurch erreicht, dass ab der zweiten Iteration die Startzeit mit dem Ausdruck (+ start-time section-duration) errechnet wird, bevor section-duration an die Dauer der aktuellen Sektion gebunden wird. Auf diese Weise ist section-duration bei Berechnung der Startzeit für die aktuelle Sektion wie erwünscht noch auf dem Wert, der ihr bei der letzten Iteration zugewiesen wurde, d.h. auf der Dauer der vorherigen Sektion:
;; Richtige Reihenfolge im loop body: (let ((pattern '(64 66 71 73 74 66 64 73 71 66 74 73)) (dtime 0.138)) (loop for start-time = 0 then (+ start-time section-duration) for (num-repeats shifts) in '((4 (0 0)) (5 (0 1)) (5 (0 0))) for section-duration = (section-duration num-repeats pattern dtime) collect (list :start-time start-time :duration section-duration))) ;; => ((:start-time 0 :duration 6.6239996) ;; (:start-time 6.6239996 :duration 8.28) ;; (:start-time 14.903999 :duration 8.28))
Wenn die beiden Symbole in der anderen Reihenfolge gebunden werden, wird die Variable section-duration an die Dauer der aktuellen Sektion gebunden, bevor die Startzeit der aktuellen Sektion berechnet wird und das ergibt ab der zweiten Iteration falsche Startzeiten:
;; ACHTUNG: *Falsche* Reihenfolge im loop body: (let ((pattern '(64 66 71 73 74 66 64 73 71 66 74 73)) (dtime 0.138)) (loop for (num-repeats shifts) in '((4 (0 0)) (5 (0 1)) (5 (0 0))) for section-duration = (section-duration num-repeats pattern dtime) for start-time = 0 then (+ start-time section-duration) collect (list :start-time start-time :duration section-duration))) ;; => ((:start-time 0 :duration 6.6239996) ;; richtige Startzeit ;; (:start-time 8.28 :duration 8.28) ;; falsche Startzeit! ;; (:start-time 16.56 :duration 8.28)) ;; falsche Startzeit!
destructuring-bind im loop body
Wie in Ein ganzer Teil erläutert, ist part eine Liste von Sektionen und eine Sektion ist eine Liste der Form (anzahl-wiederholungen (shift-1 shift-2)).
In dem Ausdruck for (num-repeats shifts) in part der loop Form wird bei jeder Iteration in einem Schritt das Symbol num-repeats an das erste Element einer Sektion und das Symbol shifts an das zweite Element der gleichen Sektion gebunden. Im Falle einer Liste '(5 (0 0)) wird also num-repeats an die Zahl 5 und shifts an die Liste (0 0) gebunden.
Hier ist der Ausschnitt mit der Partitur des Teils vom Ende des letzten Kapitels in Aktion:
(loop for (num-repeats shifts) in '((4 (0 0)) (5 (0 1)) (5 (0 0))) collect (list :num-repeats num-repeats :shifts shifts)) ;; => ((:num-repeats 4 :shifts (0 0)) ;; (:num-repeats 5 :shifts (0 1)) ;; (:num-repeats 5 :shifts (0 0)))
Das Aufspalten einer Liste wird in Lisp destructuring genannt und das damit verbundene Binden an Symbole heißt destructuring-bind, das in den Common Lisp Standard als Macro integriert ist. Das for Konstrukt in loop erlaubt ganz analog zu destructuring-bind das Aufspalten und Binden von Listen, über die iteriert wird.
Auf diese Weise lässt sich Code sehr knapp und gleichzeitig semantisch klar und gut lesbar gestalten, da für den Zugriff auf die Elemente einer Liste keine generischen Funktionen, wie first, second, rest, car, caar, cdr, etc. verwendet werden müssen, die nichts darüber aussagen, was das erste, zweite, etc. Element einer Liste ist.
Hier noch weitere Beispiele mit destructuring-bind und loop:
(destructuring-bind (a b) '(1 (2 3)) (list :a a :b b)) ;; => (:a 1 :b (2 3)) (destructuring-bind (a (b c)) '(1 (2 3)) (list :a a :b b :c c)) ;; => (:a 1 :b 2 :c 3) (destructuring-bind (a (b c) &optional d) '(1 (2 3)) (list :a a :b b :c c :d d)) ;; => (:a 1 :b 2 :c 3 :d nil) (destructuring-bind (a (b c) &optional d) '(1 (2 3) 4) (list :a a :b b :c c :d d)) ;; => (:a 1 :b 2 :c 3 :d 4) (destructuring-bind (a (b c) &rest d) '(1 (2 3) 4 5 6 7) (list :a a :b b :c c :d d)) ;; => (:a 1 :b 2 :c 3 :d (4 5 6 7)) (loop for (key value) in '((:a 1) (:b 2) (:c 3) (:e 4)) collect (list :key key :value value)) ;; => ((:key :a :value 1) (:key :b :value 2) (:key :c :value 3) (:key :e :value 4)) (loop for (key value) on '(:a 1 :b 2 :c 3 :e 4) by #'cddr collect (list :key key :value value)) ; => ((:key :a :value 1) (:key :b :value 2) (:key :c :value 3) (:key :e :value 4))