In Ruby, why does p a if a = lookup raise NameError when a is new, while x = 1 if false leaves x as nil?
answer
- parser, not runtime, decides
- left to right source order
- assignment seen means local
- body parsed before the test
- parenthesise assignment in a test
basics
~20 sRuby decides at parse time, in source order, whether a bare name is a local. In p a if a = lookup the body is parsed first, so a is a method call; x = 1 if false still declares x as nil.
solid answer
~50 sRuby's parser, not the running program, decides whether a bare identifier is a local variable or a method call: once it has *parsed* an assignment to a name, later occurrences are locals. A modifier is read left to right, so in `p a if a = lookup` the `a` in `p a` is parsed before the assignment in the test and becomes a method call; at runtime the test runs first and assigns the local, then `p a` calls a nonexistent method `a` and raises `NameError` (undefined local variable or method). Conversely, `x = 1 if false` never executes the assignment, but the parser has seen `x =`, so `x` is a local initialised to `nil`. Fix the first case with the block form `if (a = lookup) then p a end` or by assigning on its own line.
code
ruby · 12 linesPERMITS = { "AB-123" => "zone A" }.freeze
def lookup(plate) = PERMITS[plate]
# p permit if permit = lookup("AB-123")
# => NameError: undefined local variable or method 'permit'
if (permit = lookup("AB-123"))
p permit # => "zone A"
end
spare = lookup("ZZ-999") if false
p spare # => nil (declared by the parser)go deeper
Recall that a skipped assignment such as x = 1 if false still leaves x defined as nil, and that the body of a modifier cannot use a variable first assigned in its own test.
Explain that the parser classifies locals in source order, independent of execution, and trace p a if a = lookup to its NameError step by step.
Spot the assign-in-modifier-test pattern in review, explain why automatic modifier rewrites can break them, and use parenthesised assignment in conditions deliberately.
Use this as an example of why autocorrecting style cops need exceptions: some block-to-modifier rewrites change semantics, so rollout of such cops needs care and test coverage.
## Locals are decided by the parser In Ruby, a bare identifier such as `a` could be a **local variable** or a **call to a method** named `a` with no arguments. The language cannot tell from the name alone, so the **parser** decides, using a simple rule documented in Ruby's language reference: once the parser has encountered an assignment to a name, later uses of that name in the same scope are local variables. Crucially: - the decision happens **while parsing**, reading the source left to right; - it does **not** depend on whether the assignment ever **executes**; - a local that was declared by the parser but never assigned at runtime reads as `nil`. The reference gives this exact example: ```ruby a = 0 if false # does not assign to a p local_variables # prints [:a] p a # prints nil ``` ## Case 1: the modifier body is parsed before its test A statement modifier is **written** body-first but **executed** test-first. Those two orders disagree, and the parser follows the written one: ```ruby p a if a = 0.zero? # NameError: undefined local variable or method 'a' ``` Step by step: 1. The parser reads `p a`. No assignment to `a` has been seen yet, so this `a` is recorded as a **method call**. 2. The parser reads the test `a = 0.zero?` and now marks `a` as a local — too late for the body. 3. At runtime the test runs first, assigning `true` to the local `a`. 4. The test is truthy, so `p a` runs — and calls the method `a`, which does not exist: `NameError`. The Ruby reference walks through this sequence and notes that the same is true for `unless`. The error message in Ruby 3.4 and later quotes the name with plain single quotes, `'a'`; earlier releases opened the quote with a backtick. The block form does not have this problem, because the test is **written** first: ```ruby if (a = lookup_permit(plate)) p a end ``` ## Case 2: an assignment that never runs still declares the local ```ruby x = 1 if false p x # => nil, not NameError ``` The parser saw `x = 1`, so `x` is a local from that point on. At runtime the modifier skips the assignment, and an unassigned local reads as `nil`. This is why `x` "exists" even though no line ever assigned to it. This also means a block `if`/`unless` and its modifier form are **not always interchangeable**. The Ruby reference says so explicitly — "they are not exact transformations of each other due to parse order" — and RuboCop's `Style/IfUnlessModifier` documents cases it deliberately leaves in block form for that reason, such as a block whose test inspects a variable that the body assigns. ## Assignment inside a condition Assigning in the test (`if a = lookup`) is legal and common for "fetch and check" code, but it is easy to mistake for `==`: | Code | What happens | |---|---| | `if a = 0` | Ruby warns `found '= literal' in conditional, should be ==` (the warning fires when the right side is a literal) | | `if a = lookup` | no warning; the value assigned is the test | | `if (a = lookup)` | same behaviour; the parentheses signal intent | RuboCop's `Lint/AssignmentInCondition` flags assignments in conditions; with its default `AllowSafeAssignment: true`, the parenthesised form is accepted as deliberate. ## How to avoid both traps - Never rely on a modifier's test to introduce a variable that its body uses; assign on the line before, or use the block form. - Remember that "declared" and "assigned" are different: a skipped assignment still creates a `nil` local. - Treat block-to-modifier rewrites (by hand or by autocorrect) as a change that can alter which names are locals. - Parenthesise intentional assignments in conditions. The interview-grade explanation fits in one sentence: Ruby decides local variables at parse time in source order, so a modifier's body cannot see a local first assigned in its own test, and an assignment that is parsed but skipped still leaves a `nil` local behind.
- How do you rewrite `p a if a = lookup` so it works?Put the assignment where the parser meets it before the use: the block form `if (a = lookup) then p a end`, spread over lines in real code, or assign on its own line first and then write `p a if a`. In both, the parser records `a` as a local before it reads `p a`.
- Why does Ruby warn on `if a = 0` but not on `if a = lookup`?The parser warns `found '= literal' in conditional, should be ==` only when the right-hand side is a literal, because assigning a constant value in a test is almost always a typo for `==`. Assigning a method's result is a common fetch-and-test idiom, so it is left alone; RuboCop's `Lint/AssignmentInCondition` still flags it unless you wrap it in parentheses.
saying these in an interview costs you the question
- Ruby creates a local variable only when an assignment to it actually executes.
- Because the test runs first, the body of p a if a = lookup can see a.
- x = 1 if false; p x raises NameError because x was never assigned.
- A modifier and the equivalent block if are always interchangeable.
- Ruby warns about every assignment used as a condition.