In Ruby, which outer local variables can a block read and reassign, and why is a variable first assigned inside it gone afterwards?
answer
- blocks are not scope gates
- outer locals: read and reassign
- first assignment inside stays inside
- the parser decides, not the runtime
- undefined local variable or method
basics
~20 sA 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 sUnlike `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 linesclicks = 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
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.
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.
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.
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