「クロージャ、それなに?」ってエントリーで、「『クロージャ』って言葉の意味がわからない」と言ったのですが、lethevertさんからのトラックバックやshiroさんのコメントで多少は状況が見えてきました。
解決しかかったところで余計なことをいって混乱に拍車をかけてみる。
で確かに混乱はしたものの、さらにlethevertさんの解説が続いたので、自分なりの目星はついた気がします。
最近の傾向
ラムダ式(に相当するもの)やデータのように扱えるコードブロックをクロージャと呼ぶのが最近の傾向みたいですね。しかも、プログラミング言語の機能/能力として捉えることが多いようです。「ナントカ言語はクロージャが使える」みたいな用法で。
多くの人がそういう意味で使うなら、「本来は…」みたいなこと言ってもしょうがないので、別にいいや。
「ラムダ式+環境」というデータ構造
僕自身は、クロージャ=ラムダ式+環境 と思っていました。sumiiさんの言う「データ構造の一つ」に近い理解です。いつどこでそう思い込んだかは憶えていませんが、いずれにしても昔の話なので、「クロージャ」の意味が拡散する前のことでしょう(とは言っても、ランディンのオリジナル論文とかは全然知りません*1)。
僕の感じとしては、(λx.(a*x + b), {a : 2, b : 1})みたいなのがクロージャで、これは λx.(2*x + 1) を表現していることになります。{a : 2, b : 1}は記号表(名前・値マップ)のつもりです。記号表は環境とも呼びます*2。letを使って、(let a = 2, b = 1; λx.(a*x + b)) とか (let b = 1; (let a = 2; λx.(a*x + b))) と書いても同じ状況を表します。
挙動を後から変えられるラムダ式
僕が気になっていたことは、ラムダ式(無名関数)の非ローカル変数の扱いです。それで例として出したのが:
function makeAffineLinear(a) { return function(x) { return a*x + b; } }
これは、一次関数(affine linear function)を作る関数です。一次関数の定義時(makeAffineLinearを呼び出したとき)、aは確定しますがbは確定するとは限りません。
js> var f = makeAffineLinear(2) // 関数 2*x + b が定義される js> f(3) NaN js> var b = 1 js> f(3) 7 js> var b = 2 js> f(3) 8
変数fには λx.(2*x + b) (に相当する無名関数)が入ってますが、bが未定義だと f(3) = NaN(NaNは not a number)、b = 1 では f(3) = 2*3 + 1 = 7、そして b = 2 では f(3) = 2*3 + 2 = 8。と、このように、bの値がどうにでもできる点が僕は気持ち悪くて「クロージャと呼んでいいのか?」と疑問を感じていたのですが(次に続く)。
非ローカル変数の束縛
makeAffineLinearの例で、一次関数が作られる時点で、実はaとbの束縛は決定されています。「束縛が決定されている」とは、値が固定されることではなくて、a, bの値が欲しいときにどこを探せばいいかが分かっていることです。変数の名前から変数の値を求めるには記号表を引きますが、どの記号表を引けばいいかが決まるってことです。
JavaScriptを例として説明すると、makeAffineLinear(2) という呼び出しで(通常の言語のスタックフレームに相当する)呼び出しオブジェクトが生成されます。引数変数も含めたローカル変数は、この呼び出しオブジェクトのプロパティです。呼び出しオブジェクトに名前はありませんが、仮に_callだとすると、ローカル変数aの値は_call["a"]で求まります。一次関数λx.(a*x + b)と記号表_callを組にした(λx.(a*x + b), _call)でaの束縛は決定されます。でも、_call["b"]は値を持ちません。
じゃbはどうする? JavaScriptのルールでは、非ローカル変数は常に大域変数です*3。そして、大域変数は大域オブジェクトのプロパティです。大域オブジェクトも名前が付いてません*4が、仮に_globalとすると、大域変数bの値は_global["b"]です。結局、一次関数λx.(a*x + b)のbを求めるには記号表_globalを引きます。つまり、_callに値がなければ_globalを引くことになります。
だいたいの感じとしては、makeAffineLinear(2)により (λx.(a*x + b), _call, _global) みたいなデータが作られたわけ。_call、_globalという記号表(環境)によるスコープチェーンがラムダ式λx.(a*x + b)にくっついているデータと見ていいでしょう。
束縛は決まっても値は変えられる
bの値が後から自由に設定できるのは、記号表_globalがたまたまミュータブル(変更可能)だったからです。ちょっと細工をすると、ローカルなaさえも後から変更できます。
function makeALFuncAndASetter(a) { return [ // affine linear function function(x) { return a*x + b; }, // setter for the variable a function(x) { a = x; } ]; }
js> var alas = makeALFuncAndASetter(2) js> var f = alas[0] js> var setA = alas[1] js> var b = 1 js> f(3) 7 js> setA(1) js> f(3) 4
いかにも作為的ですが、function(x) {a = x;}
を経由して呼び出しオブジェクトの内部をいじってます。こうすると、λx.(a*x + b) を作った後でどうにでも挙動を変えることができますが、a, b の値を支配している記号表はずっと同じものを使い続けます。
閉じているからクロージャ?
これで、makeAffineLinear(2)で生成(定義)されたラムダ式では、非ローカル変数の束縛(使用する記号表)は決定済みだが、その記号表が変更可能なら値は変わるかもしれない事情はわかりました。
僕自身は、非ローカル変数の値をどう求めるべきか決定してない、つまり“開いている”状態だとクロージャと呼ぶのに抵抗があったのですが、makeAffineLinearの例でもやはり、ラムダ式と環境(記号表)が一緒になったものが生成されます -- こりゃクロージャですね。もっとも、冒頭で述べた昨今の傾向からすれば、そんなことにこだわる必要は全然ないのかもしれません。
実は、「閉じている」って言い方の語源のほうでいまだに僕は混乱しているのですが、そのハナシは別な機会にします。