Closures
Captured blocks and proc literals closure local variables and self
. This is better understood with an example:
x = 0
proc = ->{ x += 1; x }
proc.call # => 1
proc.call # => 2
x # => 2
Or with a proc returned from a method:
def counter
x = 0
->{ x += 1; x }
end
proc = counter
proc.call # => 1
proc.call # => 2
In the above example, even though x
is a local variable, it was captured by the proc literal. In this case the compiler allocates x
on the heap and uses it as the context data of the proc to make it work, because normally local variables live in the stack and are gone after a method returns.
Type of closured variables
The compiler is usually moderately smart about the type of local variables. For example:
def foo
yield
end
x = 1
foo do
x = "hello"
end
x # : Int32 | String
The compiler knows that after the block, x
can be Int32 or String (it could know that it will always be String because the method always yields; this may improve in the future).
If x
is assigned something else after the block, the compiler knows the type changed:
x = 1
foo do
x = "hello"
end
x # : Int32 | String
x = 'a'
x # : Char
However, if x
is closured by a proc, the type is always the mixed type of all assignments to it:
def capture(&block)
block
end
x = 1
capture { x = "hello" }
x = 'a'
x # : Int32 | String | Char
This is because the captured block could have been potentially stored in a class or instance variable and invoked in a separate thread in between the instructions. The compiler doesn’t do an exhaustive analysis of this: it just assumes that if a variable is captured by a proc, the time of that proc invocation is unknown.
This also happens with regular proc literals, even if it’s evident that the proc wasn’t invoked or stored:
def capture(&block)
block
end
x = 1
->{ x = "hello" }
x = 'a'
x # : Int32 | String | Char