2010年4月3日土曜日

なぜマクロの使いどころは分かり辛いのか

9LISPでもマクロに入るらしいんで、ちょこっとしたメモを書いておこうと思います。何故マクロの学習が難しいのか、と言う一つの答えですね。

LISP入門と言う超古い本に紹介されてるんですが、大昔のLISP(それこそMacLispやInterlispより歴史が古いLisp1.5の流れ)だと関数定義ってもっと種類があったんです。下にそれを抜書してみます。







関数の種類




実引数を評価する
(eval)

実引数はそのまま
(quote)

実引数は仮引数にそのまま渡す
(spread)

EXPR型
(ARRAY)

NEXPR型

実引数はひとまとめ
(non-spread)

LEXPR型

FEXPR型

関数と実引数をまとめる

---

MACRO型

実は超古典的なLispの文脈に於いては、関数定義の種類と言うのは4種類くらいあったんです。
このうち、可変長引数を用いる関数に関しては、現代のLispでは関数定義で方法を変える代わりにレストパラメータで解決するようになりました。問題は、古典的Lispでは引数を評価しない関数が存在した、と言う辺りです。
同書からちょっと引用してみましょうか。

EXPR型の関数は、定義した引数(仮引数)の個数と実際の引数(実引数)の個数が一致して、しかも実引数の値を求めるために評価(evaluation)が行われます。本書で扱ったプログラムはほとんど全部EXPR型で定義されています。最も常識的な関数型です。
しかし、EXPR型ではうまく定義されない関数もあります。これらを定義するにはEXPR型以外の型が必要となります。


そして、同書では関数QUOTEの定義を紹介しています(ここに注目!QUOTEは関数なんです!)。

(DN QUOTE (X) X)

これビックリなんですよね。少なくとも現代的なLispではこんなQUOTEはマクロを使っても定義出来ません。まさしく引数を評価しないため「だけの」目的の為に存在してる関数定義方法があったわけです。

実はSchemeが登場する前後辺りで、ミニマリスティックな観点でLispの大刷新が起こりました。「引数を評価しない」のはマクロも同じなんで、「引数を評価しない関数」群がマクロに統合されたんです。システム的には当然の考え方ですよね。
一方、マクロは必ず展開が行われます。つまり、一種マクロは二段階評価になっていて、まずは展開(引数をそのままコードのテンプレートに挿入)して、最終的には「評価」されます。つまり、使いどころが異なるモノ同士がシステム的観点で統合されちゃった。これがマクロ学習の困難な面を露にしたのです。
加えると、「Schemeにはマクロが無い」と言うCLerの言い分も歴史的なちょっとしたアヤが原因だ、と言うのも分かると思います。マクロは展開を伴うので「いつ展開されるのか?」と言うのが重要なポイントで、CLでは「コンパイル時」と規程されている模様です。一方、Schemeはマクロ展開時がいつになるかR5RSに正確な規程が存在しません。Scheme登場時には多くのLisp方言が今よりもバラバラに存在してたんで、「展開時」をどこにするのかまだ考える猶予があったのでしょう。かつ、その言い方を借りるとLispには「引数を評価しない」関数があって良いのです。Schemeが持ってるのはもっと緩やかな、むしろ古典的なNEXPR型やFEXPR型の関数定義だ、と言う言い方も出来るのです。


多変数のSETQ


変数に値を代入するSETQという関数があります。プログラム中では、変数値の変更は1個だけでなく複数の変数について行いたい場合がよくあります。このようなときに(SETQ … ) (SETQ … ) … (SETQ … )と書くのは面倒なので、(MSETQ x1 v1 x2 v2 … xn vn)としたいと思います。ここでxi、viはi番目の変数名とその代入値を指します。
このMSETQの定義を与えてください。



同書にはFEXPR型関数定義の例として上のような問題を掲載しています。「関数定義」の問題として、ですよ。マクロじゃないんです。現代的なLispを扱ってると信じられない状況です(笑)。つまり、大昔のLispだと、この程度だとわざわざマクロを持ち込まなくても良かった、と言う事でしょう。
回答例は以下のようになっています。

(DF MSETQ (L)
(PROG (X V)
(COND [(GREATERP 2 (LENGTH L))
(ERROR "ARGUMENTS LESS THAN 2!")])
(LOOP () (SETQ X (CAR L)) ;MSETQが使えれば、
(SETQ V (CADR L)) ;(MSETQ X (CAR L) V (CADR L) L (CDDR L))
(SETQ L (CDDR L)) ;と書けたはず。
(SET X (EVAL V))
(COND [(NULL L) (RETURN (EVAL X))]
[(NULL (CDR L))
(ERROR "ODD NUMBER OF ARGS!")]
))))

Schemeの衛生的マクロで書くとこうですかね。

(define-syntax mset!
(syntax-rules ()
((_ x)
(error "arguments less than 2!"))
((_ x v)
(set! x v))
((_ x v y)
(error "odd number of args!"))
((_ x v y ...)
(begin (set! x v)
(mset! y ...)))))

他にもこんな例題が掲載されています。

構造化構文


いわゆる構造化プログラミング(Structured Programming)では、goto文(PROG内のGOなど)を廃して、構造化構文を推奨しています。LOOPもその一例ですが、ほかにWHILEREPEATと言う構造化構文がよく用いられます。
WHILE(WHILE <条件> DO <実行文1> … <実行文n>)と言う形式で、<条件>が成立している間は実行文を繰り返し実行します。
一方REPEAT(REPEAT <実行文1> … <実行文n> UNTIL <条件>)という形式で、<条件>が成立するまで、実行を繰返します。
このWHILEREPEATという二つの構造的繰返し実行関数を定義してください。


これもマクロ例題としては良くある問題なんですが、やっぱり注目してください。当時はこれらは関数として定義出来た、んです。
当時の解法だと次のような感じですね。

(DF WHILE (L)
(PROG (PRED EXEC)
(SETQ PRED (CAR L))
(SETQ EXEC (CDDR L)) ;DO のチェックは省きました。
(LOOP ()
(COND [(NULL? (EVAL PRED))
(RETURN 'END-OF-WHILE)])
(MAPC 'EVAL EXEC) ;MAPC を使って実行します。
)))

(DF REPEAT (L)
(PROG (PRED EXEC)
(SETQ L (REVERSE L)) ;逆転して処理しました。
(SETQ PRED (CAR L))
(SETQ EXEC (REVERSE (CDDR L)))
(LOOP ()
(MAPC 'EVAL EXEC)
(COND [(EVAL PRED) (RETURN 'END-OF-REPEAT)])
)))

これも今風にSchemeの衛生的マクロで書けば以下のようになりますかね。

(define-syntax while
(syntax-rules ()
((_ (pred exec ...)) ;do の省略
(do ()
((not pred) 'end-of-while)
(begin exec ...)))))

(define-syntax repeat
(syntax-rules ()
((_ (pred exec ...))
(do ()
(pred 'end-of-repeat)
(begin exec ...)))))

いずれにせよ、当時はこの範疇の「構文」でさえ、マクロで書くような事はなかったんです。もっとも、そのせいでコード中にevalを挿入して醜くなったりしてますが。
そして、マクロに「引数を評価しない」関数の役割が統合された為、マクロの「役割」が膨大になっちゃった、という副作用が出てきたのです。

0 件のコメント:

コメントを投稿