2010年5月16日日曜日

vallog: macro while

vallog: macro while


do マクロの使い方を覚えられないクラスタです。


なかなかこれが、Schemeでは再帰ばっか練習するんで、doは意識して練習しないとdo嫌いに成りかねません。
まあ、以前も言ったんですけど、スタイル的には実はdonamed-letの変種です。Common Lispの内部では再帰とは全く違う破壊的な計算を行ないますが、かと言って、スタイルだけに注目すれば、実はnamed-letとはそんなに違いがないのです。
僕の中では

  1. 横に広がるnamed-let

  2. 縦に伸びるdo


とか言ってました。意味分かんないっすね(笑)。まあ、変数束縛の位置が、って事なんですけれども。あと、doがフグみたいに見える、ってんでフグ構文とか呼んでたりしました(笑)。

doの一般形は次のようになっています。

(do (( <<変数1>> <<初期値1>> <<ステップ1>>)
...
( <<変数m>> <<初期値m>> <<ステップm>>))
(<<終了条件>> <<式>>...<<式>>)
<<式1>>
...
<<式n>>)


ね、フグみたいでしょ(笑)?



Schemeのnamed-letの一般形は次の通りです。

(let name ((<<変数1>> <<初期値1>>) ... (<<変数m>> <<初期値m>>))
(cond (<<終了条件>>
<<式>> ... <<式>>)
(else
(name <<ステップ1>> ... <<ステップm>>))))
view raw named-let.scm hosted with ❤ by GitHub


つまり、基本的には変数、初期値、ステップの配置が違うだけ、です。doだとまとめて記述する。named-letだとそうじゃない、って事ですね。

どっちがどっちより便利、って事は基本無いわけなんですけれども。

