In a Ruby block DSL, what changes when the library runs your block with instance_eval instead of yielding a builder object to it?
answer
- who is self inside the block
- locals still visible, @ivars are not
- caller helpers: NameError
- builder style: r.threshold 90
- block.arity picks the style
basics
~20 sWith instance_eval, self inside the block becomes the DSL object, so bare calls like threshold 90 reach it, but the caller's instance variables and helper methods do not. Yielding a builder keeps self and makes calls explicit: r.threshold 90.
solid answer
~40 sA Ruby block DSL can run the user's block in two ways. **Receiver style**: the library calls `rule.instance_eval(&block)`, so `self` inside the block is the rule object and bare words like `threshold 90` are method calls on it — the cleanest syntax. The cost is that everything that used to resolve against the caller's `self` now resolves against the rule: `@oncall` reads the rule's (unset, so `nil`) instance variable, and a helper method of the caller raises `NameError`. Local variables still work, because a block is a closure. **Builder style**: the library does `yield rule`, the block takes a parameter, and calls are explicit (`r.threshold 90`); `self`, instance variables and helpers stay the caller's. A library can support both by checking `block.arity`: one parameter means builder style, none means `instance_eval`.
code
ruby · 18 linesclass Config
def initialize
@oncall = "ops"
end
def pager = "pager-team"
def build
Alerts.alert("cpu") { threshold 90; notify @oncall }
# instance_eval: @oncall is the rule's -> notify(nil)
Alerts.alert("cpu") { notify pager }
# NameError: undefined local variable or method 'pager'
Alerts.alert("cpu") { |r| r.notify @oncall }
# builder style: self is still Config -> notify("ops")
end
endgo deeper
Recall the two shapes: bare calls on a hidden receiver versus calls on a block parameter.
Explain that instance_eval switches self, so instance variables and helper methods resolve against the DSL object while locals still work.
Diagnose the silent nil and the confusing NameError in user code, and design the arity-based hybrid plus final validation.
Choose the style from who writes the blocks and where, and weigh cleaner syntax against the support cost of surprising self.
## Two shapes for the same DSL Imagine a monitoring tool whose users declare alert rules in Ruby: ```ruby Alerts.alert "cpu_high" do threshold 90 notify "ops" end ``` The library creates an `AlertRule` object and must let the block configure it. It has two options. 1. **Receiver style (evaluate on the object).** `rule.instance_eval(&block)` runs the block with `self` switched to `rule`. A bare `threshold 90` is a call to `rule.threshold(90)`. 2. **Builder style (yield the object).** `yield rule` passes the rule in as a block parameter, and the user writes `r.threshold 90`. `self` inside the block is whatever it was where the block was written. ## What the block can see | Inside the block | Receiver style (`instance_eval`) | Builder style (`yield`) | |---|---|---| | Local variables of the caller | visible (closure) | visible (closure) | | Caller's `@instance_variables` | **replaced** by the rule's, usually `nil` | visible | | Caller's helper methods | **not reachable**: `NameError` or `NoMethodError` | reachable | | DSL methods | bare: `threshold 90` | explicit: `r.threshold 90` | | Private methods of the rule | callable | not callable without `send` | The first row surprises people least; the next two cause most bugs. ## The two classic failures - **Silent `nil`.** A method on a `Config` object sets `@oncall = "ops"` and then writes `notify @oncall` inside the block. Under `instance_eval`, `@oncall` is looked up on the rule, which has none, so `notify` receives `nil` and no error is raised. - **A confusing `NameError`.** `notify pager`, where `pager` is a helper method of `Config`, fails with `undefined local variable or method 'pager'` for an instance of `AlertRule` — the message names the DSL object, not the user's class, so the user cannot see why. The standard workaround is to copy what is needed into a local variable before the block (`who = pager`), because locals are captured by the closure. ## Supporting both styles A library can let the user choose by looking at the block's parameters: ```ruby def self.alert(name, &block) rule = AlertRule.new(name) block.arity == 1 ? yield(rule) : rule.instance_eval(&block) rule end ``` `proc { }.arity` is `0` and `proc { |r| }.arity` is `1`, so users who need their own `self` add a parameter and get builder style. ## Choosing - **Receiver style** suits short, declarative files owned by the DSL, such as a rules file loaded by the tool, where the block rarely needs the caller's state. - **Builder style** suits DSLs used inside application objects, where blocks call helpers and read instance variables. - **Offer both** when users are mixed, and document the arity rule. - Whichever you pick, **fail loudly**: validate the finished rule and raise if required settings are missing, so a silent `nil` becomes a clear error. ## Debugging a receiver-style block When a block in a receiver-style DSL misbehaves, a few checks settle it quickly: - Print `self` inside the block: it shows the DSL object, which explains every missing instance variable. - Read the class named in a `NameError`: if it is the DSL's class rather than yours, the call was resolved against the DSL object. - Grep the library for `instance_eval`, `instance_exec` or `yield` near where it takes the block, to learn which style it uses. - When the library must also pass values in, it can call `rule.instance_exec(context, &block)`, which switches `self` like `instance_eval` but hands arguments to the block (the evaluation methods themselves are their own topic). ## What the interviewer wants The expected answer names the `self` switch as the single cause, lists what the block loses (instance variables and methods of the caller) and keeps (locals), and knows the arity-based hybrid.
- Why does a local variable still work inside an instance_eval block when an instance variable does not?A block captures the local variables of the scope where it was written, and `instance_eval` does not change that binding of locals. Instance variables are looked up on `self`, which `instance_eval` switches to the DSL object, so they resolve against the wrong object.
- How can a library let users choose between the two styles?Check the block's arity: a block with one parameter is called with the builder (`yield(rule)`), a block with none is run with `rule.instance_eval(&block)`. Users who need their own helpers simply add a block parameter.
saying these in an interview costs you the question
- instance_eval also hides the caller's local variables from the block.
- Under instance_eval, the caller's @ivars stay visible inside the block.
- Yielding a builder changes self to the builder object.
- An unset @ivar read inside instance_eval raises NameError.