skip to content

Captured Scope & Binding

Blocks and procs capture surrounding local variables by reference, so a closure can outlive its method and keep updating a counter. Interviewers probe what is shared and what a Binding exposes.

on this pageshow

explore

questions

5

In Ruby, which outer local variables can a block read and reassign, and why is a variable first assigned inside it gone afterwards?

level: juniorimportance: must knowfreq 58%

answer

  1. blocks are not scope gates
  2. outer locals: read and reassign
  3. first assignment inside stays inside
  4. the parser decides, not the runtime
  5. undefined local variable or method

basics

~20 s

A Ruby block can read and reassign every local already assigned above it in the enclosing scope. A name first assigned inside the block is local to that block run, so using it after the block raises NameError.

solid answer

~40 s

Unlike `def`, `class` and `module`, a block does not open an isolated scope: its scope nests inside the surrounding one, so it can read and reassign any local the parser has already seen assigned earlier in the source. `total = 0; prices.each { |p| total += p }` updates the outer `total`. A name whose first assignment is inside the block becomes a block-local variable, created afresh on each run and invisible afterwards, so reading it after the block raises `NameError` (undefined local variable or method). The decision is lexical: a lambda written before `total = 0` parses `total` as a method call. The fix is to assign the variable before the block, or to return a value from the block instead of collecting it.

code

ruby · 12 lines
ruby
clicks = 0
last_button = nil

%w[buy buy cancel].each do |button|
  clicks += 1          # reassigns the outer clicks
  last_button = button # reassigns the outer last_button
  seen_at = Time.now   # first assigned here: block-local
end

clicks       # => 3
last_button  # => "cancel"
seen_at      # NameError (undefined local variable or method 'seen_at')

go deeper

for a junior

Remember the two rules: outer locals assigned before a block can be changed inside it, and names first assigned inside a block are gone after it.

for a middle

Explain that the parser decides whether a name is a local, which is why a lambda written above the first assignment treats the name as a method call.

for a senior

Point out when leaking state through outer locals hurts readability, and prefer blocks that return values; mention Class.new and define_method as deliberate scope flattening.

for a principal

Judge where shared mutable locals in blocks are acceptable in a codebase and where a returned value or an explicit object makes data flow easier to review.

