スクリプトブロックはクロージャではない
PowerShellのスクリプトブロックは全然クロージャではない、ということに気付きました。そもそも、何故クロージャだと思い込んでいたのかが謎ですが……。レキシカルクロージャ(笑)ではない、というのが正しいでしょうか。よくわからないですが……。
1から10までの数を返すスクリプトブロックを作って配列に入れたもの、を返す関数、を作り、返ってきたスクリプトブロックを順番に実行している(つもりな)のですが、これを実行すると、$nullの配列が返ります。
function fa(){
$func_array = @()
1..10 | % {
$i = $_
$func_array += { return $i }
}
return $func_array
}
fa | % { & $_ }
Rubyで同じように書くと
def fa func_array = [] (1..10).each do | i | func_array << Proc.new { i } end return func_array end fa().each { | f | p f.call }
そっくりだ! でもこっちは、1から10までが表示されます。
なぜ、PowerShellのほうは値を返してくれないのかというと、スクリプトブロックが実行されるときに、$iが存在しないからです。つまり、実行されるときに$iがあれば、その値を返します。
これだと、1から10になります。実行時に、$iが1から10になっているからです。どうも、PowerShellのスクリプトブロックは、ブロック内で初期化されていない変数は、実行時のスコープから探すようです。実行時のスコープで変数を参照することをダイナミックスコープと呼ぶようです(クロージャはレキシカルスコープ)。たまたま同じ名前の変数を参照してしまうことがありえるので少し気持ち悪い……。evalとか、マクロ展開っぽい感じ。
function fa(){ return { return $i } }
$func = fa
1..10 | % { $i = $_; & $func }
クロージャっぽい動きにする方法も考えました。スクリプトブロック定義時の参照をどこかに保持しておかなければならないので、一つオブジェクトを作成し、そこに保持しておきたい参照と、スクリプトブロックをメンバーとして追加します。スクリプトメソッドとしてオブジェクトに追加されたスクリプトブロックは、$thisを通して、他のメンバーを参照できます(他にもっと良い方法があるかもしれませんが)。
これだと、1から10の配列が返ります。
function oa(){
$obj_array = @()
1..10 | % {
$i = $_; $o = New-Object PSObject
Add-Member -I $o -N Count -M NoteProperty -Va $i
Add-Member -I $o -N Run -M ScriptMethod -Va { return $this.Count }
$obj_array += $o
}
return $obj_array
}
oa | % { $_.Run() }
自由変数だと実行時の環境を参照してしまうので、スクリプトブロックを定義した時点でオブジェクトのメンバとして全部自前で束縛しておかないといけない、みたいな感じでしょうか……。Javaの無名クラスでクロージャ実現というのと似てるのかな、と思ったけど、Javaの場合は、無名クラスが定義されてるメソッド内のfinalな参照には、定義時のスコープを抜けていてもアクセスできるらしいです。