Tu sais déjà construire une quotation à partir de morceaux et l'exécuter. Une quotation n'est qu'une séquence de code, donc les mots de séquence que tu connais s'y appliquent :
USING: kernel math prettyprint quotations sequences ;
{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9
[ 1 + ] dup append . ! => [ 1 + 1 + ]
append réunit deux quotations, concat aplatit un tableau de celles-ci, map transforme des données en quotations, et >quotation rend le résultat appelable. MACRO: reprend exactement cette compétence pour la déplacer vers le temps de compilation.
Une définition MACRO: ressemble à un mot ordinaire, mais son corps s'exécute au moment où ton programme est compilé, et non au moment où il s'exécute. Le corps doit laisser une seule quotation sur la pile. Le compilateur insère alors cette quotation au site d'appel : il remplace l'appel de macro par le code produit par le corps. Au moment de l'exécution, il ne reste aucune macro, seulement le code généré.
USING: kernel macros sequences ;
MACRO: twice ( quot -- spliced )
dup append ;
Partout où tu écris [ 1 + ] twice, le compilateur :
[ 1 + ] sur la pile,dup append construit la quotation [ 1 + 1 + ],1 + 1 +, à cet endroit.Ainsi, un mot qui appelle twice se compile comme si tu avais écrit 1 + 1 + à la main :
: inc-twice ( x -- y ) [ 1 + ] twice ;
5 inc-twice . ! => 7
Le contraste essentiel : un mot normal qui prend une quotation la call au moment de l'exécution. Une macro n'appelle jamais rien : elle remet au compilateur du nouveau code à compiler à sa place.
Comme le corps s'exécute au moment de la compilation, les entrées d'une macro doivent être connues à ce moment-là. Elles doivent être des littéraux au site d'appel : une quotation [ … ], un nombre, un tableau, une string. Une valeur qui n'existe qu'au moment de l'exécution ne peut pas être transmise à une macro.
! 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
Le corps d'une macro est du code ordinaire qui renvoie une quotation. Les mots que tu connais déjà font l'essentiel du travail :
| pour construire… | utilise |
|---|---|
[ A B ] à partir de [ A ] et de [ B ]
|
append |
| une quotation à partir d'un tableau de celles-ci | concat >quotation |
| une quotation par élément d'une séquence |
map, puis concat >quotation
|
Deux mots supplémentaires, issus de sequences et kernel, complètent la panoplie.
<repetition> (dans sequences) crée une séquence de n copies d'un même élément. Son effet de pile est ( length elt -- repetition ), le nombre venant en premier. Le résultat est une séquence virtuelle, qu'il faut donc aplatir de la façon habituelle :
3 [ 1 + ] <repetition> concat >quotation . ! => [ 1 + 1 + 1 + ]
curry (dans kernel) ajoute une valeur au début d'une quotation, ce qui produit une nouvelle quotation qui empile cette valeur en premier :
3 [ + ] curry . ! => [ 3 + ]
{ 1 2 3 } [ . ] curry . ! => [ { 1 2 3 } . ]
curry est le moyen d'incorporer un élément précis dans une quotation, exactement ce qui transforme { 10 20 30 } et [ + ] en [ 10 + ] [ 20 + ] [ 30 + ].
Dans certains langages, les macros ont besoin d'une syntaxe spéciale de citation/décitation et d'une machinerie supplémentaire pour éviter les collisions de noms. Factor n'a besoin de rien de tout cela : une quotation est déjà une séquence de mots résolus, car l'analyseur syntaxique a recherché chacun des dup, + et append au moment où il a lu le [ … ]. Le corps d'une macro n'est donc que la manipulation de séquences que tu pratiques déjà, qui se trouve renvoyer une quotation.
$[ … ]
Une macro renvoie une quotation destinée à être insérée dans un appel. Si tu veux plutôt une simple valeur calculée au moment de l'analyse, $[ code ] exécute code pendant que le fichier est lu et intègre le résultat dans la source sous forme de littéral :
! DOCTEST: SKIP — compile-time value, shown for contrast
USING: calendar literals ;
CONSTANT: build-time $[ now ]
$[ … ] vient du vocabulaire literals, qui rassemble les mots servant à interpoler dans des littéraux des résultats calculés au moment de l'analyse. Il est mentionné pour être complet. Les quatre pochoirs que tu vas écrire, twice, repeat-quot, compose-many et each-literal, sont tous des déclarations MACRO:.
Tu t'occupes de la boîte à outils de génération de code d'une station de signaux. Chaque macro est un stencil : un petit gabarit que le compilateur utilise pour produire du code spécialisé à chaque site d'appel.
Tu n'écris que les quatre macros. Le fichier de tests définit de petits mots enveloppes qui appellent tes macros (une macro ne s'observe qu'à travers le code qu'elle génère), puis il vérifie ce que ces enveloppes renvoient à l'exécution. Laisse ces enveloppes telles quelles.
Pour chaque tâche, le corps reçoit sur la pile les entrées littérales du site d'appel et doit laisser une seule quotation. Les lignes « compiles to » montrent le code linéaire que ta quotation doit produire.
twice prend une quotation littérale et renvoie une quotation qui exécute
l'originale deux fois de suite.
! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ] -> must build [ 1 + 1 + ]
5 inc-twice . ! => 7
repeat-quot prend une quotation littérale et un nombre littéral n, et
renvoie une quotation qui exécute l'originale n fois de suite.
n = 0 renvoie la quotation vide [ ].
! : inc-thrice ( x -- y ) [ 1 + ] 3 repeat-quot ;
! body receives [ 1 + ] 3 -> must build [ 1 + 1 + 1 + ]
5 inc-thrice . ! => 8
compose-many prend un tableau littéral de quotations et renvoie une seule
quotation qui exécute chacune d'elles à tour de rôle. Un tableau vide renvoie
la quotation vide.
! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] } -> must build [ 1 + 2 * 1 - ]
5 pipeline . ! => 11
each-literal prend une séquence littérale et une quotation. Elle renvoie une
quotation qui, pour chaque élément de la séquence, empile cet élément puis
exécute la quotation : une itération qui a lieu à la compilation, ce qui donne
un code généré linéaire.
! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ] -> must build [ 10 + 20 + 30 + ]
sum-three . ! => 60
C'est la plus structurelle des quatre : la longueur de la séquence et la valeur de ses éléments façonnent le code généré. Les indices te guident pas à pas dans sa construction.
Inscris-toi sur Exercism pour apprendre et maîtriser Factor avec 47 concepts163 exercices, et un vrai mentorat humain, le tout gratuitement.