Closures

Captured blocks and proc literals closure local variables and self. This is better understood with an example:

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

Or with a proc returned from a method:

  1. def counter
  2. x = 0
  3. ->{ x += 1; x }
  4. end
  5. proc = counter
  6. proc.call # => 1
  7. 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:

  1. def foo(&)
  2. yield
  3. end
  4. x = 1
  5. foo do
  6. x = "hello"
  7. end
  8. 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:

  1. x = 1
  2. foo do
  3. x = "hello"
  4. end
  5. x # : Int32 | String
  6. x = 'a'
  7. x # : Char

However, if x is closured by a proc, the type is always the mixed type of all assignments to it:

  1. def capture(&block)
  2. block
  3. end
  4. x = 1
  5. capture { x = "hello" }
  6. x = 'a'
  7. 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:

  1. x = 1
  2. ->{ x = "hello" }
  3. x = 'a'
  4. x # : Int32 | String | Char