「7章薄いなぁ。サクッと終わらせるか」
そう思ったあの日の俺よ。甘い、甘すぎるぜ。バナナコーラより甘いぜ。
飲んだことはないのですが、要するに章末の演習問題に嵌りまくりましたよ、ということです。難しかった。特にバッククォート記法でこんがらかりまくりました。こんがらかりまくるという日本語もこんがらがるこんがらがり具合。
まず前回、「Gaucheには最初からwhen式があるから」と跳ばした my-when 式ですが、演習問題で使うので定義してみます。
(define-macro my-when
(lambda (jouken . form)
(list 'if jouken
(cons 'begin form)
)
)
)
TUTSchemeではマクロを定義するラムダ式の引数は、マクロ名を含むリスト全体だったので、「Scheme入門」では condition と body に (cadr form) (cddr form) をそれぞれ let でバインドして操作していますが、Gaucheではdefine-macroの定義の引数はマクロの引数なので、jouken と form (ただしformは残余パラメータなので、最初からリスト化されている)の2つの引数をそのまま使って操作しています。
では演習問題たち
[7.2.1]
とりあえずGaucheにnum-filterを書き直した例。
(define-macro num-filter
(lambda (x)
(list 'if
(list 'number? x)
x
0
)
)
)
あとは「入門」通りにすると同じ結果になります。ただしマクロ展開は、
(macroexpand '(num-filter x))
な感じでシクヨロ。(macroexpandとmacroexpand-1の区別がついてない)
[7.2.2]
(define-macro push
(lambda (x form)
(list 'set! x
(list 'cons form x)
)
)
)
[7.2.3]
(define-macro my-when2
(lambda (jouken . form)
(list 'if jouken
(if (null? (cdr form)) ;もしmy-when2式の本体が1つならば
(car form) ;こう!
(cons 'begin form) ;複数ならばこうだ!
)
)
)
)
「入門」ではwhen式本体が1つかどうかを、条件より先にif文で判定していますが、これは条件を先に判定しています。
[7.2.4]
(if a (if b (if c (if...#f) #f) #f) #f)
な式を作るマクロを作れ、ということですが、解答の解凍(解読)がやたら難しかったので、素直にそちらの理解に挑戦。
まずは局所関数を補助関数に書き換えます。
(define (aux 1st rest)
(if (null? rest)
1st
(list 'if
1st
(aux (car rest) (cdr rest))
'#f ;これ必要
)
)
)
何故7行目の
'#f
が必要かというと、「入門」のあとがきに拠れば、TUTSchemeでは真偽値の#fと()(空リスト)が異なるデータではないらしいからです。実際に
SC> (if #f #t)
()
と()が返ってきます。Gaucheでは
(if #f #t)
gosh> #<undef>
と、未定義値が返ってきます。
(が、しかし
(if () #t #f)
に対しては、両処理系とも
#t
が返ってくるんだよなぁ……TUTSchemeでは"()"は真なのか偽なのか。そもそも"()"はリストなのか? 興味深い問題ですが、興味深いので、「RnRSを読む(仮)」という別のタイトルで投稿したいと思っています。)
さてこのaux関数は引数に '1st と '(r e s t) の2つを渡すと
CALL aux+ 1st (r e s t)
CALL aux+ r (e s t)
CALL aux+ e (s t)
CALL aux+ s (t)
CALL aux+ t ()
RETN aux+ t
RETN aux+ (if s t #f)
RETN aux+ (if e (if s t #f) #f)
RETN aux+ (if r (if e (if ...) #f) #f)
RETN aux+ (if 1st (if r (if ...) #f) #f)
(if |1st| (if r (if e (if s t #f) #f) #f) #f)
と、初めの引数を最後の要素に、後の引数のリストを、後ろから順に真偽を判定する式を作ってくれる関数です。(最後の "t" が #f で"s"が #t なら、#f が返ってきます)大事なのは、これはあくまで「式を作る関数」なので、
「4行目の
(list 'if...
って、
(if...
と一緒じゃん」
と、とっ散らかって
(define (aux 1st rest)
(if (null? rest)
1st
(if 1st
(aux (car rest) (cdr rest))
'#f
)
)
)
とすると式が作られず、単に後ろの引数の最後の要素が取り出されるだけです。……ぅん、私やっちゃったょ。。。
で、マクロは
(define-macro my-and
(lambda form
(if (null? form)
#t
(aux (car form) (cdr form))
)
)
)
とGauche用に書き換えればオッケー。「補助関数なんてぬりぃことやってんじゃねーYO!」というワイルドなYOUには局所関数版。
(define-macro my-and
(lambda form
(letrec [[aux (lambda (1st rest)
(if (null? rest)
1st
(list 'if
1st
(aux (car rest) (cdr rest))
'#f
)
)
)
]]
(if (null? form)
#t
(aux (car form) (cdr form))
)
)
)
)
以前に記しましたように、TUTSchemeではマクロを定義するラムダ式の引数は、マクロ名を含むマクロ式リスト全体であるのに対し、Gaucheではマクロを定義するラムダ式の引数は、マクロの引数なので、Gaucheに書き直す際には、場合に応じて、引数を残余パラメータ(可変長引数)にすることに注意しましょう……え? 「訓練されたGaucheユーザーは伝統的なマクロなんぞ使わず syntax-rules で健全にやるんだよ」・・だと・・?
[7.3.1]は特にGaucheとで変わらないと思うのでとばします。いや、難しいんですけどね。特に
(cons 'x (cons (list 'y) z)
が
`(x (y) ,@z)
になるのは(演習問題は、後者を前者に書き直す形式)ちょっとこんがらがりますね。
[7.3.2]以下は、今まで定義したマクロ(num-filter, push, my-when, my-and)をバッククォート記法で書き直せというもの。ところで偶然に気付いたのですが、Gaucheでは伝統的なマクロの定義では、引数をdefineで定義するときと同じように
(define-macro (f x)
定義式)
)
で行けるみたいです。
・num-filter
(define-macro (num-filter x)
`(if (number? ,x) ,x 0)
)
・push
(define-macro (my-push x form)
`(set! ,x (cons ,form ,x))
)
さてここまでサラサラとできたのですが、次、my-whenで嵌りまくりました。teratailに質問も辞さず、ってくらい嵌りました……今でも検討中ですが。
とりあえずやってみた my-when、バッククォート版。
上手く行かないmy-when
(define-macro (my-when jouken . form)
`(if ,jouken
(if (null? (cdr ,form))
,@form
(cons 'begin ,form)
)
)
)
分かる方が見たら「アホか……アホかぁっ!」と言いたくなるのではないでしょうか。おうよ! ERRORくらいましたさ! なので「入門」の答えを参考にこれで水曜どうでしょう。
これも上手く行かない
(define-macro (my-when jouken . form)
`(if ,jouken
(if (null? (cdr ,form))
(car ,form)
(begin ,@form)
)
)
)
検証用に
(difine x -1)
(my-when (< x 0) (- x))
(my-when (< x 0) (display x) (newline) (- x))
をやってみるのですが
gosh> *** ERROR: invalid application: (1)
とか言われちゃって、そりゃ 1 は関数じゃないしかと言って (1) はクオータ付けなきゃcar/cdrしてくれないし、どうすりゃいいのさアンタぁ、ねえアンタぁあ……ひょっとして、コンマ(,)って、変数に付けるとは限らないの? じゃないの? ひょっとしてS式にもコンマいるんじゃないの?
困ったときのグーグル先生に尋ねるとこんなページ(PDFページなので注意)を見つけましたよ。そっか、コンマはS式そのものを評価してくれるのか。(よく読むと「入門」にもそのように書いてある)
というわけで書き直し。
(define-macro (my-when jouken . form)
`(if ,jouken
(if (null? ,(cdr form))
,(car form)
(begin ,@form)
)
)
)
これで
(my-when (< x 0) (- x))
は 1 が返ってくるようになりましたが、
(my-when (< x 0) (display x) (newline) (- x))
では
gosh>
*** ERROR: invalid application: (#<undef> 1)
Stack Trace:
_______________________________________
0 (newline)
At line 78 of "(stdin)"
と返されて何だ? 何が不満なんだ? 言ってみろよぉお! とエラーを読めない悲しさに叫ぶ有様。ともかく確かに (#<undef> 1) はcar/cdrされないような気がするので、コンマの位置をあっちこっちに変えて試行錯誤。結局
(define-macro (my-when jouken . form)
`(if ,jouken
(if ,(null? (cdr form)) ;コンマの位置、ここかよ!
,(car form)
(begin ,@form)
)
)
)
で、検証用の二つの式、ともに期待していたような答えを返してくれるようになりました。コードにも書いている通り、ホンマ、「ここかよ!」ってところにコンマの位置がありました。
しかしながらこのマクロを
(macroexpand '(my-when x y))
すると
gosh> (if x (if #t y (begin y)))
になるんですよね……いや確かにこれ、
(if x y)
と同義なわけですけど、「入門」の解答にあるように、まず引数の数を判別するやり方、すなわち次のコード
(define-macro (m-when jouken . form)
(if (null? (cdr form))
`(if ,jouken
,(car form)
)
`(if ,jouken
(begin ,@form)
)
)
)
なら
(macroexpand '(my-when x y))
で
gosh> (if x y)
って、きれいにマクロ展開されるんですよね……これ、アルゴリズム上どうしようもないのかなあ。でも if が三つも必要って何か冗長な気がするんですよね。(マサカリという名の救助は常に求めています)
さて最後
・my-and
これも補助関数 aux をバッククォート記法に書き直す際、コンマの位置で迷いまくりました。迷うって言うよりもはや、数打ちゃ当たるかもしれん的試行錯誤ですよ。その試行錯誤を一々書いてもしょうがないので上手く行った版を以下に。
(define (aux 1st rest)
(if (null? rest)
1st
`(if ,1st
,(aux (car rest) (cdr rest)) ;どうやらコンマの位置はここらしい
#f
)
)
)
コードにも書いてますが、コンマを打つのは、まさかのこんな場所。まさかの。まさかの関数の前。ほあ。
マクロ定義は特にバッククォート記法は必要ないので、同じでいいです。多分。
補助関数版は、以下。
(define-macro (my-and . form)
(letrec [[aux (lambda (1st rest)
(if (null? rest)
1st
`(if ,1st
,(aux (car rest) (cdr rest))
'#f
)
)
)
]]
(if (null? form)
#t
(aux (car form) (cdr form))
)
)
)
こうやってバッククォート記法に振り回された経験知から参考までに述べますと
・コンマは変数(引数)そのものに付くとは限らない
・変数が与えられ、その返り値が出されるS式に付けると上手く行きそう
でしょうか。
マサカリ、そしてバナナコーラを実際に飲んだことのある方の感想、待っております。