2010年4月14日水曜日

dlambda

R5RSの衛生的マクロって使い辛い、使い辛い、使い辛い……。

って文句ばっか言っててもしゃーないんで、逆の例をお見せしましょう。これは衛生的マクロだったら書きやすいんですけど、CLの伝統的マクロなら書きづらい、と言う好例だと思います。そしてあんまSchemeマクロの例示でも見ないんですよね。多分本邦初公開(笑)。

LET OVER LAMBDA Edition 1.0dlambdaと言うマクロが紹介されています。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は引数にキーワードとそこに渡したい引数、そして手続き定義を取ります。上の例で言うとキーワードが例えばincincは無引数で、そして手続き定義が(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の分配に頭を悩ますハメになるんで、記述コストは高く付くんじゃないか、と思います。

あとは、<キー>に対応する<節>が無かった場合はどうするか?要するにデフォルト挙動をどうするのか、だけ考えれば良い。もうちょっと具体的に言うと、caseelse節を取れるんで、そこをどうするか、だけ考えれば良いのです。
そこで、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 件のコメント:

コメントを投稿