Chapter_7: "Casting SPELs" $
Wir lernen in diesem Kapitel ein unglaublich machtvolles Sprachelement kennen. Conrad Barski nennt es SPEL ("Semantic Program Enhancement Logic"). Bekannter ist es unter dem Namen Makro, hat aber mit einem Makro in C++ nur wenig gemeinsam. Eigentlich geht es hier, wie der Name SPEL schon andeutet, um Zauberei. SPELs ermöglichen uns, auf der elementarsten Ebene die Sprache von Maxima selbst für unsere Zwecke zu gestalten. Wir werden zum Designer einer Sprache.
Die Funktion, die in Maxima Makros ermöglicht, heißt buildq, build quoted. Dieser Name beschreibt, wie wir gleich sehen werden, eigentlich sehr schön, worum es bei einem Makro geht.
Ein quote ist ein Hochkomma und wir wissen schon, dass gequotete Ausdrücke nicht ausgewertet werden. Ausdrücke schreiben zu können, die erst zu einer späteren Zeit ausgewertet werden, eröffnet ungeahnte Möglichkeiten, die, wie gesagt, schon manchmal an Zauberei grenzen. Eine Makrodefinition wird mit einem ::= statt einem := geschrieben. Der zusätzliche : deutet die nachträgliche Auswertung an. Conrad Barski zu Ehren soll buildq nun in SPEL umbenannt werden.
SPEL([rest])::= buildq([rest],buildq(splice(rest)))$
Diese Umbenennung zeigt auf raffinierte, aber auch sehr abstrakte Weise, wie buildq funktioniert. buildq besitzt grundsätzlich zwei Argumente, eine Liste mit Substitutionsparametern und einen Ausdruck. Würde hier wie gewohnt von innen nach außen ausgewertet werden, bekämen wir sofort eine Fehlermeldung. Der Parser würde feststellen, dass das innere buildq nur ein einziges Argument besitzt. Statt dessen werden zuerst einmal nur die Werte der Substitutionsparameter gequotet in den Ausdruck eingesetzt. Die Auswertung findet dann anschließend statt.
Von links nach rechts: Der Ausdruck [rest] in einer Funktionsdefinition
bedeutet, dass diese Funktion optionale Argumente besitzt. Diese werden dann in einer
Liste namens rest gesammelt und weitergereicht.
buildq setzt nun diese Liste in den Ausdruck
buildq(splice(rest)) ein.
Eine besondere Rolle spielt bei dieser Ersetzung die Funktion splice.
splice macht aus der Liste rest wieder einzelne Argumente.
Wenn SPEL also nun vorschriftsmäßig mit zwei Parametern aufgerufen
wird, wird letztendlich daraus wieder buildq mit exakt denselben
zwei Argumenten. Auf diese Weise können wir nun das Symbol SPEL
an Stelle von buildq verwenden.
Erst ersetzen, später auswerten. Dieser Trick erlaubt es, Funktionen zu schreiben, die zur Laufzeit eines Programms Funktionen oder jeden anderen beliebigen Code schreiben.
In der nun folgenden Definition des Makros game_action werden wir uns sehr konkret ansehen können, wie SPEL funktioniert.
game_action(command,subj,obj,place,[rest])::= SPEL( [command,subj,obj,place,rest], block( infix(command), command(subject,object):= block( if location = place and subject = subj and object = obj and have(subj) then apply(sconcat,rest) else sconcat("you cannot ",command," like that. ") )))$
Bei näherer Betrachtung sehen wir, dass SPEL tatsächlich nur zwei Argumente besitzt. Die Liste der Ersetzungsparameter und einen Block. Und der Block enthält genau das, was in den Definitionen von weld und dunk analog ist. Es ist die Struktur. game_action ist im Prinzip eine Art Schablone. An passender Stelle werden dann in diese die Namen des Kommandos, des Subjekts, Objekts und des Ortes, an dem die Aktion stattfinden soll, eingesetzt. Die unterschiedlichen Textausgaben der einzelnen Kommandos werden dann als optionale Parameter eingetragen.
Ein Beispiel sagt hier mehr als tausend Worte. Wir definieren mit Hilfe von game_action das Kommando weld noch einmal neu. Wenn wir dabei die Eingabe mit einem Semikolon abschließen, erhalten wir ja bekanntlich als Antwort den Wert dieses Makros. In diesem Fall ist das die Definition des Operators weld.
game_action("weld",chain,bucket,attic, if have(bucket) and not chain_welded then ( chain_welded: true, "the chain is now securely welded to the bucket. " ) else "you do not have a bucket. ");
==> subject weld object := block( if location = attic and subject = chain and object = bucket and have(chain) then apply(sconcat, [if have(bucket) and not chain_welded then ( chain_welded : true, "the chain is now securely welded to the bucket. ") else "you do not have a bucket. "]) else sconcat("you cannot ", "weld"," like that. "))
SPEL!
Ob der Lisp Compiler, der in unserem Spiel der Frosch des Zauberers zu sein scheint, dabei tatsächlich ins Schwitzen kommt, bleibt zu bezweifeln. |
Das Makro game_action ermöglicht es, dass wir uns bei der Definition der Kommandos auf die speziellen Einzelheiten des jeweiligen Kommandos konzentrieren können. Die ganze Sache sieht dann wesentlich schöner und übersichtlicher aus. Im Falle von dunk dann so.
game_action("dunk",bucket,well,garden, if chain_welded then ( bucket_filled: true, "the bucket is now full of water. " ) else "the water level is too low to reach. ")$
Und nun unser letztes Kommando. splash.
Befindet sich Wasser im Eimer, wecken wir damit den Zauberer. |
game_action("splash",bucket,wizard,living_room, if not bucket_filled then "the bucket has nothing in it. " else if have(frog) then sconcat( "the wizard awakens and sees that you stole his frog. ", "he is so upset he banishes you to the netherworlds ", "- you lose! the end. ") else sconcat( "the wizard awakens from his slumber and greets you warmly. ", "he hands you the magic low_carb donut - you win! the end. ") )$
Du hast damit ein vollständiges Text
Adventure Game geschrieben!
|