2006/01/10
関数型言語でfor文が無いのは、という話がどこかでもりあがってるのを見た。 それについては「再帰が基本で、forはsyntax sugar」でいいと思うのだけれど、 もうひとつ、for文みたいなイディオムは、ファーストクラスクロージャと 組み合わさると、変な落とし穴を作ることがある。
ナイーブな実装を考える。
(define-syntax for (syntax-rules () ((for (var init) test update body ...) (let ((var init)) (let loop () (when test body ... update (loop))))))) gosh> (for (i 0) (< i 5) (set! i (+ i 1)) (print i)) 0 1 2 3 4 #<undef>
一見うまく動く。しかし、for文のbodyでクロージャが作られ、それが for文のエクステントを越えて生き延びると、困ったことが起きる。
gosh> (define p '()) p gosh> (for (i 0) (< i 5) (set! i (+ i 1)) (push! p (lambda () i))) #<undef> gosh> p (#<closure #f> #<closure #f> #<closure #f> #<closure #f> #<closure #f>) gosh> (map (cut <>) p) (5 5 5 5 5)
処理の本体をクロージャにして抽象化するのはSchemerなら息を吸うように やってることだが、ループ変数を破壊的に変更してしまうとそれが 等価な変換ではなくなってしまうのだ。 些細なことに思えるかもしれないが、こういうところにトラップがあると まるで息を吸うのに不自由する感じがある。
Schemeのdo構文は再帰のsyntax sugarなのでこの問題はない。forも ループ変数を破壊的変更でなく再帰関数の引数にして持ち回ればこの問題は 起きないのだが、手続き型言語のfor文は(C++の「イテレータ」イディオムを 含めて)破壊的変更を暗黙のうちに仮定しているかのようだ。
なおCommon Lispのloopマクロやdotimesマクロはループ変数を 破壊的変更しくさるのだが、これはきっと破壊的変更の悪をプログラマに 教えんとするSteeleの親心に違いない。そうに違いないのだ。 過去に学ばないプログラマは何度も何度も何度も何度もクローズした はずの変数がいつのまにか書き換わってることに悩んで何時間も デバッグに費すのだ。おかげで昨日も締切前の貴重な時間を無駄にした。くそぅ。
Tags: Programming, Lisp, Scheme