karasuyamatenguの日記

 | 

2010-10-08

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

05:04

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


その他各種言語

PHP

http://www.1x1.jp/blog/2010/10/lexical_closure_php.html

postscript

http://d.hatena.ne.jp/yshl/20101009

common lisp

http://d.hatena.ne.jp/Shinnya/20101009/1286586885

IO

http://gist.github.com/618229

Scala

http://gist.github.com/618242

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さんによる。

matzmatz 2010/10/09 16:07 えーとですね。まだ、言語ごとにやってることが違うんですよ。
つまり、PerlとRubyはブロック内だけで毎回新しい変数localvarを作ってる(ので、closure間で変数を共有していない)のに対して、Python(関数レベルしかスコープがない)などはもっと広いスコープでlocalvarが有効でclosure間で同じ変数を共有しています。localvarのスコープがループの中だけの言語(Ruby,Perl)とclosuresとlocalvarが同じスコープの言語(Python,JavaScript)というクロージャとは関係ない部分の違いです。

kwatchkwatch 2010/10/10 00:20 matz氏のコメントを補足します。
Rubyの場合、each+ブロックのかわりにfor文を使うと、Pythonと同じ挙動になります。

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"

この理由ですが、Rubyでは (eachで使うような)ブロックは新しいスコープを作成しますが、for inだと作成しません。そのため、3行目のlocalvarは、for文の外側である1行目と同じスコープになります。
これに対し、元記事のコードではeachとブロックを使っているために、3行目のlocalvarは1行目と違うスコープになっています。これが挙動が違う理由です。
なおPythonでも、iを引数のデフォルト値として使うと、(仕組みはともかく見た目は)RubyやPerlと同じ挙動になります。

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

sumimsumim 2010/10/10 23:11 Smalltalkのコードですが、他の言語と記述内容と見た目を統一するため、両方とも次のコードに差し替えていただければ幸いです。出力は変わりません。

| 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
]

インデントの全角スペースは半角スペース×3などに適宜置き換えてください。お手数ですが、よろしくお願いいたします。

karasuyamatengukarasuyamatengu 2010/10/15 09:40 Matzさん: 解説ありがとうございました。もう一度勉強しなおしてみます。

kwatchさん: Rubyはfor inを最初っから使うべきでした。そして、Pythonのデフォルト値トリックも大変興味深いものです。コードの貢献ありがとうございました。

suminさん: コードをご指示のとうり変更しておきました。あんな感じでよろしいでしょうか?

あとツイッターやブログでコメントしてくれた皆様、ありがとうございました。勉強になりました。

 |