Skip to content

Closures capture variables, not values

Level: 201 · a Python programmer who has met nonlocal and the lambda-in-a-loop trap

One line: A block, proc or lambda carries the variables around it — so it sees later assignments and can make them — and a block parameter is a fresh variable per call, while an outer variable assigned in a loop is one variable shared by every closure made there.

A block, a proc and a lambda are all closures: they hold the variables of the scope that wrote them, not a copy of the values those variables had. So a lambda that reads n returns whatever n is at call time (row 1), the two lambdas made by one make_counter call share one count while a second call gets its own (row 2), and a block can assign an outer local outright (row 3). Scope runs one way: a name first assigned inside a block does not exist after it (row 4). What makes the loop trap of other languages rare in Ruby is row 5 — a block parameter is a fresh variable on every call, so three lambdas closing over |i| return 1, 2, 3 — but the trap is still there whenever the variable lives outside the block: an outer j assigned inside map, or the i of a for loop, which makes no scope of its own, is one variable, so every lambda returns 3 and i is still 3 after the loop (row 6). A block parameter with the same name as an outer local shadows it (row 7), and a |x; tmp| list after the parameters declares tmp block-local, so the block's assignment does not reach the outer tmp (row 8). The captured variable is a real one: f.binding.local_variable_get(:n) reads it from outside (row 9). And a def is the one construct here that is not a closure — a method body opens a fresh scope and cannot see the locals around it (row 10).

Verified output of closures_capture_variables_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. a closure sees the later value of n:                 2
 2. two lambdas share one count; a new maker starts over: 2, 0
 3. a block assigns the outer total:                     6
 4. a name first assigned inside stays inside:           defined?(inner) = nil
 5. a block parameter is fresh per call:                 [1, 2, 3]
 6. an outer j assigned in the block; a for loop's i:    [3, 3, 3], [3, 3, 3], i after the loop = 3
 7. a block parameter shadows the outer x:               x = 10
 8. |x; tmp| makes tmp block-local:                      tmp = "outer"
 9. the captured variable, seen from outside:            f.binding.local_variable_get(:n) = 2
10. a def is not a closure:                              NameError: a def cannot see the outer n

Reading the output

Row 1 prints 2 although f was created while n was 1: the lambda holds the variable. Row 2 prints 2, 0 — two inc.calls moved the first counter's count, and the second make_counter call has a count of its own. Row 3's total is 6 after each added to it from inside the block; row 4's defined?(inner) is nil after a block assigned inner, because a variable born inside a block dies with it. Row 5 is [1, 2, 3]: three calls of the block, three variables named i, three lambdas each holding its own. Row 6 is [3, 3, 3] twice — once for an outer j assigned inside the block and once for a for loop's i — and then i after the loop = 3, the sign that for leaked its variable into the surrounding scope. Row 7 leaves x = 10, since the block's |x| was a different variable; row 8 leaves tmp = "outer", since |x; tmp| gave the block a private tmp. Row 9 reads 2 out of the lambda's binding. Row 10 is the NameError raised when a method body mentions n.

Compared with Python

Python's nested def and lambda close over the enclosing function's variables in the same way (rows 1 and 2), with one rule Ruby does not have: reading an outer variable is free, but assigning to it makes a new local unless the function declares it nonlocal — so total += x without the declaration is an UnboundLocalError, and with it the total is 6 (row 3). Row 4 is the same in both. Rows 5 and 6 are the famous trap, seen from Python's side: a comprehension or a for loop uses one variable, so [lambda: i for i in range(1, 4)] gives three closures that all return 3, and the fixes are a factory function whose parameter is fresh per call — exactly Ruby's block parameter — or the lambda i=i: default that copies the value at definition time. A parameter shadows (row 7) and an assignment in a def is local by default (row 8), which is why Python never needed a |x; tmp| syntax. The captured variable lives in a cell, f.__closure__[0].cell_contents, and co_freevars names it (row 9); a module-level name is a global rather than a cell, which is why the twin builds its closures inside functions. Row 10 is the difference that bites in the other direction: a nested Python def is a closure and sees n, while a Ruby def does not. Python states the rules under naming and binding ↗ and the nonlocal statement ↗, and answers the loop question in the programming FAQ ↗.

Verified output of closures_capture_variables_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. a closure sees the later value of n:                 2
 2. two closures share one count; a new maker starts over: 2, 0
 3. assigning the outer total, without / with nonlocal:  UnboundLocalError / 6
 4. a name first assigned inside stays inside:           reading inner outside = NameError
 5. a factory call or an i=i default is fresh per call:  [1, 2, 3], [1, 2, 3]
 6. a comprehension's i; a for loop's i:                 [3, 3, 3], [3, 3, 3], i after the loop = 3
 7. a parameter shadows the outer x:                     x = 10
 8. an assignment in a def is local by default:          tmp = 'outer'
 9. the captured variable, seen from outside:            f.__closure__[0].cell_contents = 2, co_freevars = ('n',)
10. a nested def is a closure:                           sees n = 2

Try it

  1. Write make_account(balance) that returns three lambdas — deposit, withdraw and balance — sharing one balance variable, with withdraw refusing to go below zero. Open two accounts and show that moving money in one leaves the other alone.
  2. Build three procs in a loop three ways — each with a block parameter, each assigning an outer variable, and for — and predict each result before running. Then fix the for version by moving the loop body into a method that takes i as a parameter, the way Python's factory function does, and explain why that works although a def cannot see i at all.
Solution to 1

closures_capture_variables_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: make_account(balance) returns three lambdas that share one
# balance variable; two accounts do not interfere, because each call of
# make_account has its own balance.

def make_account(balance)
  deposit  = ->(amount) { balance += amount }
  withdraw = ->(amount) { amount <= balance ? (balance -= amount) : "refused: #{amount} > #{balance}" }
  read     = -> { balance }
  { deposit: deposit, withdraw: withdraw, balance: read }
end

a = make_account(100)
b = make_account(5)

puts "a deposit 50   -> #{a[:deposit].call(50)}"
puts "a withdraw 30  -> #{a[:withdraw].call(30)}"
puts "b withdraw 30  -> #{b[:withdraw].call(30)}"
puts "a balance      -> #{a[:balance].call}"
puts "b balance      -> #{b[:balance].call}"
puts "shared inside a: deposit and read see one variable: #{a[:deposit].call(0) == a[:balance].call}"

Verified output of closures_capture_variables_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

a deposit 50   -> 150
a withdraw 30  -> 120
b withdraw 30  -> refused: 30 > 5
a balance      -> 120
b balance      -> 5
shared inside a: deposit and read see one variable: true

See also