パンの袋をとめるアレをクロージャといいます。
DXRubyWSではシグナルハンドラやオートレイアウトの定義でクロージャを多用するのだが、クロージャと言われてもピンとこない人は結構いるのではないかと思う。今回はクロージャとは、という話。
■スタックとローカル変数
Rubyでメソッドを定義するとその中ではその中だけで使えるローカル変数を定義することができる。
def foo bar = Object.new # barはローカル変数 end
ローカル変数は通常VMのスタック領域に確保される。上記コードではObject.newしたオブジェクトがbarに格納されるが、メソッドfooを抜けるとスタック領域は解放されて、変数barも無くなるので、Object.newで作ったオブジェクトは参照が無くなって次のGCで回収される。
■ブロックとProcオブジェクト
メソッドに渡す引数としてのブロックは、変数に格納するとProcオブジェクトになる。
def foo(&b) bar = b end foo do Object.new end
上記のコードでは、引数bとしてObject.newしているコードのProcオブジェクトが渡される。それをbarに格納しているが、これもメソッドfooを抜けると参照が無くなるので、Procオブジェクトは実行されることなくその生涯を終える。
■ブロックのスコープの不思議
メソッドの引数として渡されるブロックは、Rubyを使う人なら意識せずやっているだろうとは思うが、ブロックの外のローカル変数を参照することができる。
foo = 1 5.times do foo += 1 end p foo #=> 6
timesというメソッドの中でブロックを呼び出しているのだが、当然そっちのメソッドの中はスコープが切れているのでローカル変数fooを見ることはできない。このブロック自身がfooの場所を覚えているということである。
■Procオブジェクトとスタック
さて、ではこれは何が起こっているのかというのが本題となる。
def foo a = 0 Proc.new do a += 1 end end bar = foo p bar.call #=> 1 p bar.call #=> 2 p bar.call #=> 3
メソッドfooはローカル変数aを0で初期化して、a+=1するProcオブジェクトを生成して返す。これがメソッドの外の変数barに格納される。メソッドfooはそれで終わるので本来であればスタック領域は解放されてローカル変数aも存在しなくなるのだが、しかし変数barに格納されているProcオブジェクトは変数aを参照・更新しようとしているわけだ。
ここで、Procオブジェクトは自身が参照しているローカル変数のスタック領域を保存する。Procオブジェクトが参照を持っているからfooのスタック領域は消えないのである。ただし、fooは呼ばれるたびにスタック領域を確保するので、fooを抜けたあとでそこにあった変数aにアクセスできるのは返してきたProcオブジェクトだけである。fooを呼ぶたびに同じ形をしたスタックが生成されてProcオブジェクトが生成されることになる。
def foo a = 0 Proc.new do a += 1 end end bar = foo baz = foo p bar.call #=> 1 p bar.call #=> 2 p baz.call #=> 1 p baz.call #=> 2
■クロージャ
Rubyでは、Procオブジェクトが外側のローカル変数を参照した場合に、自動的にスタック領域を保持するように動作する。通常のブロックはyieldされるだけならただのブロックだが、Procオブジェクトに変換されるとローカル変数を参照していた場合にスタック領域を保持するようになる。参照していないと保持しない。この、スタック領域を特別に保持しているオブジェクトを一般的にクロージャと言う。
通常のブロック構文(イテレータとか)だとすぐに実行されるので意識することはまったく無いが、DXRubyWSのシグナルハンドラの場合はそのブロックをProcオブジェクトとして保存しておいて後で呼び出すから、ブロックから参照しているローカル変数はどうなるの?という疑問を持つことに、この仕掛けを知らない人はおそらく、なる。
その回答としては、Procオブジェクトはスタック領域を保持するクロージャになるので、ブロック構文を実行した時点のスタック領域に入っている参照が使われる、ということになる。このあたりはローカル変数とスタックのイメージを持てないと理解するのは難しい。
まあ、Rubyの基本機能であるので、覚えておいて損は無いと思う。