クオーテーションを部品から組み立てて実行する方法は、もう知っていますね。クオーテーションはコードの並びにすぎないので、知っているシーケンスのワードがそのまま使えます。
USING: kernel math prettyprint quotations sequences ;
{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9
[ 1 + ] dup append . ! => [ 1 + 1 + ]
appendは2つのクオーテーションを連結し、concatはそれらの配列を平坦化し、mapはデータをクオーテーションに変換し、>quotationは結果を呼び出し可能にします。MACRO:は、まさにその技術を取ってコンパイル時へと移します。
MACRO:の定義は普通のワードに見えますが、その本体はプログラムが実行されるときではなく、コンパイルされているときに実行されます。本体はスタックに1つのクオーテーションを残さなければなりません。するとコンパイラーがそのクオーテーションを呼び出し箇所に展開します。マクロの呼び出しが、本体が生成したコードに置き換わるのです。実行時にはマクロはもう残っておらず、生成されたコードだけが存在します。
USING: kernel macros sequences ;
MACRO: twice ( quot -- spliced )
dup append ;
[ 1 + ] twiceと書いた場所では、コンパイラーが次のことをします。
[ 1 + ]を置いて本体を実行します。dup appendがクオーテーション[ 1 + 1 + ]を組み立てます。1 + 1 +をその場所に展開します。つまり、twiceを呼び出すワードは、1 + 1 +を手で書いたかのようにコンパイルされます。
: inc-twice ( x -- y ) [ 1 + ] twice ;
5 inc-twice . ! => 7
重要な対比はここです。クオーテーションを受け取る普通のワードは、実行時にそれをcallします。マクロは何も呼び出しません。代わりにコンパイラーに新しいコードを渡し、その場所でコンパイルさせます。
本体はコンパイル時に実行されるので、マクロの入力はその時点でわかっていなければなりません。呼び出し箇所ではリテラルである必要があります。つまり[ … ]のクオーテーション、数値、配列、文字列です。実行時にしか存在しない値は、マクロに渡せません。
! 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
マクロの本体は、クオーテーションを返す普通のコードです。すでに見てきたワードがほとんどをこなしてくれます。
| 組み立てたいもの… | 使うもの |
|---|---|
[ A ]と[ B ]から[ A B ]を作る |
append |
| それらの配列から1つのクオーテーション | concat >quotation |
| シーケンスの要素ごとに1つのクオーテーション |
map、次にconcat >quotation
|
sequencesとkernelから、あと2つのワードがこの道具箱を完成させます。
<repetition>(sequencesにあります)は、1つの要素のn個のコピーからなるシーケンスを作ります。スタック効果は( length elt -- repetition )で、個数が先に来ます。結果は仮想シーケンスなので、いつもどおり平坦化します。
3 [ 1 + ] <repetition> concat >quotation . ! => [ 1 + 1 + 1 + ]
curry(kernelにあります)は、クオーテーションの先頭に値を付け加え、その値を最初にプッシュする新しいクオーテーションを作ります。
3 [ + ] curry . ! => [ 3 + ]
{ 1 2 3 } [ . ] curry . ! => [ { 1 2 3 } . ]
curryを使うと、特定の要素をクオーテーションに焼き込めます。まさに{ 10 20 30 }と[ + ]を[ 10 + ] [ 20 + ] [ 30 + ]に変えるものです。
言語によっては、マクロに特別なクオートやアンクオートの構文、そして名前の衝突を避けるための追加の仕組みが必要です。Factorにはどれも要りません。クオーテーションはすでに解決済みのワードの並びだからです。パーサーは[ … ]を読むときに、dup、+、appendのそれぞれを解決しています。つまりマクロの本体は、いつも行っているシーケンス操作そのもので、それがたまたまクオーテーションを返すだけなのです。
$[ … ]
マクロは、呼び出しに展開されるクオーテーションを返します。代わりに、解析時に計算される普通の値が欲しいときは、$[ code ]がファイルを読み込んでいる間にcodeを実行し、その結果をリテラルとしてソースに折り込みます。
! DOCTEST: SKIP — compile-time value, shown for contrast
USING: calendar literals ;
CONSTANT: build-time $[ now ]
$[ … ]は、解析時の結果をリテラルに埋め込むためのワードを集めたliteralsボキャブラリにあります。ここでは網羅のために触れておきます。これから書く4つのステンシル、twice、repeat-quot、compose-many、each-literalは、どれもMACRO:の宣言です。
信号所のコード生成ツールキットを保守しています。各マクロはステンシル、つまりコンパイラーが各呼び出し地点で特化したコードを打ち出すのに使う小さなテンプレートです。
書くのは4つのマクロだけです。テストファイルには、あなたのマクロを呼び出す小さなラッパーワードが定義されていて(マクロは、それが生成するコードを通してしか観察できません)、そのラッパーの実行時の結果を確認します。ラッパーはそのままにしておいてください。
各タスクでは、本体はスタック上で呼び出し地点のリテラル入力を受け取り、1つのクォーテーションを残さなければなりません。「コンパイル結果」の行は、あなたのクォーテーションが生成すべき直線的なコードを示します。
twiceはリテラルのクォーテーションを受け取り、元のクォーテーションを続けて2回実行するクォーテーションを返します。
! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ] -> must build [ 1 + 1 + ]
5 inc-twice . ! => 7
repeat-quotはリテラルのクォーテーションとリテラルのカウントnを受け取り、元のクォーテーションを続けてn回実行するクォーテーションを返します。n = 0のときは空のクォーテーション[ ]を返します。
! : inc-thrice ( x -- y ) [ 1 + ] 3 repeat-quot ;
! body receives [ 1 + ] 3 -> must build [ 1 + 1 + 1 + ]
5 inc-thrice . ! => 8
compose-manyはクォーテーションのリテラル配列を受け取り、それぞれを順番に実行する1つのクォーテーションを返します。空の配列のときは空のクォーテーションを返します。
! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] } -> must build [ 1 + 2 * 1 - ]
5 pipeline . ! => 11
each-literalはリテラルのシーケンスとクォーテーションを受け取ります。そして、シーケンスの各要素について、その要素をプッシュしてからクォーテーションを実行するクォーテーションを返します。コンパイル時に起こる繰り返しなので、生成されるコードは直線的になります。
! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ] -> must build [ 10 + 20 + 30 + ]
sum-three . ! => 60
これは4つの中で最も構造的なマクロです。シーケンスの長さと要素の値の両方が、生成されるコードを形作ります。ヒントでは、それを1歩ずつ組み立てていきます。