do マクロの使い方を覚えられないクラスタです。
なかなかこれが、Schemeでは再帰ばっか練習するんで、
do
は意識して練習しないとdo
嫌いに成りかねません。まあ、以前も言ったんですけど、スタイル的には実は
do
はnamed-let
の変種です。Common Lispの内部では再帰とは全く違う破壊的な計算を行ないますが、かと言って、スタイルだけに注目すれば、実はnamed-let
とはそんなに違いがないのです。僕の中では
- 横に広がる
named-let
- 縦に伸びる
do
とか言ってました。意味分かんないっすね(笑)。まあ、変数束縛の位置が、って事なんですけれども。あと、
do
がフグみたいに見える、ってんでフグ構文とか呼んでたりしました(笑)。do
の一般形は次のようになっています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(do (( <<変数1>> <<初期値1>> <<ステップ1>>) | |
... | |
( <<変数m>> <<初期値m>> <<ステップm>>)) | |
(<<終了条件>> <<式>>...<<式>>) | |
<<式1>> | |
... | |
<<式n>>) |
ね、フグみたいでしょ(笑)?

Schemeの
named-let
の一般形は次の通りです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(let name ((<<変数1>> <<初期値1>>) ... (<<変数m>> <<初期値m>>)) | |
(cond (<<終了条件>> | |
<<式>> ... <<式>>) | |
(else | |
(name <<ステップ1>> ... <<ステップm>>)))) |
つまり、基本的には変数、初期値、ステップの配置が違うだけ、です。
do
だとまとめて記述する。named-let
だとそうじゃない、って事ですね。どっちがどっちより便利、って事は基本無いわけなんですけれども。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; ファイルを読み込んで行数を表示する手続き(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
の方がシックリくる場合が多いような気がします。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; ファイルを読み込んで表示して、行数を表示する手続き(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-let
はcond
を使って暗黙のbegin
で上手い具合にみっともなさを回避してるんですが、基本、named-let
のシンプルさはぶち壊れてるんです(笑)。目立ちませんが(笑)。反面、後者の
do
ヴァージョンは本体部に「計算以外の余計な作業」をまとめられます。これはシンプル過ぎる例なんですが、長く余計な作業がある、って事はままあるんです。そう言う時、さすがの再帰構文でもシンプルに書けなくなる。いや、構造はシンプルなんですが、コード自体は汚く見える場合があるんですよね。いや、すまない。いい例が思いつかなかった(爆)。ただ、普段は再帰で構わないけど、いざとなったら
do
の方がシンプルに書ける場合があるんだよ、って事です。はい。
注:いや、ホントに下手な例でゴメン。というのも、do
に関して言っても、カウンター内で出来る事は全てやってしまうのがスタイル的には美しいんですが、それで悩むんだったら素直に再帰した方が良い、ってのが事実。要するに再帰での「引数内のカウンター処理だけじゃどうしようもない」部分が出てきた場合、do
の出番だ、って言い方の方が正しいかも。
さて、valvallowさんの記事によると、どうやら「
NIL
じゃない返り値が欲しい」との事。まあ、これは当然でしょうね。ポール・グレアムが何でマクロで書いた
while
でNIL
以外の返り値を返すようにしなかったのか?想像するに、二つ程理由が考えられて、マクロの導入章(第7章なのに!)の辺りなんで、あんまややこしい例じゃなくってシンプルな例にしたかった事。あとは破壊的変更が前提のマクロなんで、返り値を返すとマズイ、って事があったんでしょうね。グレアムの
while
は次のようにしてみれば面白い結果が出てきます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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でお馴染みでしょうが、関数
last
とbutlast
ってのを使ってみます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
を二つに分けちゃう。最後と、それ以外、です。このアイディアで雛形は一応次の通りになります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defmacro my-while% (test &body body) | |
;; var の初期値は nil で、(last body) は do のボディの式が評価されてから実行される | |
`(do ((var nil ,@(last body))) | |
((not, test) var) | |
,@(butlast body))) ; body の最後尾以外の評価がボディの仕事 |
では実行してみますか。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)
の更新用。もうひとつは、前回のそれの保存用だ!これでどうだ。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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))) |
では実行してみましょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
上手く行きましたね。大成功!!!これで終わり……とはいかないんですよ、残念ながら(笑)。
実はこのマクロは次のような問題があるんです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%%
マクロの内部でvar0
、var1
って変数名を使ってるわけなんですけど、これが外部から与えられると途端にぶつかってしまってどうにもこうにも行かなくなる。つまり、これを避けるのが
gensym
です。だから、こう書かないとなりません。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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)))) |
これで安心してどんな変数名を
my-while
マクロ内に持たせても大丈夫です。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!
を用いれば次のように記述されますね。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defmacro! my-while! (test &body body) | |
`(do ((,g!var0 nil ,@(last body)) | |
(,g!var1 nil ,g!var0)) | |
((not ,test) ,g!var1) | |
,@(butlast body))) |
ちなみに、再帰版は次のようになるでしょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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 件のコメント:
コメントを投稿