って文句ばっか言っててもしゃーないんで、逆の例をお見せしましょう。これは衛生的マクロだったら書きやすいんですけど、CLの伝統的マクロなら書きづらい、と言う好例だと思います。そしてあんまSchemeマクロの例示でも見ないんですよね。多分本邦初公開(笑)。
LET OVER LAMBDA Edition 1.0に
dlambda
と言うマクロが紹介されています。d
はdestructuring(分配)のd
だそうです。コードは以下のようなものです。
(defmacro! dlambda (&body ds)
`(lambda (&rest ,g!args)
(case (car ,g!args)
,@(mapcar
(lambda (d)
`(,(if (eq t (car d))
t
(list (car d)))
(apply (lambda ,@(cdr d))
,(if (eq t (car d))
g!args
`(cdr ,g!args)))))
ds))))
筒井康隆風に言うと
俺は「ひゃあ」と叫んで椅子から5センチ程飛び上がった。
ってカンジでしょう(笑)。
まず注釈しておきますが、
defmacro!
自体がLET OVER LAMBDA Edition 1.0依存のマクロなんです。そしてdefmacro!
はonce-only
問題を解決するレイヤーだけでなく、その他の関数やマクロの上に成り立っています。まさしくOn Lispなんですが……。レイヤー組み立てる側にはいいんですが、逆方向にレイヤー降っていこうとすればシャレになりませんね(笑)。
defmacro!
を組み立てる為の全部の手順なんてここで紹介しねえぞ(笑)。んな事やってられっか、っての(笑)。ぶっちゃけ、一度ライブラリ化狙って
asdf
にしちまおうか、とか思ったんですが、asdf
作るのもメンド臭くってやってられっか、とか思いました。平たく言うと失敗したんです(爆)。わけわかんねーぞ、asdf
(怒)。こんちくしょーめ。上のワケワカメなマクロには目立った特徴が二つあります。それらは
defmacro!
を用いている以上、once-only
問題を解決したいのが第一義である。- 引数の分配のメカニズムがメンド臭い。
の二点です。
once-only
問題を説明するのはシンドイんですが……。要するに、dlambda
に与えた引数によっては二重に評価されて計算結果がおかしくなる可能性がある、と言う事です。これを避ける為にdefmacro!
を使わざるを得ないんですけど……。ここは取りあえずピンと来なくて構いません。しかし、要するに言い換えると、
once-only
問題が浮上してくるのは、CLのdefmacro
が衛生的ではないから、です。つまり、何だかんだ言って一つ目の問題はSchemeの衛生的マクロなら気にせんで構わない、と言う事です。二番目に関しては、引数分配に高階関数である
mapcar
を使ってて、これはこれで強力なテクニックなんですが、一見何をしてるのか分かりません。分からないでしょ(笑)?単純に言うと、case
式にはめ込むキーと、そこに列挙する節を上手い具合に分断する為にmapcar
で操作してるんです。ちょっと慣れたら読めるんですが、実際問題、個人で意図して
mapcar
による大技繰り出すのは構わないんですが、他人が書いたコードだと一発では分かんないっすね(苦笑)。意図してるところが見え辛い。加えると、
mapcar
が出る時って大体計算結果のリストが欲しいわけです。と言う事はmapcar
以降は部分評価になる。と言う事はバッククオート解除にコンマやコンマアットが十中八九出てくる。って事はネストしたバッククオートが出てくる……。まあ、今は意味が分からなくて結構なんですけど、要するに読みづらくなるのは確定だって事です。泣きたくなるだろ?泣きたいんだよ、こっちはよ。とっころがね~~。上の
dlambda
と同等のコードはSchemeの衛生的マクロだったらアッサリ書けるんですよ。直球勝負ですね。いや、不思議。何でこれをSchemeの代表的マクロとして紹介せんのだ、って程アッサリ仕上がります。しかも意味はCLに比べると明確です。
(define-syntax dlambda
(syntax-rules (else)
((_ (key d0 body0 ...) ... (else d1 body1 ...)) ;基本的な変換式
(lambda arg
(case (or (null? arg) (car arg))
((key) (apply (lambda d0
body0 ...) (cdr arg)))
...
(else (apply (lambda d1
body1 ...) arg)))))
((_ (key d body ...) ...) ;再帰的定義で else 節の扱いを変える
(dlambda (key d body ...) ... (else () #f)))))
CL13行に対してScheme12行です。もっとも、行数比較には意味が無いんですけど、先ほど指摘した通り、
defmacro!
の背後には恐ろしい程のコード量がある。圧縮率から言うとSchemeの衛生的マクロの圧勝でしょう。こちらはR5RS仕様書の範囲内で書きあがるのです。まあ、百聞は一見に如かず。一回LET OVER LAMBDA Edition 1.0の例示に従って動かしてみましょうか。その後、衛生的マクロでどうしてこんなに簡単に
dlambda
を書けるのか、考えてみます。
> (define count-test
(let ((count 0))
(dlambda
(inc () (set! count (+ count 1)) count)
(dec () (set! count (- count 1)) count))))
> (count-test 'inc)
1
> (count-test 'dec)
0
>
dlambda
は引数にキーワードとそこに渡したい引数、そして手続き定義を取ります。上の例で言うとキーワードが例えばinc
、inc
は無引数で、そして手続き定義が(set! count (+ count 1))
とcount
です。その形式の引数を複数取ってます。そして、
count-test
に引数としてキーワードシンボルを与えると、それに準じた手続きが実行されるわけです。局所変数count
の初期値は0だったので、'inc
を引数で与えるとcount
は1に書き換えられ、もう一度引数を'dec
で呼び出すと0が返される。もっと複雑な事も出来ますね。再びLET OVER LAMBDA Edition 1.0の例示に従ってみます。
> (define count-test
(let ((count 0))
(dlambda
(reset () (set! count 0) count)
(inc (n) (set! count (+ count n)) count)
(dec (n) (set! count (- count n)) count)
(bound (lo hi)
(set! count
(min hi
(max lo
count)))
count))))
> (count-test 'reset)
0
> (count-test 'inc 100)
100
> (count-test 'bound -10 10)
10
> (define dlambda-test
(dlambda
(something-special ()
(display "SPECIAL") (newline))
(else args
(for-each display (list "DEFAULT: " args)) (newline))))
> (dlambda-test 1 2 3)
DEFAULT: (1 2 3)
> (dlambda-test)
DEFAULT: ()
> (dlambda-test 'something-special)
SPECIAL
>
さて、Schemeの衛生的マクロだと
dlambda
が何故書きやすいのか?乱暴に言うと、原始的なパターンマッチ構文であるcase
がパターンを指定する書き方である衛生的マクロと相性が良いと言う事に他ならない、と言う事だと思います。Schemeでは
case
式は嫌われているのか、あまり目にする事が無いんですが、R5RSにキチンと定義されている組み込み構文です。(case <キー> <節1> <節2> ... )
ライブラリ構文
構文: <キー> はどんな式でもよい。各<節> は次の形式をとること。
((<データ1> ... )
<式1> <式2> ... ),
ここで各<データ> は,なんらかのオブジェクトの外部表現である。<データ> はすべて異なっていなければならない。最後の<節> は“else 節” でもよい。これは次の形式をとる。
(else <式1> <式2> ... )
.
意味:case
式は次のように評価される。<キー> が評価され,その結果が各<データ> と比較される。もし<キー> を評価した結果が,ある<データ> と(eqv?
の意味で) 等価ならば,対応する<節> の各式が左から右へと評価され,そしてその<節> の最後の式の(1個または複数個の) 結果がcase
式の(1個または複数個の) 結果として返される。もし<キー> を評価した結果がどの<データ> とも異なるとき,else
節があればその各式が評価されてその最後の(1個または複数個の) 結果がcase
式の(1個または複数個の) 結果になるが,なければcase
式の結果は未規定である。
(case (* 2 3)
((2 3 5 7) 'prime)
((1 4 6 8 9) 'composite)) => composite
(case (car '(c d))
((a) 'a)
((b) 'b)) => 未規定
(case (car '(c d))
((a e i o u) 'vowel)
((w y) 'semivowel)
(else 'consonant)) => consonant
つまり、
case
が要求するパターンにパターンとして記述した要素を当てはめれば一丁上がり、と言う事です。加えて、通常Common Lispでは
&body
でレストパラメータとして式本体をリストにしちゃうせいで分配がメンド臭かったりするわけですが、基本Schemeの衛生的マクロでは本体を「要素のパターン」として記述します。つまり、分配自体が必要がなく、そのパターン自体さえ適切に記述出来れば置換自体は衛生的マクロがすべて面倒を見てくれるわけです。衛生的マクロ版
dlambda
の変換の基本的アイディアは次の通りです。
(dlambda (key d body ...) ...) ;; このパターンを
||
変換
||
\ /
\/
(lambda arg ;; こう変換する
(case (car arg)
((key) (apply (lambda d
body ...) (cdr arg)))
...))
マクロ
dlambda
はクロージャを返せば良いので、(lambda arg ..)
で書き始めます。引数arg
は可変長引数なんで括弧は要りません。そして、dlambda
の記述パターンにarg
はありませんが、これはリストの第一要素にdlambda
を使った式が来れば後続する要素が実引数として処理されるんでこれで良いのです。λ式
の性質ですよね。そして、
arg
で外部から与えられる実引数の第一要素は<キー>
になります。以降の要素は処理されるべきものとしてリストにまとめられています。従って、case
内の
(apply (lambda d body ...) (cdr arg))
が生きてくる。
基本的にはこれだけ、なんです。考え方としてはCL版の
dlambda
より簡単です。CLのLegacy Macroだと&body
の分配に頭を悩ますハメになるんで、記述コストは高く付くんじゃないか、と思います。あとは、
<キー>
に対応する<節>
が無かった場合はどうするか?要するにデフォルト挙動をどうするのか、だけ考えれば良い。もうちょっと具体的に言うと、case
はelse
節を取れるんで、そこをどうするか、だけ考えれば良いのです。そこで、
dlambda
のパターンをelse
と言うキーワードを用いて次のように変更します。
(dlambda (key d0 body0 ...) ... (else d1 body1 ...))
つまり、
dlambda
は必ずelse
節を持たなきゃならないと仮定する。else
が記述された場合、もはやarg
の第一要素は<節>
の<データ>
を意味しません。従って、
(else (apply (lambda d body ...) arg))
が
else
節になります。次にデフォルトの挙動が無引数の場合にどうなるか、です。つまり
arg
が空リストだった場合。arg
が空リストの場合、CLと違ってSchemeは(car arg)
だとエラーを返します。これを何とかしないとならないんですが、ここはcase
の次の性質により回避は簡単です。<キー>
はどんな式でもよい。
従って、
(case (or (null? arg) (car arg)) ...)
で書いて構わないのです。返り値が
#t
だろうと(car arg)
だろうと結局お構いなし、ですね。と言うか、#t
が返された途端にelse
節が実行されます。つまり、ここまでで
else
と必須として、次のような変換パターンで95%は完成するわけです。
(dlambda (key d0 body0 ...) ... (else d1 body1 ...))
||
変換
||
\ /
\/
(lambda arg
(case (or (null? arg) (car arg))
((key) (apply (lambda d
body ...) (cdr arg)))
...
(else (apply (lambda d
body ...)))))
あとは衛生的マクロでの再帰的定義を用いて
else
を記述しないパターンを定義すれば良いわけです。else
節が#f
を返すようにして潰しちゃいます。
(dlambda (key d body ...) ...)
||
変換
||
\ /
\/
(dlambda (key d body ...) ... (else () #f))
これで完成ですね。
0 件のコメント:
コメントを投稿