Jim の紹介と適切なクロージャとは何かについての疑問 [Tcl]
Jim [1][2] という、クロージャを持った Tcl の処理系があった。
それでまず気になるのがクロージャの自由変数の取り扱いなのだけど、Jim ではプロシージャ定義や匿名関数の仮引数のあとに static variable と呼ばれる関数内に閉じた変数を定義する部分があって、そこに初期値を与えなければ定義時点で見えている値で初期化される。クロージャを使ったカウンタはこんな感じになる。(ドットがプロンプトで{>が継続プロンプトです)
. proc make-counter {} { {> set x 0 {> lambda {} x {incr x} {> } . set counter [make-counter] <reference.<functio>.00000000000000000003> . $counter 1 . $counter 2
別の言い方をすると局所的に束縛されていない変数を自動的に上に上に探していくということはしてくれなくて、自由変数がどれか―この場合は x ―ということを明示的に教えてやらないといけない。
また、先に「初期化される」と書いたのはこの匿名関数が保持する x は make-counter のローカル関数の x とは別のメモリ領域を指すことになるからだ。
これは以下のような例にすると分かる。
. proc make-counter2 {} { {> set x 0 {> list [lambda {} x {incr x}] [lambda {} x {incr x}] {> } . foreach {a b} [make-counter2] {} . $a 1 . $a 2 . $b 1 . $b 2 . $a 3
なお foreach 部分は多重代入のためのトリック。ここでは2つの匿名関数を作っているが、同じ make-counter2 の局所変数の x を元にしてはいるものの、2つのカウンタが別々にカウントアップしていくのがわかる。
これは例えば Common Lisp のクロージャの仕組みとは異なっている。Lisp ではこうだ。
> (defun make-counter2 () (let ((x 0)) (values (function (lambda () (incf x))) (function (lambda () (incf x)))))) MAKE-COUNTER2 > (multiple-value-setq (a b) (make-counter2)) #<Closure: #e3729c> > (funcall a) 1 > (funcall a) 2 > (funcall b) 3 > (funcall b) 4 > (funcall a) 5
ここでも1つの変数を元に2つのカウンタを作っているが、共有のカウンタ値をカウントアップしていく。これは2つの匿名関数が make-counter2 のローカル変数 x への「参照」を保持しているからだ。
では Jim のクロージャはまだ真のクロージャと呼べるものではないのだろうか。
Evolutions of Lua [3] によると Lua 3.1 のクロージャ実装では、やはり外側の変数そのもの(変数の参照)ではなく、その frozen value をクロージャに持たせることで lexical scoping を実現していたという(したがって上記のようなカウンタは作れなかっただろう)。
その実装が批判を受けたことで作者らは "full" lexical scoping の実装を模索したというようなことが書いてある。
しかし Common Lisp も Jim も Lua 3.1 も、もし自由変数が immutable であれば差は生じないはずだ。クロージャの実例としてよく上記のようなカウンタが使われるけど、これはもともと関数型言語らしい例ではない。
だから問題は「副作用が普通な言語において適切なクロージャ実装とは何か」ということになるのだと思う。
もし Common Lisp の方式が適切で Lua 3.1 や Jim がそうでないとしたらそれはどういう根拠によるものなのだろうか。
[1] http://jim.berlios.de/
[2] http://wiki.tcl.tk/jim
[3] http://www.tecgraf.puc-rio.br/~lhf/ftp/doc/hopl.pdf
コメント 0