スクリプト言語間における「lexical closure」の違い、それともプログラムの違い?

Matzさんに「closureの件、結論としては元記事のPerl, Python(+JavaScript), Schemeプログラムがそれぞれまったく別のことをしていただけで、closureの仕様はすべて同じであるということ。」と指摘されたので、もう一度整理してみます。
http://twitter.com/yukihiro_matz/status/26707927087

sumimさんのSmalltalkRubyの例(http://d.hatena.ne.jp/sumim/20101008)も加えます。

実験内容

言語間でやっていることの相違がないように、できるだけ似たコードにする。


ループでiを0から4まで回す
ループブロック内で:
iを埋め込んだlexical variable、'localvar'を定義。
localvarを参照したclosureをリストに追加。
5つのclosureの値をプリント

つまり、各言語においてループブロックの中でlocalvarを作り、それをcloseするようにする。

perl


my @closures;
foreach my $i (0..4) {
my $localvar="foo" . $i;
push(@closures, sub { $localvar });
}
map { printf "%s\n", $_->() } @closures;


perl colv.pl
foo0
foo1
foo2
foo3
foo4

ちなみに、perlは「異常の罪」から無罪になった: http://twitter.com/yukihiro_matz/status/26701743280

JavaScript


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


js colv.js
foo4
foo4
foo4
foo4
foo4

Python


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


python colv.py
foo4
foo4
foo4
foo4
foo4

Ruby with .each{}

sumimさんによる。 参照: http://d.hatena.ne.jp/sumim/20101008


closures = Array.new
(1..5).each{ |i|
localvar="foo" + i.to_s
closures << proc{ localvar }
}
closures.each{ |f| p f[] }


"foo1"
"foo2"
"foo3"
"foo4"
"foo5"

手元にRubyが無いので、ここで実行: http://www.ruby.ch/interpreter/rubyinterpreter.shtml

Ruby with for i in (by kwatchさん)


closures = Array.new
for j in (1..5)
localvar = "foo" + j.to_s
closures << proc{ localvar }
end
closures.each {|f| p f[] }

"foo5"
"foo5"
"foo5"
"foo5"
"foo5"
kwatchさん コード貢献ありがとうございました。 for inの方がeachより他の例と似ているので、こっちを最初っから使うべきでした。

Smalltalk: Squeak4.1

sumimさんによる。 参照: http://d.hatena.ne.jp/sumim/20101008

closures

World findATranscript: nil.
closures := OrderedCollection new.
(1 to: 5) do: [:i |
| localvar |
localvar := 'foo', i printString.
closures add: [localvar]
].

closures do: [:f |
Transcript cr; show: f value
]


"=>
foo1
foo2
foo3
foo4
foo5 "

Smalltalk: Squeak4.0以前

sumimさんによる。 参照: http://d.hatena.ne.jp/sumim/20101008

closures

World findATranscript: nil.
closures := OrderedCollection new.
(1 to: 5) do: [:i |
| localvar |
localvar := 'foo', i printString.
closures add: [localvar]
].

closures do: [:f |
Transcript cr; show: f value
]


"=>
foo5
foo5
foo5
foo5
foo5 "

結果

挙動は二つに分れる:

  • iteration毎の値が残る: foo0 foo1 foo2 foo3 foo4
  • 全て最後の値になる: foo4 foo4 foo4 foo4 foo4

分類すると

ということで、「えせ」と「普通」はタイ。

結論、というよりは疑問

プログラムを同じにする努力をしたんだけど、別のことをやっているってことになるのかな?

素人の印象だが、ループブロック内のローカル変数を使い回される一つのinstanceとみなすか、iteration毎に別の個体としてとらえるか、という言語の違いのように見える。つまり、ループのブロックがiteration毎にcloseする対象となるスコープになるか、ならないのか、というclosureの仕様の相違のような気がする。

言語エキスパートの皆様、今後もご指導のほどよろしくお願いします。

というわけでまだ釈然としないけど、shota243さんから教わった必殺技「ダブルlambda」でJavaScriptPythonでもfoo1 foo2 foo3..ができることがわかったので、大収穫。
http://d.hatena.ne.jp/karasuyamatengu/20101008/1286518255


その他各種言語

Python 2

Pythonでも、iを引数のデフォルト値として使うと...


closures=[]
for i in range(5):
localvar="foo"+str(i)
# ここが上のと違う
closures.append(lambda arg=localvar: arg)
for f in closures:
print f()

foo0
foo1
foo2
foo3
foo4
kwatchさんによる。