Funktionen können in Lisp ihre Argumente entweder parallel oder
sequenziell abarbeiten. In diesem Kapitel soll der Unterschied
zwischen den beiden Arbeitsweisen aufgezeigt werden. Um zunächst
einmal klar zu machen, worum es überhaupt geht, werfen wir einen
Blick auf die Arbeitsweise von
setq:
(setq a 100 b (1+ a)) => 101
Offensichtlich arbeitet
setq sequenziell, d.h. die Argumente
werden in der Reihenfolge von links nach rechts abgearbeitet, und
bei der zweiten durchgeführten Zuweisung kann schon auf das Ergebnis
der ersten Zuweisung zugegriffen werden. Wie aber verhält es sich mit
selbstgeschriebenen Funktionen in dieser Hinsicht?
Testen wir doch einmal, was unser
let da macht. Ein Hinweis:
Die Funktion
let wird im entsprechenden
Kapitel beschrieben, der Inhalt dieses Kapitels sollte bekannt sein.
Auch verstanden haben sollte man das, was im Kapitel
Namespaces zu finden ist, es könnte
hier doch sonst vielleicht nur hilfloses Kopschütteln entstehen.
(let '((a 1)(b (1+ a)))
'(progn
(print a)
(print b)
)
) => ???
Nun, hier werden wir uns wahrscheinlich eine Fehlermeldung einhandeln!
Deshalb wahrscheinlich, weil ja auch noch eine Variable
a mit einem
übergeordneten Namensraum existieren könnte, deren Wert für die Zuweisung
an
b verwendet würde. Testen wir das doch einmal explizit aus - wir
setzen noch eine Funktion als äusseren Namensraum um den
let-Aufruf
herum und schaffen dort eine solche übergeordnete Variable
a:
(defun test( / a b c)
(setq a 100 b 200 c 400)
(let '( (a 1)
(b (1+ a))
(c (1+ b))
)
'(list a b)
)
)
=> (1 101 201)
Nun, dieses Ergebnis war zu vermuten! Schliesslich wird ja die ganze
Liste mit den Bindungen für
let zuerst evaluiert, und dann wird
überhaupt erst der neue Namensraum durch das
lambda geschaffen!
Bis zu diesem Zeitpunkt liegen die Symbole noch im alten Namensraum,
d.h. dem übergordneten der Funktion
test.
Können wir hier eine sequenzielle Abarbeitung, wie wir sie bei
setq
beobachtet haben, erreichen? Natürlich können wir das. Was wir brauchen,
ist ein Mechanismus, der die Namensräume etwa so verschachtelt:
(let '((a 1))
'(let '((b (1+ a)))
'(let '((c (1+ b)))
'(list a b c)
)
)
)
=> (1 2 3)
In anderen Lisp-Dialekten wird die sequenzielle Version durch einen Stern
im Namen gekennzeichnet, sie heisst also let* im Gegensatz zum
parallel verarbeitenden let. Was nun noch fehlt, ist die Funktion
selbst - hier ist sie schon:
(defun let*(bindings expr / )
(if(cdr bindings)
(let
(list(car bindings))
(cons 'let*
(list
(cons 'quote(list(cdr bindings)))
(cons 'quote(list expr))
)
)
)
(let bindings expr)
)
)
Das sieht auf den ersten Blick nicht sehr aufregend aus, setzt aber
doch einen etwas komplizierten Prozess in Gang. Testen wir aber zunächst
die Funktion an unserem Beispiel, und zwar wieder innerhalb der
Testfunktion:
(defun test( / a b c)
(setq a 100 b 200 c 400)
(let* '( (a 1)
(b (1+ a))
(c (1+ b))
)
'(list a b c)
)
)
=> (1 2 3)
Aus dem Ergebenis können wir schliessen, dass bei let* jede
Variable sofort ihren entsprechenden Namensraum hat. Die namensgleichen
übergeordneten Symbole werden verdeckt - in diesem Beispiel spielen sie
keine Rolle mehr!
Die Funktion let* arbeitet rekursiv. Die jeweils erste
Variablenbindung wird in einen Aufruf von let umgewandelt,
ind den ein let* ohne diese erste Bindung eingebettet ist.
Hier noch einmal zum besseren Verständnis die Schritte, wie sich der
Ablauf vollzieht:
; Der Aufruf
(let* '( (a 1)
(b (1+ a))
(c (1+ b))
)
'(list a b c)
) => (1 2 3)
;Erstes let aufgelöst
( (lambda (a)
(let* '( (b (1+ a))
(c (1+ b))
)
'(list a b c)
)
)
1
) => (1 2 3)
;zweites let aufgelöst
( (lambda (a)
( (lambda (b)
(let* '( (c (1+ b)))
'(list a b c)
)
)
2
)
)
1
) => (1 2 3)
; drittes let aufgelöst
( (lambda (a)
( (lambda (b)
( (lambda (c)
'(list a b c)
)
3
)
)
2
)
)
1
) => (1 2 3)