;; ファイルを読み込んで行数を表示する手続き(Scheme)
(define (count-lines filename)
(call-with-input-file filename
(lambda (p)
(let loop ((c (read-char p))
(count 0))
(cond ((eof-object? c) (close-input-port p) count)
(else (loop (read-char p) (if (char=? c #\newline)
(+ count 1)
count))))))))
(define (count-lines/do filename)
(call-with-input-file filename
(lambda (p)
(do ((c (read-char p) (read-char p))
(count 0 (if (char=? c #\newline)
(+ count 1)
count)))
((eof-object? c) (close-input-port p) count)))))


ただ、個人的には、ここで言う副作用とは、例えば入出力であるとか、そう言うケースですが、副作用が関わる式、特に出力を実行しなければならない場合、doの方がよりシンプルに書ける場合が多いとは思います。named-letだと、スッキリ決まらないケースが多くて、そう言う場合、doの方がシックリくる場合が多いような気がします。

;; ファイルを読み込んで表示して、行数を表示する手続き(Scheme)
(define (print-and-count-lines filename)
(call-with-input-file filename
(lambda (p)
(let loop ((c (read-char p))
(count 0))
(cond ((eof-object? c) (close-input-port p) count)
(else (display c) ; 基本 begin に頼って形式的には汚くなる
(loop (read-char p) (if (char=? c #\newline)
(+ count 1)
count))))))))
(define (print-and-count-lines/do filename)
(call-with-input-file filename
(lambda (p)
(do ((c (read-char p) (read-char p))
(count 0 (if (char=? c #\newline)
(+ count 1)
count)))
((eof-object? c) (close-input-port p) count)
(display c))))) ;ボディに出力命令がサッと置ける


上の例はあまり良くないんですが、と言うのも、named-letcondを使って暗黙のbeginで上手い具合にみっともなさを回避してるんですが、基本、named-letのシンプルさはぶち壊れてるんです(笑)。目立ちませんが(笑)。
反面、後者のdoヴァージョンは本体部に「計算以外の余計な作業」をまとめられます。これはシンプル過ぎる例なんですが、長く余計な作業がある、って事はままあるんです。そう言う時、さすがの再帰構文でもシンプルに書けなくなる。いや、構造はシンプルなんですが、コード自体は汚く見える場合があるんですよね。

いや、すまない。いい例が思いつかなかった(爆)。ただ、普段は再帰で構わないけど、いざとなったらdoの方がシンプルに書ける場合があるんだよ、って事です。はい。


注:いや、ホントに下手な例でゴメン。というのも、doに関して言っても、カウンター内で出来る事は全てやってしまうのがスタイル的には美しいんですが、それで悩むんだったら素直に再帰した方が良い、ってのが事実。要するに再帰での「引数内のカウンター処理だけじゃどうしようもない」部分が出てきた場合、doの出番だ、って言い方の方が正しいかも。


さて、valvallowさんの記事によると、どうやら「NILじゃない返り値が欲しい」との事。まあ、これは当然でしょうね。
ポール・グレアムが何でマクロで書いたwhileNIL以外の返り値を返すようにしなかったのか?想像するに、二つ程理由が考えられて、マクロの導入章(第7章なのに!)の辺りなんで、あんまややこしい例じゃなくってシンプルな例にしたかった事。あとは破壊的変更が前提のマクロなんで、返り値を返すとマズイ、って事があったんでしょうね。

グレアムのwhileは次のようにしてみれば面白い結果が出てきます。

CL-USER> (defmacro my-while (test &body body)
`(do ()
((not ,test))
,@body))
MY-WHILE
CL-USER> (let ((i 0))
(my-while (< i 10)
(print i)
(incf i)) ; (setf i (+ i 1)) と同じ
i) ; i を返してみる
0
1
2
3
4
5
6
7
8
9
10 ;何と i は 10 になってる!
CL-USER>


whileの指定はi < 10の筈なのに、何とiは10になっています。つまり、返り値になるのはこのiになるのは自明でしょう。これはマズいです(笑)。恐らく返り値は9であって欲しい、ってのが皆願う事でしょうから。
だから破壊的変更が前提なら、返り値をNILでもしとけば良い。堅実な判断ですよね。

さて、それでは返り値を、Common Lispらしく最後に実行された値を返すように改造していきましょう。まずは、LOLでお馴染みでしょうが、関数lastbutlastってのを使ってみます。

CL-USER> (last '(0 1 2 3 4 5))
(5)
CL-USER> (butlast '(0 1 2 3 4 5))
(0 1 2 3 4)
CL-USER>


関数lastはリストの最後の値のリストを返し、関数butlastはリストの最後の要素を除いたリストを返します。今何でこれを使おうと思ったのか、と言う理由は、繰り返しが何回実行されようと、必要なのは計算の最後の作業結果だという事だからです。
例えば、上のvalvallowさんの例だと、欲しいのは式をして実行された(print i)(incf i)全体じゃなくって、あくまで(incf i)だけ、なんです。言い換えるとbodyの最後の計算結果さえ分かっていれば良い、と言う事です。
すなわち、bodyを二つに分けちゃう。最後と、それ以外、です。

このアイディアで雛形は一応次の通りになります。

(defmacro my-while% (test &body body)
;; var の初期値は nil で、(last body) は do のボディの式が評価されてから実行される
`(do ((var nil ,@(last body)))
((not, test) var)
,@(butlast body))) ; body の最後尾以外の評価がボディの仕事
view raw my-while%.lisp hosted with ❤ by GitHub


では実行してみますか。

CL-USER> (let ((i 0))
(my-while% (< i 10)
(print i)
(incf i)))
0
1
2
3
4
5
6
7
8
9
10 ; あれ!?やっぱり10になっている!
CL-USER>


やっぱりダメだ(笑)!bodyを二つに分ける、ってアイディアは秀悦だとは思うんですが、返り値が10になってます。こりゃイカン。
この理由は、終了条件が調べられる前にvarが更新されちゃうから、なんです。つまり、テスト式に行く前にiが10になってしまう。ここを直さないとどーにもならん、わけです。
さて、ここでやっつけ仕事のハック。嫌でもvarが更新されちゃうんだったら、その前の状態を保存しとけばエエんちゃうの?ってのがアイディア。つまり、変数を二つ用意しちゃうんだ!!!一つは今まで通り(last body)の更新用。もうひとつは、前回のそれの保存用だ!これでどうだ。

(defmacro my-while%% (test &body body)
;; var0 の初期値は nil で、(last body) は do のボディの式が評価されてから実行される
`(do ((var0 nil ,@(last body))
;; var1 の初期値も nil で、更新値は「前回の」var0 の値
(var1 nil var0))
((not ,test) var1) ; 返り値は var1 になる
,@(butlast body)))
view raw my-while%%.lisp hosted with ❤ by GitHub


では実行してみましょう。

CL-USER> (let ((i 0))
(my-while%% (< i 10)
(print i)
(incf i)))
0
1
2
3
4
5
6
7
8
9
9 ; やった!返り値が 9 になった!
CL-USER>


上手く行きましたね。大成功!!!これで終わり……とはいかないんですよ、残念ながら(笑)。
実はこのマクロは次のような問題があるんです。

CL-USER> (let ((var0 0))
(my-while%% (< var0 10)
(print var0)
(incf var0)))
; in: LAMBDA NIL
; (LET ((VAR0 0))
; (MY-WHILE%% (< VAR0 10) (PRINT VAR0) (INCF VAR0)))
;
; caught STYLE-WARNING:
; The variable VAR0 is defined but never used.
;
; compilation unit finished
; caught 1 STYLE-WARNING condition
; Evaluation aborted.
CL-USER> (let ((var1 0))
(my-while%% (< var1 10)
(print var1)
(incf var1)))
; in: LAMBDA NIL
; (LET ((VAR1 0))
; (MY-WHILE%% (< VAR1 10) (PRINT VAR1) (INCF VAR1)))
;
; caught STYLE-WARNING:
; The variable VAR1 is defined but never used.
;
; compilation unit finished
; caught 1 STYLE-WARNING condition
; Evaluation aborted.
CL-USER>


これが変数衝突ですね。つまり、my-while%%マクロの内部でvar0var1って変数名を使ってるわけなんですけど、これが外部から与えられると途端にぶつかってしまってどうにもこうにも行かなくなる。
つまり、これを避けるのがgensymです。だから、こう書かないとなりません。

(defmacro my-while (test &body body)
;; テンプレートの外部で var0、var1 で gensym を束縛する
(let ((var0 (gensym)) (var1 (gensym)))
`(do ((,var0 nil ,@(last body))
(,var1 nil ,var0))
((not ,test) ,var1)
,@(butlast body))))
view raw my-while.lisp hosted with ❤ by GitHub


これで安心してどんな変数名をmy-whileマクロ内に持たせても大丈夫です。

CL-USER> (let ((i 0))
(my-while (< i 10)
(print i)
(incf i)))
0
1
2
3
4
5
6
7
8
9
9 ; 返り値は9!!!
CL-USER> (let ((var0 0))
(my-while (< var0 10)
(print var0)
(incf var0)))
0
1
2
3
4
5
6
7
8
9
9 ; 多い日も安心!
CL-USER> (let ((var1 0))
(my-while (< var1 10)
(print var1)
(incf var1)))
0
1
2
3
4
5
6
7
8
9
9 ; 横モレしない!
CL-USER>


gensymをいつ使うか?基本的にはいつでもです。意図してアナフォリックマクロを書く時以外はバンバン使って構わないと思います。あって困るもんじゃない。
基本的には、

  • defmacroで引数として与えられた変数以外で、テンプレート内に「いきなり」現れる名前全部に対してgensymを使って構わない


と言うのが原則です。複雑なマクロでテンプレート内にバンバン新しい変数が現れる場合はgensymだらけになりますが、それで良い、のです。繰り返しますが、gensymはあって困るもんじゃない。むしろ無けりゃ困るんです。

まあ、もっともみっともなくなる可能性もありますが、その回避の為にOn Lispではwith-gensymsというコードが紹介されています。また、上のmy-whileはLOL流にdefmacro!を用いれば次のように記述されますね。

(defmacro! my-while! (test &body body)
`(do ((,g!var0 nil ,@(last body))
(,g!var1 nil ,g!var0))
((not ,test) ,g!var1)
,@(butlast body)))
view raw my-while!.lisp hosted with ❤ by GitHub


ちなみに、再帰版は次のようになるでしょう。

(defmacro my-while2 (test &body body)
(let ((self (gensym)) (exp (gensym)) (acc (gensym)))
`(labels
((,self (,exp ,acc)
(if (not, test)
,acc
(,self (progn ,@body) ,exp))))
(,self (progn ,@body) nil))))
(defmacro! my-while2! (test &body body)
`(labels
((,g!self (,g!exp ,g!acc)
(if (not ,test)
,g!acc
(,g!self (progn ,@body) ,g!exp))))
(,g!self (progn ,@body) nil)))

0 件のコメント:

コメントを投稿