前回、Scheme/Racketに於いては、マクロはほぼsyntax-rulesで間に合うと言う話を書いた。
より正確に言うと、syntax-rulesで書けないようなマクロは挑戦してはならない、的な勢いで書いていた。
ところが、Lispマクロに付いての、ポール・グレアムのOn Lispではアナフォリックマクロ、と言われるモノが紹介されている。これがSchemeのマクロ、syntax-rulesで書けない、となると悔しさ爆発、って人が続出するんだな(笑)。
アナフォリックマクロとは何だろうか。
実のことを言うとこれは別にポール・グレアムの「発明」ってワケじゃないらしい。古典的なLispコミュニティの中では粛々と受け継がれてきた「伝統の技法」らしい。
それはLispマクロの「危険性」を上手く利用したマクロの1つだ。
例えば次のような連想リストによるデータベースがあったとしよう。
(define *database* '(("星田オステオパシー" . "https://blog.goo.ne.jp/hosidaosuteo")
("シン・MOEヨ日本ト世界(仮称)" . "https://blog.goo.ne.jp/blue_031")
("ハリソン君の素晴らしいブログZ" . "https://blog.goo.ne.jp/harrison2018")
("新しいアカウントで始めました。" . "https://blog.goo.ne.jp/isamrx72")))
ここで定義されたサイトが存在する時に、そのアドレスの文字列を取り出す関数を書く、とする。
この場合、直球勝負で書くと次のようになる。
(define (foo s)
(if (assoc s *database*)
(cdr (assoc s *database*))
"404 Not Found"))
問題はassocを2回書いていて、ちと無駄に見える辺りだ。そして実際に実行しても、条件部で一回計算され、そしてthen節でも一回計算される。
それを避ける際に、通常はassocでの計算結果をletにでも束縛する。
(define (foo s)
(let ((result (assoc s *database*)))
(if result (cdr result) "404 Not Found")))
ポール・グレアムが紹介したaifと言うマクロ(アナフォリックifの意)は条件節で計算した結果をそのままthen節から参照可能なifだ。
(define (foo s)
(aif (assoc s *database*) (cdr it) "404 Not Found"))
これでletでの束縛分の「1行」が省略出来る。
仕組み自体は実は簡単で、aifと言うマクロが展開されると上で見せたletを使ったヴァージョンでの構造になる。そしてシンボル"it"にはletでの計算結果が束縛されている。
ところがこれがsyntax-rulesでは書けないのだ。
このaifの話を論じる前に、ピーター・ノーヴィグ先生のお言葉を紹介しておこう。
(aifは)便利な機能だが、結果的には裏目に出て、頻繁に匙を投げることがあった。理由はこういう事だ。例えばコードを以下のように書きたいことがある。(if (total-score x)(print (/ that number-of-trials))(error "No scores"))その後で少し変更を加えたとする。(if (total-score x)(if *print-scores* (print (/ that number-of-trials)))(error "No scores"))問題は、変数thatが(total-score x)ではなくて*print-scores*を参照するように変わってしまうことだ。私のマクロは参照透過性に違反している。一般的には、それがマクロの醍醐味であり、マクロが便利な理由でもある。しかし、このケースでは、参照透過性の違反が混乱を引き起こしている。
これに対して、ポール・グレアムの方も反論している。
Norvigは,ifを次のように再定義したら便利だろうと述べたが,(defmacro if (test then &optional else)
`(let ((that ,test))
(if that ,then ,else)))参照透明性を侵すという観点からこのマクロを却下している.しかし問題は組み込みオペレータを再定義したことに因るもので,アナフォラを使ったことに因るのではない. 上の定義のb)節は,式が「任意のコンテキスト内」で必ず同じ値を返すことを要求している. 次のletの中では,シンボルthatが新しい変数を表しても問題はない.(let ((that 'which))
...)letは当然新しいコンテキストを生成するはずのものだからだ.上のマクロの問題はifを再定義していることで, そうすると新しいコンテキストが作られることにはならない. アナフォリックマクロに別の名前を与えれば問題は消失する. (それ以前に,CLtL2にあるようにifの再定義は違法だ.) そのようなマクロがaifの定義の一部であることにより, そのマクロが生成したコンテキスト内ではitが新しい変数である限り, そのマクロは参照透明性を侵さない.さてaifは確かに別の慣習に違反するが,それは参照透明性とは関係ない. その慣習とは,新しく生成された変数は ソースコード内で何らかの形で分かるようになっているべきというものだ. 上のletはthatが新しい変数を参照するようになることをはっきり示している. aif内でのitの束縛がはっきり見えないという議論もあるかも知れない. しかしこれは余り説得力のある意見ではない. aifは1個の変数を生成するだけだし, しかもその変数を生成することだけがaifを使う理由だからだ.
ちなみに、ポール・グレアムは1つ勘違いしてて、ノーヴィック先生は「既存のifを再定義した」わけではない。ノーヴィック先生は「ifが標準に含まれる前のLispで」自らifを作ったんだ(※1)。
つまり、これを試してみたのはCommon Lispが成立する以前の話で、その時に「自作ifをアナフォラとしてマクロで定義した時」の反省を書いてるんだ。
さて、どっちの意見が正しいのか、と言うのは「分からん」(笑)。
が、取り敢えずSchemeでのアナフォリックifの実装に付いて考えていこうと思う。
syntax-rulesに慣れると、このaifは別に簡単に書けるんじゃねーの?と思ってしまう。
(define-syntax aif
(syntax-rules ()
((_ test consequent alternate)
(let ((it test))
(if it consequent alternate)))))
ロジックも簡単だし、これでエエんちゃうの?と。
ところがこれじゃあ上手く動かないんだな。
> (define (foo s)
(aif (assoc s *database*) (cdr it) "404 Not Found"))
> (foo "星田オステオパシー")
. . it: undefined;
cannot reference an identifier before its definition
>
うん、どっかで見たエラーだ。これで見たエラーと全く同じだ。
simple-loopの場合、脱出の為に仕込んだ筈のシンボル、returnがいざ、マクロ使用段階で使用しようとすると、「全く別のシンボル」に置き換えられてしまってて使い物にならなくなってた、と言うオチだった。
このaifの失敗版も全く同じ原因でエラーを起こしてる。ここでマクロ展開形に使用されたシンボルitは、内部的にはletで束縛されたitも、条件節にハメ込まれたitも全く同じモノを指している。
しかしながら、遺ってる情報はそれだけ、であり、itと言う「名称」は全く別の、何か我々の預かり知らぬ名称に「差し替えられて」いて、なおかつそれが何になったのか、を知る術が全くない。
このように、syntax-rulesを使う際に、マクロ展開形に於いては自由にシンボルを使って構わないんだけど、一旦そのマクロを使う段階に於いては外部からはそのシンボルを指定出来ないんだ。
syntax-rulesが形作るマクロも「健全なマクロ」と呼ばれる。それはそこで新しくプログラマが導入したシンボルは「すべからく」全く別の、知りようがないシンボルに置き換えられる。だからこそ「健全なマクロ」と呼ばれるわけだ。
何故にそれが「健全」なんだろう。
と言うのも、マクロを使うのは当然プログラミングで使うわけだが、偶然、マクロで使用したシンボルと「その外側にある」シンボルがバッティングして、思いも拠らない挙動をする事を避けられれば「健全」なんだ。
例えば、On Lispでは次のような「エラーを起こす可能性がある」Common Lispでのforマクロの例を出している。
(defmacro for ((var start stop) &body body) ; 誤り
`(do ((,var ,start (1+ ,var))
(limit ,stop))
((> ,var limit))
,@body))
これは90%くらいは正しく動くマクロだ。ただし、forマクロを呼び出す際に、forマクロ内で使ってるlimitと同名シンボルを使うとエラーを起こしてしまう。
(for (limit 1 5)
(princ limit))
;;; エラー例; in: FOR (LIMIT 1 5)
; (LIMIT 5)
;
; caught ERROR:
; The variable LIMIT occurs more than once in the LET.
;
; compilation unit finished
; caught 1 ERROR condition
つまり、Common Lispのマクロの場合、マクロ内で使われてる「変数名」とマクロの外から与えられる「変数名」を区別する手段がないんだ。これがCommon Lispのマクロが「健全ではない」と言われる原因になっている。
実際はgensymと言う「自動シンボル生成器」を使ってこのテの問題を回避するようにするんだけど、Common Lispのマクロだと、初心者段階ではとにかく「何でもかんでもgensymする」しか思いつかず、結構大変な思いをすることになる。
言っちゃえば、Schemeのsyntax-rulesは「自動gensym付きの」マクロを簡単に書けるようになってて、Common Lispのように「同名シンボルを与え」てもバグることを心配する必要がない。
上のforなんかも、Schemeではそのまま移植しても全然問題なく動くのを目にするだろう。
(define-syntax for ;;; ここが defmacro
(syntax-rules ()
((_ (var start stop) body ...) ;;; for ((var start stop) &body body)
(do ((var start (add1 var)) ;;; `(do ((,var ,start (1+ ,var))
(limit stop)) ;;; (limit ,stop))
((> var limit)) ;;; ((> ,var limit))
body ...)))) ;;; ,@body))
;;; Scheme だとマクロ定義内での limit と外部から与えられた limit は;;; 別物なんで問題なく動く> (for (limit 1 5)
(display limit))
12345>
つまり、ANSI Common Lispの場合、「健全ではない為」、マクロ内で設定したitを外部からも与えても認識する、って「欠点」を逆手に取って、アナフォリックマクロなんかが書けるようになってる、ってこったな。
そしてSchemeのsyntax-rulesの場合、「自動gensym機能付き」の為、シンボルによって自動gensymを回避する手段がない。
そんなわけで、syntax-rulesではアナフォリックマクロが書けないわけ。
ここでsyntax-rulesを使う際のヒントを纏めておこう。
- syntax-rulesでのマクロ展開部では自分の好きなシンボルを導入して良いが、そのマクロ内だけで完結するようにしなければならない。
と言うわけでまたもやsyntax-caseに戻る。
ぶっちゃけた話、実用的には、何のためにsyntax-caseがあるのか、と言うと、殆どこのアナフォリックマクロを書く為にある、って言って過言ではない。
いや、言い過ぎか(笑)?どっちにせよ、「マクロ内で使ってるシンボルを外部から与えても認識出来るマクロを書く為にある」ってぇのがその殆どの用途、となる。simple-loopで見せたsyntax-caseもその例になってる。
とは言っても、実はポール・グレアムのOn Lispに挙げられているアナフォリックマクロ群をSchemeに移植するのはなかなか骨が折れるんだ。
もう一度syntax-caseを使った際の記述テンプレートを示そう。
(define-syntax マクロ名(lambda (変数)(syntax-case 変数 ()((マクロ記述形式)#'(マクロ展開形))... )))
syntax-rulesに比べるとちと記述要素が多い。それこそdefine-syntax-caseなんつー定義式を作りたくなる程だ。
そして重要なのは、syntax-caseも言わば「自動gensym付きの」マクロだ。
従って、「このシンボルは自動gensymから外してくれ」と言う場合、自分で指定しなければならない。これがsyntax-rulesでは出来ない事で、それをwith-syntax構文と言う。
(with-syntax ((指定シンボル名 (datum->syntax 有効マクロ名 シンボル)) ...)#'本体)
例えば、aifマクロ内でのitを、外部から与えた際にも使えるようにしたい場合、
(with-syntax ((it (datum->syntax #'aif 'it)))#'本体)
と書くわけだ。
これでaifを書くと、
(define-syntax aif
(lambda (expr)
(syntax-case expr ()
((aif test-form then-form else-form)
(with-syntax ((it (datum->syntax #'aif 'it)))
#'(let ((it test-form))
(if it then-form else-form)))))))
となる。
syntax-rulesで想定してた形式よりは随分と長くなってるし、ポール・グレアムの原作に比べてもそうだ。
しかし、やっとこれでScheme版aifは上手く動く事となる。
> (define (foo s)
(aif (assoc s *database*) (cdr it) "404 Not Found"))
> (foo "星田オステオパシー")
"https://blog.goo.ne.jp/hosidaosuteo"
>
練習代わりに書いてみると、syntax-caseでawhenとawhileくらいは上手く書ける事が分かる。
(define-syntax awhen
(lambda (expr)
(syntax-case expr ()
((awhen test-form body ...)
(with-syntax ((it (datum->syntax #'awhen 'it)))
#'(let ((it test-form))
(when it body ...)))))))
(define-syntax awhile
(lambda (expr)
(syntax-case expr ()
((awhile e body ...)
(with-syntax ((it (datum->syntax #'awhile 'it)))
#'(do ((it e e))
((or (not it) (null? it)))
body ...))))))
;;; 実行例> (awhen (assoc "星田オステオパシー" *database*)
(display (cdr it))
(newline)
(string-replace (cdr it) "https" "http"))
https://blog.goo.ne.jp/hosidaosuteo
"http://blog.goo.ne.jp/hosidaosuteo"
> (let ((lst *database*))
(awhile lst
(display (caar it))
(newline)
(set! lst (cdr it))))
星田オステオパシー
シン・MOEヨ日本ト世界(仮称)
ハリソン君の素晴らしいブログZ
新しいアカウントで始めました。
>
ところがその次から上手く行かなくなってくる。
アナフォリックマクロ、aandは今定義したawhenを利用して作るマクロだ。
OnLispに従って書いてみようとすると、多分最初のヴァージョンは次のようになるんじゃないか。
;;; これが何故か上手く動かない
(define-syntax aand
(syntax-rules ()
((aand) #t)
((aand arg) arg)
((aand arg0 arg1 ...)
(awhen arg0 (aand arg1 ...)))))
そもそもOnLispではaandはawhenを用いて定義してる。と言うこたぁ既にawhenを定義してて、それ以上複雑なマクロを設計する必要はないんじゃないか、syntax-rulesで充分じゃないかって思うわけだ。
ところがどっこい、次のようなエラーが出る。
> (aand (assoc "星田オステオパシー" *database*) (cdr it) (string-replace it "https" "http"))
. . it: undefined;
cannot reference an identifier before its definition
>
またもやシンボル"it"が認識されない。syntax-rulesにawhenを埋め込んだ時点で使用したwith-syntaxの効果がキャンセルされた?
ため息をつきつつ、しょーがないんで、aandをsyntax-caseとwith-syntaxを用いたスタイルで書き直してみる。
;;; 第2版: やっぱり上手く動かない(define-syntax aand
(lambda (expr)
(syntax-case expr ()
((aand) #'#t)
((aand arg) #'arg)
((aand arg0 arg1 ...)
(with-syntax ((it (datum->syntax #'aand 'it)))
#'(awhen arg0 (aand arg1 ...)))))))
1つ、syntax-caseの構文上の注意をしておくと、マクロ展開形は全部#'ではじめないとならない。
従って、syntax-rulesだと展開形の返り値として#t、arg、だったものがsyntax-caseの場合#'#t、#'argとしなければなんない。ちと異様に見えるだろう。
まぁ、どっちにせよ、これでも上手く動かないんだけど。
> (aand (assoc "星田オステオパシー" *database*) (cdr it) (string-replace it "https" "http"))
. . it: undefined;
cannot reference an identifier before its definition
>
「なんでやねん?」ってこの時点で相当混乱する筈だ。with-syntaxでitから「自動gensym」効果は外してる筈である。なのに認識されない。
これね、何が原因か、っつーと、このaandマクロ上でawhenがそもそも構文として認識されてないのが原因らしいんだよな。
考えてみると、相当ヘンな状況なんだよ。大域状態で自作マクロを定義してる状態なのにそのマクロがマクロとして認識されてない。syntax-caseはそういうおかしな挙動込み、なんだよな。
んで、よくよくThe Scheme Programming Language, 4th Edition見てみるとだ。Exampleで挙げられてる例とか見ても自作マクロを使ってマクロを作る例とか1つもねぇんだわ。
だからリファレンスだけ、じゃ不十分だっつーの。チュートリアルがねぇとどうしようもねぇだろ(怒
つまり、ここで暫定的にやんなきゃならん事は、
- awhenと言うシンボルはaand内で構文だ、と認識させなければならない
- そしてそのawhen内で使うシンボルitを自動gensymから外さないとならない
もうこうなってくると、OnLispのオリジナルのaandに比べるとsyntax-caseによるマクロは「かなり手間だ」って事が分かってくる。
いずれにせよ、with-syntaxでawhenを構文だ、と指定して、なおかつそのawhen内でitの自動gensymを外す指定を下す。
;;; これでやっと動く(define-syntax aand
(lambda (expr)
(syntax-case expr ()
((aand) #'#t)
((aand arg) #'arg)
((aand arg0 arg1 ...)
(with-syntax ((it (datum->syntax #'awhen 'it))
(awhen (datum->syntax #'aand 'awhen)))
#'(awhen arg0 (aand arg1 ...)))))))
;;; 実行例> (aand (assoc "星田オステオパシー" *database*) (cdr it) (string-replace it "https" "http"))
"http://blog.goo.ne.jp/hosidaosuteo"
>
この時点で、相当syntax-caseは厄介だ、って事が分かってくる。大域的に定義したマクロをマクロとして認識しねぇ、とか一体どこをどうひっくり返せば書いてんだ、と怒髪天を衝く思いである(笑)。いや、マジで。
このaandに比べるとacondもalambdaも実装はラクなモンだ。
(define-syntax acond
(lambda (expr)
(syntax-case expr (else)
((acond) #'#f)
((acond (else e ...)) #'(begin e ...))
((acond (test0 e0 ...) (test1 e1 ...) ...)
(with-syntax ((it (datum->syntax #'acond 'it)))
#'(let ((sym test0))
(if sym
(let ((it sym)) e0 ...)
(acond (test1 e1 ...) ...))))))))
(define-syntax alambda
(lambda (expr)
(syntax-case expr ()
((alambda parms body ...)
(with-syntax ((self (datum->syntax #'alambda 'self)))
#'(letrec ((self (lambda parms body ...)))
self))))))
;;; acondの実行例> (acond ((assoc "星田オステオパシー" *database*) (display (cdr it)))
(else "404 Not Found"))
https://blog.goo.ne.jp/hosidaosuteo
>
アナフォリックマクロとしてはalambdaが一番面白く役立つかもしんない。
> ((alambda (x (acc 1)) (if (zero? x) acc (self (sub1 x) (* x acc)))) 10)
3628800
>
無名関数のまま再帰が書ける、たぁなかなか美味しい機能だ。
しかし喜んでいられるのもここまで、である。
残り1つ、ablockだけはどうあがいても書けなかった。
;;; 失敗(define-syntax ablock
(lambda (expr)
(syntax-case expr ()
((ablock tag)
(with-syntax ((alambda (datum->syntax #'ablock 'alambda)))
#'(call/cc (lambda (tag) ((alambda () #f))))))
((ablock tag arg)
(with-syntax ((alambda (datum->syntax #'ablock 'alambda)))
#'(call/cc (lambda (tag) ((alambda (x) x) arg)))))
((ablock tag arg0 arg1 ...)
(with-syntax (((x0 x1 ...) (generate-temporaries #'(arg0 arg1 ...)))
(alambda (datum->syntax #'ablock 'alambda))
(it (datum->syntax #'ablock 'it))
(self (datum->syntax #'alambda 'self)))
#'(call/cc (lambda (tag)
((alambda (x0 x1 ...)
(let ((it x0))
(self x1 ...))) arg0 arg1 ...))))))))
う〜ん、構文的にはこれでイイ筈なんだけど、どうあがいても「it」が認識されない。
そもそもalambdaにはitと言うアナフォラが存在しない。その存在しないアナフォラを指定する事が「不可能」だと言うのが詰んでる原因じゃないか、と踏んでるんだが・・・・・・。
一応、ネット検索して調べてはみたんだが、分かった範疇では誰もこれに挑戦してない、って事だ(笑)。
実際、Racketには有志によるAnaphoricモジュールなるものが提供されてるんだが、どういうわけだか残念なんだがablockは実装されていない。多分syntax-caseで書くのさえ無理なんだろ(笑)。
と言うわけでsyntax-caseはここに来て敗北、だ。
ここまでで分かるのはsyntax-caseは複雑性を増す。かつ完璧じゃないんだ。
少なくともリファレンスの類で調べられる範疇では、やっぱそこまで自由度は高くない。っつーか「健全性」と「自由度」がある種トレードオフの関係になってる、ってのが分かった事になる。
そしてsyntax-caseは言っちゃアレだがやっぱ実用段階と言うよか実験段階っぽいんだよなぁ。
なお、ここまで話を展開しといてアレなんだけど、元々、Schemeにはアナフォリックifは必要ないんだ。述語の計算結果を知りたければcondを使えばいい。
> (cond ((assoc "星田オステオパシー" *database*) => (lambda (x) (string-replace (cdr x) "https" "http")))
(else "404 Not Found"))
"http://blog.goo.ne.jp/hosidaosuteo"
>
Schemeのcondやcaseは => と言うキーワードを使って条件節の計算結果を参照出来る。そして => を使った場合、実行節は一引数の関数を取り、その関数が条件節の計算結果に適用される。
何のことはない、SchemeのcondやcaseはANSI Common Lispのそれらより高機能で、それがないが故にANSI Common Lispではアナフォリックifを作成しないとならなかったんだ。
実は、On LispにもPAIP(実用Common Lisp)にもこの、Schemeのcondやcaseについてチラッと書かれてるが、意外にこの機能については知られてない模様だ。
Scheme の処理系によっては cond の節の中でテスト式の返した値を使う方法を提供している.------On Lisp
;;; syntax-rules と識別子を用いた、より Scheme らしい aif(define-syntax aif
(syntax-rules (=>)
((_ test consequent alternate)
(if test consequent alternate))
((_ test => expression alternate)
(cond (test => expression)
(else alternate)))))
;;; 実行例> (aif (assoc "星田オステオパシー" *database*)
=> (lambda (x)
(string-replace (cdr x) "https" "http"))
"404 Not Found")
"http://blog.goo.ne.jp/hosidaosuteo"
>
と言うわけで、syntax-caseの冒険を試みたものの、何か釈然とせず、消化不良でここで唐突に終了するもの、とする。
※1: 現在の多くのLisp処理系やその仕様では、ifが根源的な構文で、これを利用してcondやcaseをマクロとして実装する、と言うケースが多いが、往年の古いLispでは逆に、条件分岐の根底はcondであって、そもそもifを持たなかった。