In Ruby, when a library runs your configuration block through builder.instance_eval, why do your instance variables and helper methods stop working inside it?
answer
- self moves, the closure does not
- @ivars follow self
- bare calls resolve on the receiver
- locals still visible
- instance_exec passes arguments in
basics
~20 sinstance_eval sets self to the builder while the block runs, so @ivars and bare method calls resolve against the builder, not your object. Local variables still work because the block stays a closure over its scope.
solid answer
~40 s`BasicObject#instance_eval` runs the block with `self` set to the receiver. Everything that depends on `self` moves with it: `@title` reads the builder's instance variable (or `nil` if the builder never set one), a bare call such as `month_label` is looked up on the builder, so your own private helper raises `NameError`, and the builder's private methods become callable without a receiver. Local variables do not depend on `self`: the block is still a closure, so a local assigned above the block is visible inside it. The usual fixes are to copy what you need into a local before the block, or for the library to call `instance_exec(value, &block)` so the data arrives as a block parameter.
code
ruby · 24 linesclass ReportBuilder
def initialize = (@title = "Untitled")
def title(value = nil) = value ? (@title = value) : @title
def self.build(&config) = new.tap { |b| b.instance_eval(&config) }
end
class MonthlyReport
def initialize = (@name = "March sales")
def call
name = @name # copy into a local first
ReportBuilder.build do
p @name # => nil: @name is looked up on the builder
title name # the local is still visible
# month_label # NameError: looked up on the builder
end
end
private def month_label = "March"
end
MonthlyReport.new.call.title # => "March sales"go deeper
Recall that instance_eval changes self for the block: @variables and bare method calls now mean the builder's, while local variables above the block still work.
Explain why locals survive (the parser binds them, the closure carries them) while @ivars and receiverless calls follow self, and show instance_exec passing data in as block parameters.
Diagnose a configuration block that silently reads nil: spot the @ivar resolved on the builder, fix it with a local or instance_exec, and note the lambda-arity and return-value traps.
Weigh how much a team's configuration blocks should rely on self switching, since every reader must know which names follow self and which follow the closure.
## What instance_eval changes `instance_eval` is defined on `BasicObject`, so every Ruby object has it. Given a block, it runs that block with **`self` set to the receiver**. A configuration method written as `builder.instance_eval(&config)` therefore turns your block into code that runs *as if it were inside the builder*. That is what makes a terse configuration block possible: `title "March sales"` inside the block is a call to `builder.title`, with no receiver written. Everything in Ruby that is resolved against `self` moves to the builder for the duration of the block: - **Instance variables.** `@title` means "the `@title` of `self`", so it now reads or writes the builder's variable. If the builder never assigned one, it reads `nil`. Your own object's `@title` is simply not reachable by that name. - **Bare method calls.** A call with no explicit receiver, such as `month_label`, is sent to `self`. If the builder has no such method, Ruby raises `NameError` (for a bare name that could have been a local) or `NoMethodError` (for a call with arguments or parentheses). - **Private methods.** Because the calls are receiverless calls on the builder, the builder's *private* methods are callable too. The rdoc for `instance_eval` states it directly: the code gets access to the receiver's instance variables and private methods. ## What stays the same The block is still a **closure**: it keeps the local variables of the scope where it was written. Locals are resolved by the parser, not through `self`, so switching `self` does not touch them. | Written inside the block | Resolved against | Result in the example below | |---|---|---| | `@name` | the builder (`self`) | `nil` — the builder has no `@name` | | `title name` | `title` on the builder, `name` as a local | the local's value is passed in | | `month_label` | the builder (`self`) | `NameError` | | `name` alone | the enclosing method's locals | `"March sales"` | One subtle consequence: if you assigned a local called `limit` before the block, `limit` inside the block is that local, even when the builder also has a `limit` method. The parser saw the assignment first, and a local always wins over a bare method name. ## Getting your data into the block 1. **Copy it into a local first.** `name = @name` above the block is the simplest fix; the closure carries the value in. 2. **Pass it as an argument.** `instance_exec` is `instance_eval` with arguments: `builder.instance_exec(report, &config)` runs the block with `self` set to the builder and `report` as its first block parameter. 3. **Call your own object explicitly.** Capture `me = self` in a local and call `me.month_label`; public methods are then reachable. Private ones still are not, because an explicit receiver is used. Whether a library should evaluate the block on the builder at all, or yield the builder as a block argument so that `self` never moves, is a design choice made by the library author, not something the caller can change from inside the block. ## Two details of the call itself - `instance_eval` passes the **receiver as the block's only argument**, so `builder.instance_eval { |b| b.equal?(self) }` returns `true`. A plain block ignores arguments it does not declare, but a **lambda** checks arity strictly: `obj.instance_eval(&-> { self })` raises `ArgumentError` (given 1, expected 0). `instance_exec` passes only the arguments you give it, so the same lambda works there. - `instance_eval` **returns the value of the block's last expression**, not the receiver. A `build` method therefore returns the builder explicitly, on its own line or through `tap`. ## Mistakes to avoid - Expecting `@ivars` from the calling object to be visible inside the block, or to be copied onto the builder. - Calling a private helper of your own class from the block and being surprised by `NameError`. - Believing local variables disappear inside the block; they are the reliable channel. - Relying on the return value of `instance_eval` to be the builder. - Passing a zero-parameter lambda to `instance_eval`. ## The takeaway `instance_eval` moves **one** thing that matters here: `self`. Instance variables and receiverless calls follow `self`, so they switch to the builder; local variables follow the closure, so they stay. Knowing which of the two a name depends on tells you, without running the code, what a line inside a configuration block will read.
- Why does Object.new.instance_eval(&-> { self }) raise, while instance_exec with the same lambda works?`instance_eval` with a block always yields the receiver as one argument. A plain block ignores an argument it does not declare, but a lambda keeps strict arity, so a zero-parameter lambda raises `ArgumentError` (given 1, expected 0). `instance_exec` yields only the arguments you pass, zero here, so the lambda runs with `self` set to the object.
- What does builder.instance_eval { title "X" } return?The value of the block's last expression, here whatever `title "X"` returns (the string). It does not return the receiver, which is why a `build` method that uses `instance_eval` returns the builder explicitly, for example through `tap`.
saying these in an interview costs you the question
- instance_eval copies my object's instance variables onto the builder
- Local variables from the surrounding method are invisible inside the block
- The builder's private methods cannot be called inside the block
- instance_eval returns the receiver, so build can end on that call
- A zero-parameter lambda passed to instance_eval runs like a block