skip to content

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%

answer

  1. scope as an object
  2. receiver and source_location too
  3. missing name: NameError
  4. new name lives only in the Binding
  5. 4.0: implicit_parameter_get for _1 and it

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.

solid answer

~40 s

`binding` returns a `Binding` object for the current scope: its local variables, `self` (`Binding#receiver`) and `source_location`; `Proc#binding` returns the one a block or lambda closed over. `local_variable_get(:name)` reads a local and raises `NameError` if none exists, `local_variable_set(:name, value)` writes it, and `local_variable_defined?` tests it, all on the same variables the method uses, so setting an existing local changes what the method sees. Setting a name the method never assigned creates a variable that exists only in that `Binding`: the method's locals were fixed when it was compiled, so a bare reference to the new name is still a method call and raises `NameError`. A practical use is reading a keyword parameter named after a reserved word, such as `if:`. In Ruby 4.0 these methods reject numbered parameters; `implicit_parameter_get` reads `_1` and `it`.

code

ruby · 14 lines
ruby
def click_counter
  clicks = 0
  -> { clicks += 1 }
end

increment = click_counter
3.times { increment.call }

scope = increment.binding
scope.local_variable_get(:clicks)      # => 3
scope.local_variable_set(:clicks, 0)   # reset from outside
increment.call                         # => 1
scope.local_variable_defined?(:clicks) # => true
scope.local_variable_get(:missing)     # NameError

go deeper

for a junior

Recall that binding returns an object for the current scope, and local_variable_get reads a local from it by name.

for a middle

Explain that a Binding shares the live environment, that missing names raise NameError, and that Proc#binding is the scope a closure was written in.

for a senior

Explain why a name created by local_variable_set is invisible to compiled code, name the reserved-word keyword use case, and know the Ruby 4.0 implicit-parameter change.

for a principal

Argue where Binding access belongs, such as tooling and debuggers, and why application code should prefer explicit arguments over reaching into captured scope.

## What a Binding is A **`Binding`** is an object that stands for a scope: the local variables visible at one point in the code, the value of `self` there, and where that point is in the source. Two methods create one: - **`Kernel#binding`** returns the binding of the place it is called. - **`Proc#binding`** returns the binding a block, proc or lambda closed over, which is the scope it was written in. A binding does not copy values. It refers to the same environment the code runs in, so reading through it gives current values and writing through it changes them. ## The accessor methods | Method | Returns | When the name is not a local | |---|---|---| | `local_variables` | Array of Symbols | not applicable | | `local_variable_get(:name)` | the value | raises `NameError` | | `local_variable_set(:name, value)` | `value` | creates the name in the Binding only | | `local_variable_defined?(:name)` | `true` or `false` | returns `false` | | `receiver` | the `self` of that scope | not applicable | | `source_location` | `[path, line]` | not applicable | ## Existing names versus new names Ruby fixes each method's set of local variables when it compiles the method; the parser records every name assigned anywhere in its body. 1. **Existing local.** `local_variable_set(:clicks, 0)` writes the same slot the method reads, so the method sees `0` next time it uses `clicks`. 2. **Name assigned later in the method.** It is already in the local table with the value `nil`, so `local_variable_get` returns `nil` rather than raising. 3. **Brand-new name.** Ruby adds it to the binding's own environment. `local_variable_get` through that same binding returns it, but the method's compiled code has no slot for it: a bare `tax` there is parsed as a method call and raises `NameError`. The core documentation shows exactly this with `local_variable_set(:b, 3)` followed by `p b` raising `NameError`. The new name is added to a fresh environment attached to that `Binding` object, so it is reachable only through that object. The documentation also describes `local_variable_get(:x)` as the short version of `binding.eval("x")`: it reaches the same variable without compiling a string of code. ## Proc#binding and captured scope Because a lambda's binding is the scope it closed over, `Binding` makes a closure's hidden state reachable: - `increment.binding.local_variable_get(:clicks)` reads a counter lambda's captured count. - `local_variable_set(:clicks, 0)` resets it from outside, which the lambda then sees. - `local_variables` lists every local of the enclosing scope, including names the body never mentions. This is handy in a debugger or a test and a code smell in application code, where an explicit reset lambda or an object states intent better. ## Ruby 4.0 and implicit parameters Ruby 4.0 took numbered parameters (`_1` to `_9`) out of the ordinary-local part of the `Binding` API and added a separate API for them and `it`: - `local_variables` no longer lists numbered parameters. - `local_variable_get`, `local_variable_set` and `local_variable_defined?` raise `NameError` for them. - New methods `implicit_parameters`, `implicit_parameter_get` and `implicit_parameter_defined?` read `_1`, `_2` and `it` inside a block. ## Common misreadings - A `Binding` is not a snapshot: values read through it are the current ones, and later writes by the method are visible through it. - `local_variable_get` does not return `nil` for an unknown name; it raises `NameError`. - `Proc#binding` is the scope where the block was written, not the scope that calls it. - `binding` called in a different method returns that method's scope; ordinary code reaches a caller's locals only when the caller hands over a `Binding` or a block, and debugging hooks such as `TracePoint#binding` are the exception. ## When to reach for it - Reading a keyword parameter whose name is a reserved word, such as `def price_for(sku, if: true)`, where `if` cannot be referenced directly: `binding.local_variable_get(:if)`. - Tools that inspect or evaluate code in a caller's scope, such as consoles and debuggers. - Not for passing data between methods: explicit arguments and return values are clearer and faster.

  • What does binding.local_variable_get(:x) return if x is assigned only on a later line of the same method?
    It returns `nil`. The parser put `x` into the method's local table when it read the whole body, and the slot starts as `nil`, so the binding finds the name even though the assignment has not run yet. `local_variable_defined?(:x)` is `true` for the same reason.
  • How do you read the value of it or _1 through a Binding on Ruby 4.0?
    Use `binding.implicit_parameter_get(:it)` or `implicit_parameter_get(:_1)`, and `implicit_parameters` to list them. Ruby 4.0 made `local_variable_get` and friends reject numbered parameters and dropped them from `local_variables`.

saying these in an interview costs you the question

  • local_variable_set with a new name defines a local the method can then use
  • A Binding holds a snapshot copy of the locals' values
  • local_variable_get returns nil for a name that is not a local
  • Proc#binding returns the scope where the proc is called
  • binding.local_variable_get(:_1) reads a numbered parameter in Ruby 4.0