スクリプト言語間における「lexical closure」の違い

update:

なんとMatz様から直々の指摘を受けてしまった:
http://twitter.com/yukihiro_matz/status/26685527109

実はこのポストの後、ベンチプレスを1セットやって一息ついていいたところ、schemeの例が間違っていることに気付いた。確かにループのボディーにあたるlambdaでletでローカルを作ってキャプチャーするとMatz様のおっしゃる通り。

俺はPerlに心を侵されていたのか… いつも「動けばいいやぁ~」みたいな気持で使っていたclosureだが、もう一度勉強しなおした方がよさそうだ。


(let ([closures '()])
(for-each
;; これがループのボディーになるよな。
(lambda (i)
;; じゃ、ここにローカル
(let ([loop-local-var (sprintf "foo~A" i)])
(set! closures (cons
;; このクロージャで捉える。
(lambda () loop-local-var)
closures))))
'(0 1 2 3 4))

;; さて、どう出るか?
(for-each
(lambda (f)
(printf (f)))
(reverse closures))
)

foo4
foo4
foo4
foo4
foo4

Smalltalk

sumimさんのSmalltalkによる実装。Rubyもある。
http://d.hatena.ne.jp/sumim/20101008
なんと、バージョンで挙動が違う。

以下、オリジナルポスト。


人気のダイナミック言語の殆どが「lexical closure」をサポートしているということになっているが、その挙動には違いがある。schemeperlでclosureを覚えたが、そのつもりで、pythonjavascriptを使っていて痛いめにあったことが何度かある。pythonjavascriptはループ内のlocal variableをちゃんと捕獲(close)できない。とっておいたものが全部最後の値になってしまう。これからそういうことがないように、相違をまとめておく。

perlはちゃんとループの変数を捕獲できる


#!/usr/bin/perl -w
use strict;
my @closures;
for(my $i=0; $i<5; $i++) {
my $localvar="foo" . $i;
push(@closures, sub { $localvar });
}
map { printf "%s\n", $_->() } @closures;
# output
# foo0
# foo1
# foo2
# foo3
# foo4
closureはこうでなきゃ。

Pythonはループの変数を覚えておくことができない


closures=[]
for i in range(5):
closures.append(lambda: "foo"+str(i))
for f in closures:
print f()
"""
foo4
foo4
foo4
foo4
foo4
"""

javascriptもループの変数を「close」することができない。


var closures=[];
for (var i=0; i<5; i++) {
var localvar="foo"+i;
closures.push(function() { return localvar });
}
closures.map(function(f) { print(f()) });
// foo4
// foo4
// foo4
// foo4
// foo4

schemeは当然できる

lisp系言語の場合、全てが関数やspecial formでlexical closureは当然作動するのであまり意味ないが、とりあえずやってみた。


(let ([closures '()])
(for-each
(lambda (i)
;; close on i. push the func into a list.
(set! closures (cons
(lambda () i)
closures))
)
'(0 1 2 3 4))

;; invoke each closure
(for-each
(lambda (f)
(printf "foo~A\n" (f)))
(reverse closures))
)
;; foo0
;; foo1
;; foo2
;; foo3
;; foo4

Paul Grahamが言っていたようにperlの方がpythonよりLISPっぽい。一方、OOもclosureで実装したりすごくLISPっぽい使い方をするjavascriptがこのような挙動なのはちょっと驚いた。

closureのようなダイナミック言語の機能は全てLISPが元祖なので、これが「正しい挙動」だろう。pythonjavascriptも言語スペックに基ずいて正確に作動しているて、上記の使い方は厳密には間違いってことになるんだろう。しかし、プログラミングを楽にしてくれる、プログラマの期待通りに動いていくれる便利なclosureという意味では、どこでも見える変数を掴んで、それを後で参照できるLISP系やそれを忠実に再現しているPerlがちゃんとしたクロージャの実装という風に思える。

ところでrubyluaはどうなんだろう?