コンテンツにスキップ

クロージャ

捕捉されたブロックと Proc リテラルはローカル変数と self をクロージャに格納します。例を見てみるとわかりやすいでしょう。

x = 0
proc = ->{ x += 1; x }
proc.call # => 1
proc.call # => 2
x         # => 2

もしくは、メソッドが返す proc の場合は以下となります。

def counter
  x = 0
  ->{ x += 1; x }
end

proc = counter
proc.call # => 1
proc.call # => 2

上記の例において、x はローカル変数であるにも関わらず、Proc リテラルによって捕捉されています。通常であれば、ローカル変数はスタックに存在し、メソッドが終了すると消えますが、このような場合はコンパイラは x をヒープに割り当て、Proc が動作するためのコンテキストのデータとして利用します。

クロージャの変数の型

ローカル変数の型に対して、コンパイラは「それなりに」賢く解釈します。例をあげます。

def foo
  yield
end

x = 1
foo do
  x = "hello"
end
x # : Int32 | String

コンパイラは、ブロックのあとに x が Int32 か String であると判断できます (ただ、この場合だとメソッドは必ず yield するので、常に String であることは明白です。将来的にはそこまで判断できるように改善するつもりです)。

もしブロックの後で x に何かが代入されたとき、コンパイラはその型が変更されたと判断します。

x = 1
foo do
  x = "hello"
end
x # : Int32 | String

x = 'a'
x # : Char

しかし、もし x が Proc によってクロージャに格納された場合は、その型はすべての代入された型の組み合わせとなります。

def capture(&block)
  block
end

x = 1
capture { x = "hello" }

x = 'a'
x # : Int32 | String | Char

この理由は、捕捉されたブロックはグローバル変数やクラス変数、そしてインスタンス変数に保持されることもあり、そして別のスラッドで実行される可能性もあるためです。このことに対して、コンパイラが綿密な分析をすることはありません。コンパイラはただ、Proc に変数が捕捉されていたら、その proc がいつどこで実行されるかは未知である、として扱います。

これは通常の Proc リテラルにも当てはまります。そして、その proc が実行も保持もされないことが明白であっても同様です。

def capture(&block)
  block
end

x = 1
->{ x = "hello" }

x = 'a'
x # : Int32 | String | Char