トラック
/
Factor
Factor
/
演習
/
信号のステンシル
信号のステンシル

信号のステンシル

学習演習

はじめに

クオーテーションを部品から組み立てて実行する方法は、もう知っていますね。クオーテーションはコードの並びにすぎないので、知っているシーケンスのワードがそのまま使えます。

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. スタックに[ 1 + ]を置いて本体を実行します。
  2. dup appendがクオーテーション[ 1 + 1 + ]を組み立てます。
  3. そのコード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つのクォーテーションを残さなければなりません。「コンパイル結果」の行は、あなたのクォーテーションが生成すべき直線的なコードを示します。

1. 2回繰り返す

twiceはリテラルのクォーテーションを受け取り、元のクォーテーションを続けて2回実行するクォーテーションを返します。

! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ]  ->  must build [ 1 + 1 + ]
5 inc-twice .   ! => 7

2. N回の繰り返し

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

3. まとめて合成

compose-manyはクォーテーションのリテラル配列を受け取り、それぞれを順番に実行する1つのクォーテーションを返します。空の配列のときは空のクォーテーションを返します。

! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] }  ->  must build [ 1 + 2 * 1 - ]
5 pipeline .   ! => 11

4. 展開された繰り返し

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歩ずつ組み立てていきます。

GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Factor Exercism

信号のステンシルを始める準備はできましたか?

Exercismに登録すれば、47個のコンセプト163個の演習、そして本物の人間によるメンタリングとともに、Factorを学んでマスターできます。すべて無料です。