Eigentlich sind BLOCK
und RETURN-FROM
ja Sonderoperatoren. Da ich meinen Interpreter aber nicht zu sehr aufblähen möchte (schon erst recht nicht, weil er irgendwann einen Compiler zur Seite bekommen wird, der dieselben Sonderoperatoren auch alle nochmal verstehen muß), versuche ich, die Anzahl dieser zu minimieren, weshalb ich BLOCK
und RETURN-FROM
als Makros über CATCH
und THROW
implementiert habe.
Es gibt ein Paper darüber, wie das geht, doch hat der dort angegebene Code leider einen fatalen Fehler: Er setzt die Existenz eines Operators BLOCK-TO-TAGNAME
voraus, der aus einem Symbol ein anderes Symbol macht, das erstens (wie ein GENSYM
) nirgendwo sonst vorkommen und andererseits für jede Eingabe eindeutig sein soll (d.h. BLOCK-TO-TAGNAME
ist eine injektive Funktion im mathematischen Sinne von SYMBOL
nach SYMBOL
). Das Schreiben einer solchen Funktion ist nun dummerweise nicht möglich*, wenn man sich nicht Nebenwirkungen zunutze macht, wie z.B. das Auffüllen einer Hashtable. Letzteres wäre wiederum speicherverwaltungstechnisch ein furchtbares Problem, weil die Einträge, jedenfalls in einem einigermaßen einfachen Ansatz, niemals wieder entfernt werden könnten und immer mehr würden.
Also einen Schritt zurück. Wofür ist BLOCK-TO-TAGNAME
eigentlich da? Der Grund für seine Existenz liegt darin, daß man gerne das Mapping zwischen Blocknamen und CATCH
-Tagnamen vom Makro BLOCK
, in dem der CATCH
-Tag eingeführt wird, zum Makro RETURN-FROM
übermitteln möchte, in dem es verwendet werden muß. Diese Zuordnung muß lexikalisch eindeutig sein, d.h. folgendes liefert 3 zurück, nicht 2:
(block mulk
(let ((fn (lambda () (return-from mulk 3))))
(block mulk (funcall fn))
2)
Folglich muß der äußere MULK
-Block einen anderen CATCH
-Tag verwenden als der innere. Ich löse das zur Kompilierzeit, indem ich in der Auswertungsumgebung der Makros eine Zuordnungstabelle anlege, die nach dem Kompilieren verschwindet:
(defmacro block (block-name &body body)
(let ((catch-tag (gensym)))
`(compiler-let ((%block-mapping% (acons ',block-name ',catch-tag %block-mapping%))))
(catch ',catch-tag
,@body))))
(defmacro return-from (block-name &optional value &environment env)
`(throw #,(cdr (assoc block-name %block-mapping% :test #'eq))
,value))
Dem geneigten Leser wird aufgefallen sein, daß ich hier zwei Features verwendet habe, die es in ANSI Common Lisp nicht mehr gibt, obgleich es sie früher einmal gab: #,
und COMPILER-LET
. Den Platz von #,
hat heute LOAD-TIME-VALUE
eingenommen. In der Tat läßt sich LOAD-TIME-VALUE
in SBCL ähnlich wie #, verwenden und respektiert sogar die Auswertungsumgebung des Compilers:
CL-USER> (sb-cltl2:compiler-let ((x 10))
(load-time-value x))
10
Was ist aber mit COMPILER-LET
? Nicht, daß mich jemand daran hindern würde, es in Toilet Lisp zu implementieren -- vielleicht mache ich das auch noch. Doch wollte ich nicht möglichst wenige Sonderoperatoren definieren? Wenn es doch schon einen gäbe, der diesen Zweck erfüllte...
Und den gibt es: MACROLET
läßt sich effektiv für dieselben Sachen einsetzen wie einstmals COMPILER-LET
, auch wenn sein Existenzsinn eigentlich woanders liegt. Ein bißchen Hacking hier und da, et voilà:
(defmacro block (block-name &body body)
(let ((catch-tag (gensym)))
`(macrolet ((%block-mapping% () `(quote ,(acons ',block-name ',catch-tag (%block-mapping%)))))
(catch ',catch-tag
,@body))))
(defmacro return-from (block-name &optional value &environment env)
`(throw ',(cdr (assoc block-name (cadr (macroexpand `(%block-mapping%)
env))
:test 'eq))
,value))
Häßlicher als zuvor mit #,
und COMPILER-LET
, soviel ist sicher. Dafür funktionieren die Definitionen auch in CMU CL, und das ist doch auch ganz schön. Es schadet dem Debuggingaufwand jedenfalls sicherlich nicht.
MAKE-SYMBOL
konstruieren.) Angenommen, es gäbe eine injektive Funktion f von SYMBOL nach SYMBOL, die nicht die Abstraktionsbarriere bricht, d.h. sie darf nur Namen und Paket des Eingabesymbols betrachten und die Identität mit anderen Symbolen vergleichen. Dann sind A und B für f aber ununterscheidbar aufgrund des Namens und des Pakets. Die einzige Unterscheidungsmöglichkeit wäre Identitätsvergleich mit einer der Funktion zur Definitionszeit (Achtung: Hier nehmen wir an, daß die Funktion konstruiert wurde, denn sonst muß es einen solchen Zeitpunkt nicht geben!) bekannten Symbol, aber auch dies wird für A und B gleichermaßen stets das Ergebnis falsch liefern, weil A und B frisch sind. Also sind A und B für f nicht unterscheidbar, d.h. f(A) = f(B). Das ist ein Widerspruch, also kann f zu keinem bestimmten Zeitpunkt definiert worden sein. Folglich ist f nicht konstruierbar.
Comments
Submit a comment
Note: This website uses a JavaScript-based spam prevention system. Please enable JavaScript in your browser to post comments. Comment format is plain text. Use blank lines to separate paragraphs.