skip to content

In Ruby, what does declaring a block-local variable with |row; total| do, and when would you reach for one?

level: middleimportance: nice to knowfreq 18%

answer

  1. names after the semicolon
  2. fresh local for each block run
  3. outer same-named variable untouched
  4. block parameters shadow outer names too
  5. counts as an ordinary parameter

basics

~20 s

Names listed after a semicolon in a block's parameter list are block-local variables: fresh locals that start as nil on each run and never touch an outer variable with the same name, even when the block assigns to them.

solid answer

~40 s

A block can read and assign the local variables of the scope it was written in, so `total = n` inside a block normally overwrites an outer `total`. Declaring `|row; total|` makes `total` a **block-local variable**: a new local that exists only inside that block run, starts as `nil`, and leaves any outer `total` untouched. Block parameters themselves already shadow outer names, so `|total|` would also protect it, but only a name after the `;` gives you a scratch variable that is not a parameter. You reach for it in long methods where a helper name inside a block might collide with a variable defined earlier. Because the list counts as ordinary parameters, a block with `|; tmp|` cannot also use `_1` or `it`.

code

ruby · 8 lines
ruby
line = "== report =="

["a,1", "b,2"].each do |raw; line|
  line = raw.split(",").join(" -> ")
  puts line
end

puts line # => == report ==

go deeper

for a junior

Recall that a block can change an outer local it assigns to, and that names after a semicolon in the pipes are fresh block-only variables.

for a middle

Explain the two assignment rules inside blocks, show that parameters also shadow, and note that block-locals start as nil each run and receive no yielded values.

for a senior

Judge when block-locals help versus extracting a method, and explain why a declared |; tmp| list forbids it and numbered parameters.

for a principal

Frame naming discipline in long blocks as a readability policy: whether a team prefers block-locals, distinct names or small methods, and how reviewers enforce it.

## Blocks share the surrounding locals A Ruby block is written inside a method body (or at the top level) and can see the **local variables** that exist there. Assignment inside the block follows a simple rule: - If a local with that name **already exists** outside the block, assignment inside the block **updates the outer variable**. - If it does **not** exist yet, assignment creates a new local that lives only for that run of the block. That first rule is usually what you want, for example when accumulating into `sum`. It becomes a bug when the block reuses a name by accident. ```ruby total = 0 [3, 4].each { |n| total = n } total # => 4, the outer variable was overwritten ``` ## Declaring block-local variables The block parameter list accepts a semicolon. **Names after the `;` are block-local variables**: ```ruby total = 0 [3, 4].each { |n; total| total = n } total # => 0, the outer variable is untouched ``` Facts about them: 1. They are **new locals** scoped to the block, even when an outer variable has the same name. 2. They start as **`nil`** on every run of the block; nothing carries over from the previous run. 3. They receive **no arguments**: `yield` fills the ordinary parameters before the `;` only. 4. The list may contain only block-locals: `|; tmp, idx|` is valid. Ruby's own calling-methods reference shows the effect with a `place` variable: with `; place` declared, the outer `place` keeps its value; without it, the block's assignment replaces it. ## Parameters shadow too A block **parameter** with the same name as an outer local also shadows it: ```ruby row = "header" ["a", "b"].each { |row| row.upcase } row # => "header" ``` Ruby 2.6 removed the old "shadowing outer local variable" warning for this case, so the shadowing is silent. Block-local variables extend the same protection to scratch names that are not parameters. | Declaration | Outer `total` after the block | |---|---| | `{ \|n\| total = n }` | overwritten | | `{ \|n; total\| total = n }` | unchanged | | `{ \|total\| total = 1 }` | unchanged (parameter shadows) | ## Interaction with it and numbered parameters A block that declares anything between the pipes, even only block-locals, is treated as having **ordinary parameters**. Ruby then rejects the implicit parameters: - `proc { |; tmp| _1 }` is a `SyntaxError`: "numbered parameters are not allowed when an ordinary parameter is defined". - `proc { |; tmp| it }` is a `SyntaxError`: "'it' is not allowed when an ordinary parameter is defined". If you need a scratch variable in a short block that uses `it`, name the parameter explicitly instead. ## Tracing a run Consider `["a,1", "b,2"].each do |raw; line| ... end` in a method that already set `line = "== report =="`: 1. The first run binds `raw` to `"a,1"` and creates a **fresh** `line`, set to `nil`. 2. The block assigns `line = "a -> 1"`; only the block's `line` changes. 3. The second run gets **another fresh** `line`, again `nil`, before its own assignment. 4. After `each` finishes, the method's `line` still holds `"== report =="`. Without `; line`, step 2 would overwrite the method's variable, and any code after the loop that expected the header would print the last row instead. ## When it is worth using Block-local variables are rare in modern Ruby code, and many developers have never written one. They earn their place when: - a **long method** already has a local such as `result` or `line`, and a block deep inside wants a temporary with the same natural name; - code is being **pasted into a block** and you want a guarantee it cannot clobber the method's state; - you want to **document** that a temporary belongs only to the block. Most teams prefer the simpler fix of extracting the block's body into a small method or choosing a distinct name, which avoids the question entirely. Knowing the syntax still matters for reading older code and for explaining how block scope works.

  • Does a block-local variable keep its value between two runs of the same block?
    No. Each run of the block gets fresh block-local variables that start as `nil`. If you need state that survives across runs, keep it in a variable defined outside the block and assign to that deliberately.
  • Why is proc { |; tmp| it } rejected by the parser?
    Any declaration between the pipes, even only block-locals, counts as an ordinary parameter list. `it` and numbered parameters are allowed only in blocks without ordinary parameters, so Ruby raises a `SyntaxError` saying an ordinary parameter is defined, unless a local variable named `it` is already in scope.

saying these in an interview costs you the question

  • A block can never modify a local variable defined outside it
  • Block-local variables keep their value between block runs
  • yield fills block-local variables after the semicolon
  • A block parameter named like an outer variable overwrites that variable
  • Block-local variables can be combined with it or _1