Du weißt bereits, wie du eine Quotation aus einzelnen Teilen zusammensetzt und ausführst. Eine Quotation ist einfach eine Sequenz von Code, also gelten die Sequenz-Wörter, die du kennst, auch für sie:
USING: kernel math prettyprint quotations sequences ;
{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9
[ 1 + ] dup append . ! => [ 1 + 1 + ]
append verbindet zwei Quotations, concat flacht ein Array aus ihnen ab, map verwandelt Daten in Quotations, und >quotation macht das Ergebnis aufrufbar. MACRO: nimmt genau diese Fähigkeit und verlagert sie in die Compilezeit.
Eine MACRO:-Definition sieht aus wie ein gewöhnliches Wort, aber ihr Rumpf läuft während dein Programm kompiliert wird, nicht während es läuft. Der Rumpf muss eine Quotation auf dem Stack hinterlassen. Der Compiler fügt diese Quotation dann an der Aufrufstelle ein: Er ersetzt den Makroaufruf durch den Code, den der Rumpf erzeugt hat. Zur Laufzeit ist kein Makro mehr übrig, nur der generierte Code.
USING: kernel macros sequences ;
MACRO: twice ( quot -- spliced )
dup append ;
Überall dort, wo du [ 1 + ] twice schreibst, macht der Compiler Folgendes:
[ 1 + ] auf dem Stack aus,dup append baut die Quotation [ 1 + 1 + ] auf,1 + 1 +, an dieser Stelle ein.Ein Wort, das twice aufruft, wird also kompiliert, als hättest du 1 + 1 + von Hand geschrieben:
: inc-twice ( x -- y ) [ 1 + ] twice ;
5 inc-twice . ! => 7
Der entscheidende Gegensatz: Ein normales Wort, das eine Quotation entgegennimmt, ruft sie zur Laufzeit mit call auf. Ein Makro ruft nie etwas auf. Es übergibt dem Compiler neuen Code, den dieser an seiner Stelle kompiliert.
Weil der Rumpf zur Compilezeit läuft, müssen die Eingabewerte eines Makros dann bekannt sein. An der Aufrufstelle müssen sie Literale sein: eine [ … ]-Quotation, eine Zahl, ein Array, ein String. Einen Wert, der erst zur Laufzeit existiert, kann man einem Makro nicht übergeben.
! DOCTEST: SKIP — illustrative call sites, not standalone code
[ 1 + ] twice ! ok: the quotation is a literal
[ 1 + ] 3 repeat-quot ! ok: the quotation and the number are literals
Der Rumpf eines Makros ist gewöhnlicher Code, der eine Quotation zurückgibt. Die Wörter, die du schon kennengelernt hast, erledigen den Großteil der Arbeit:
| um … zu bauen | verwendest du |
|---|---|
[ A B ] aus [ A ] und [ B ]
|
append |
| eine Quotation aus einem Array aus ihnen | concat >quotation |
| eine Quotation pro Element einer Sequenz |
map, dann concat >quotation
|
Zwei weitere Wörter aus sequences und kernel vervollständigen den Werkzeugkasten.
<repetition> (in sequences) erzeugt eine Sequenz aus n Kopien eines Elements. Ihr Stack-Effekt ist ( length elt -- repetition ): Die Anzahl kommt zuerst. Das Ergebnis ist eine virtuelle Sequenz, also flachst du sie auf die übliche Weise ab:
3 [ 1 + ] <repetition> concat >quotation . ! => [ 1 + 1 + 1 + ]
curry (in kernel) stellt einer Quotation einen Wert voran und erzeugt so eine neue Quotation, die diesen Wert zuerst auf den Stack legt:
3 [ + ] curry . ! => [ 3 + ]
{ 1 2 3 } [ . ] curry . ! => [ { 1 2 3 } . ]
Mit curry backst du ein bestimmtes Element in eine Quotation ein, und genau das macht aus { 10 20 30 } und [ + ] die Quotationen [ 10 + ] [ 20 + ] [ 30 + ].
In manchen Sprachen brauchen Makros eine spezielle Quote-/Unquote-Syntax und zusätzliche Maschinerie, um Namenskonflikte zu vermeiden. Factor braucht nichts davon: Eine Quotation ist bereits eine Sequenz von aufgelösten Wörtern. Der Parser hat jedes dup, + und append nachgeschlagen, als er die [ … ] gelesen hat. Der Rumpf eines Makros ist also einfach die Sequenzbearbeitung, die du ohnehin schon machst. Sie gibt nur eben eine Quotation zurück.
$[ … ]
Ein Makro gibt eine Quotation zurück, die in einen Aufruf eingefügt wird. Wenn du stattdessen einen einfachen Wert möchtest, der zur Parsezeit berechnet wird, dann führt $[ code ] den code aus, während die Datei gelesen wird, und faltet das Ergebnis als Literal in den Quelltext ein:
! DOCTEST: SKIP — compile-time value, shown for contrast
USING: calendar literals ;
CONSTANT: build-time $[ now ]
$[ … ] stammt aus dem Vokabular literals, das die Wörter sammelt, mit denen man Parsezeit-Ergebnisse in Literale interpoliert. Es ist der Vollständigkeit halber erwähnt. Die vier Schablonen, die du schreiben wirst, twice, repeat-quot, compose-many und each-literal, sind alle MACRO:-Deklarationen.
Du betreust das Toolkit zur Codegenerierung einer Signalstation. Jedes Makro ist eine Schablone: eine kleine Vorlage, mit der der Compiler an jeder Aufrufstelle spezialisierten Code ausstanzt.
Du schreibst nur die vier Makros. Die Testdatei definiert kleine Wrapper-Wörter, die deine Makros aufrufen (ein Makro lässt sich nur anhand des Codes beobachten, den es erzeugt), und prüft dann die Laufzeitergebnisse der Wrapper. Lass die Wrapper so, wie sie sind.
Für jede Aufgabe bekommt der Body die literalen Eingaben der Aufrufstelle auf dem Stack und muss eine Quotation hinterlassen. Die Zeilen mit „compiles to" zeigen den geradlinigen Code, den deine Quotation erzeugen soll.
twice nimmt eine literale Quotation und gibt eine Quotation zurück, die
die ursprüngliche zweimal hintereinander ausführt.
! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ] -> must build [ 1 + 1 + ]
5 inc-twice . ! => 7
repeat-quot nimmt eine literale Quotation und eine literale Anzahl n
und gibt eine Quotation zurück, die die ursprüngliche n-mal hintereinander
ausführt. n = 0 gibt die leere Quotation [ ] zurück.
! : inc-thrice ( x -- y ) [ 1 + ] 3 repeat-quot ;
! body receives [ 1 + ] 3 -> must build [ 1 + 1 + 1 + ]
5 inc-thrice . ! => 8
compose-many nimmt ein literales Array von Quotationen und gibt eine
einzelne Quotation zurück, die jede der Reihe nach ausführt. Ein leeres
Array gibt die leere Quotation zurück.
! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] } -> must build [ 1 + 2 * 1 - ]
5 pipeline . ! => 11
each-literal nimmt eine literale Sequenz und eine Quotation. Sie gibt
eine Quotation zurück, die für jedes Element der Sequenz dieses Element
auf den Stack legt und dann die Quotation ausführt. Die Iteration findet
schon zur Compilezeit statt, sodass der erzeugte Code geradlinig ist.
! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ] -> must build [ 10 + 20 + 30 + ]
sum-three . ! => 60
Das ist die strukturellste der vier: Sowohl die Länge der Sequenz als auch ihre Elementwerte bestimmen den erzeugten Code. Die Hinweise führen dich Schritt für Schritt durch den Aufbau.
Melde dich bei Exercism an, um Factor mit 47 Konzepte163 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.