## Blocks nest in the scope around them Ruby has a few constructs that start a brand-new local scope, where no outer local is visible: `def`, `class` and `module`. Those are often called **scope gates**. A **block**, the `{ ... }` or `do ... end` code attached to a method call, is not one of them. A block does open a scope of its own, but that scope is nested in the one where the block is written, which is why blocks are sometimes described as **flat scope**: outer locals flow straight into them, while names created inside stay inside. ## The rules for a block 1. A local variable assigned **before** the block, in any enclosing block or the enclosing method, is visible inside it and can be **reassigned** there. The change is seen outside after the block. 2. A name whose **first assignment** is inside the block becomes a block-local variable. It is created fresh on every run of the block and disappears when that run ends. 3. A **block parameter** such as `|price|` is always local to the block, even if an outer local has the same name. 4. Nested blocks see the locals of every level around them. The Ruby syntax documentation says both halves directly: locals created inside a block do not leak to the surrounding scope, and variables defined in an outer scope appear in the inner one. ## The parser decides, not the runtime Ruby decides whether a bare name is a local variable or a method call **while parsing**, based on whether an assignment to that name appeared earlier in the source. That has two consequences: - A name used after a block, when its only assignment was inside the block, is not a known local there. Ruby treats it as a method call, finds no method, and raises `NameError` with "undefined local variable or method". - A block or lambda written **above** the line that first assigns a name treats that name as a method call too, even if the assignment runs before the lambda is called. ```ruby report = -> { "clicks: #{clicks}" } clicks = 3 report.call # NameError: clicks is a method call inside the lambda ``` Inside a block, `local_variables` lists both the outer names and the block's own, which is a quick way to see what the block can reach. ## Every run starts fresh A block attached to `each` or `times` is executed once per element, and each execution gets new block-local variables. That produces a subtle bug: ```ruby 3.times do |i| seen ||= [] seen << i p seen # [0], then [1], then [2] end ``` `seen` is first assigned inside the block, so it is block-local and starts as `nil` on every run; `||=` builds a new one-element array each time. Moving `seen = []` above the block turns it into one shared outer local, and the output becomes `[0]`, `[0, 1]`, `[0, 1, 2]`. ## Common cases | Code | Result | |---|---| | `total = 0; [5, 7].each { \|n\| total += n }; total` | `12`, the outer local was reassigned | | `[1, 2].each { \|n\| square = n * n }; square` | `NameError`, `square` was block-local | | `last = nil; items.each { \|i\| last = i }; last` | the final item | | `x = 10; [1].each { \|x\| }; x` | `10`, the parameter shadows but does not touch the outer `x` | ## Flattening scope on purpose Because blocks do not gate scope, metaprogramming code sometimes replaces the gates with block-taking methods so that a local can cross them: - `Class.new do ... end` instead of `class ... end` keeps the outer locals visible in the class body. - `define_method(:click) { ... }` instead of `def click` keeps them visible in the method body. The result is a method that shares a local with the code that defined it, which is the same closure rule applied one level up. It is a niche technique; ordinary application code rarely needs it. ## Idioms that avoid the trap - Initialise an accumulator before the block (`total = 0`, `last = nil`) when you really need the side effect. - Prefer letting the block's value do the work: `prices.sum`, `items.map { ... }`, `items.find { ... }` return the result instead of leaking it through an outer variable. - Keep block-local scratch variables block-local; that isolation is a feature, not a bug.

  • How can a counter local be shared between a class body and its methods when class and def open new scopes?
    Replace the gates with blocks: `clicks = 0; Counter = Class.new { define_method(:click) { clicks += 1 } }`. `Class.new` and `define_method` take blocks, and blocks keep outer locals visible, so every `Counter` instance increments the one captured `clicks`.
  • Why does a lambda written above the line total = 0 fail even if total is assigned before the lambda is called?
    Ruby decides at parse time whether `total` is a local. When the parser reads the lambda body, no assignment to `total` has appeared yet, so it compiles `total` as a method call, which raises `NameError` when no such method exists.

saying these in an interview costs you the question

  • A block has its own scope, so it cannot change outer locals
  • A variable first assigned inside each is available after the loop
  • Ruby decides at runtime whether a bare name is a variable
  • Updating a total from a block needs an instance or global variable
  • Blocks follow the same scope rules as def
open as a page

In Ruby, how can a lambda returned from a method keep incrementing a local variable after that method has returned?

level: middleimportance: must knowfreq 62%

basics

~20 s

A Ruby lambda closes over the local variable itself, not a copy of its value. When the lambda is created, CRuby moves the method's locals into a heap environment, so the lambda can still read and reassign clicks after the method returns.

open as a page

In Ruby, how do you memoise a price lookup in a lambda that closes over a Hash, and why can cache[sku] ||= miss the cache?

level: middleimportance: should knowfreq 38%

basics

~20 s

Create the Hash as a local in a factory method and return a lambda that checks it before computing; every call shares that Hash. cache[sku] ||= price_for(sku) recomputes whenever the stored price is nil or false, so check key? instead.

open as a page

In Ruby, why can a lambda kept in a long-lived registry hold a large local array and its creating object in memory, and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A CRuby lambda keeps the whole environment it was created in: every local of the enclosing scopes plus self, not only the names its body uses. A large array local stays reachable while the lambda lives, so build long-lived lambdas in small methods.

open as a page

In Ruby 4.0, what do Kernel#binding and Binding#local_variable_get/set expose, and why is a local created by local_variable_set invisible to the method?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Kernel#binding returns a Binding wrapping the current scope's locals and self. local_variable_get and local_variable_set read and write existing locals by name; setting a new name adds it only to the Binding, because the method's code was compiled without it.

open as a page