In Ruby, why does x = 10 if false followed by p x print nil instead of raising NameError?
answer
- the parser, not the run
- local exists from the assignment line
- value nil until executed
- p a if a = ... still fails
- self.name calls the method
basics
~20 sRuby creates a local variable when the parser sees an assignment to it, not when the assignment runs. After x = 10 if false, x exists as a local for the rest of the scope and holds nil.
solid answer
~40 sA **local variable is created by the parser**: once it has seen `x = ...` in a scope, every later bare `x` in that scope is a local, whether or not the assignment executed. The modifier `if false` skips the assignment, so `x` exists with the value `nil`, and `local_variables` already lists `:x`. The same rule explains two traps. `p a if a = 0.zero?` raises `NameError`, because the parser reads the bare `a` on the left before it reaches the assignment and compiles it as a method call. And after `total = total()` inside a method, every later `total` means the local, so calling the method again needs `self.total` or `total()`.
code
ruby · 10 linesclass Invoice
def total = 120
def summary
total = total() * 2 # local now shadows the method
[total, self.total] # => [240, 120]
end
end
p Invoice.new.summarygo deeper
Remember that a skipped assignment still creates the local variable, which then holds nil.
Explain that the parser decides local-versus-method by source position, and use it to predict both the if false case and the modifier-if NameError.
Recognise the production symptom: a local set in one branch is nil in the other, and a local shadowing a method changes what later lines call.
Use the rule to justify review conventions such as not reusing method names for locals, which keeps code readable without tracing the parser.
## Locals are decided when the code is parsed Ruby must decide, for every bare lowercase name such as `x`, whether it is a **local variable** or a **method call with no arguments**. It decides once, while parsing, using a simple rule: if an assignment to that name has appeared earlier in the current scope, the name is a local; otherwise it is a method call. Because the decision is made by position in the source, not by what happens at run time: - the local exists from the assignment's line to the end of the scope; - before any assignment executes, the local holds `nil`; - a branch that never runs still creates the local. ```ruby x = 10 if false p local_variables # => [:x] p x # => nil ``` Nothing raises: the parser saw `x = 10`, so the later `x` is a local, and since the assignment was skipped its value is `nil`. ## The modifier-if trap runs the other way The same rule produces an error when the read appears **before** the assignment in the source text, even though it runs after it: ```ruby p a if a = 0.zero? # NameError: undefined local variable or method 'a' ``` 1. The parser reads `p a` first. No assignment to `a` has been seen yet, so `a` becomes a method call. 2. It then reads `a = 0.zero?`, which declares `a` as a local from that point on. 3. At run time the condition executes first and assigns `true` to the local. 4. Then `p a` runs, but it was compiled as a call to a method `a`, which does not exist, so Ruby raises `NameError`. The fix is to put the assignment on its own line, or to use a full `if a = 0.zero? ... end` block where the assignment comes first in the text. ## A local shadows a method with the same name | Code | What the later bare name means | |---|---| | no assignment to `total` in scope | a call to the method `total` | | `total = total()` earlier in the method | the local variable `total` | | `self.total` or `total()` anywhere | always the method | - Writing `total = total()` caches the method's result in a local of the same name; after that line, a bare `total` never calls the method again. - To reach the method once a local shadows it, use an explicit receiver (`self.total`) or empty parentheses (`total()`). - The shadowing lasts only to the end of the current scope; `def`, `class` and `module` start fresh. ## How this shows up in real code - A local assigned only in one branch of a conditional is still `nil` in the other branch, so later code may call methods on `nil` and raise `NoMethodError` far from the cause. - Code reviewers flag a local that shares a method's name, because a reader cannot tell which one a bare reference means without scanning upward. - The rule is also why `defined?(x = 1)` returns `"assignment"` without assigning, yet leaves `x` declared as a `nil` local afterwards. ## Checking what the parser decided Several tools confirm which names are locals at a given point: - `local_variables` returns an array of symbols for every local declared so far in the scope, including ones whose assignment was skipped. - `defined?(x)` returns `"local-variable"` once the parser has seen an assignment to `x`, and `"method"` or `nil` before that. - `binding.local_variable_defined?(:x)` answers the same question through a `Binding` object, which is handy inside a debugger session. None of these change the rule; they only let you observe it. ## How to answer in an interview State the rule in one line (locals are created by the parser when it sees an assignment), predict the output of the `if false` snippet (`nil`), and then show you know the mirror image: the modifier-if case where the read comes first in the text and raises `NameError`. Mentioning `self.name` or `name()` to reach a shadowed method rounds out the answer.
- What does p a if a = 0.zero? raise, and how do you fix it?It raises `NameError`, because the parser compiles the bare `a` in `p a` before it has seen the assignment, so that `a` is a method call. At run time the condition assigns the local first, but the compiled call still looks for a method. Put the assignment on its own line first, or use a full `if a = 0.zero?` ... `end` so the assignment precedes the read in the text.
- In a branch that never runs, what does a local assigned only in the other branch hold?It holds `nil`. The parser declared the local when it saw the assignment in the other branch, so later code in the same scope can read it, but no value was ever stored. That is a common source of `NoMethodError` on `nil` well after the conditional.
saying these in an interview costs you the question
- A local variable comes into existence only when its assignment line executes
- p x after x = 10 if false raises NameError
- Ruby hoists the assignment, so x holds 10 even when the branch is skipped
- Once total is a local, a bare total still calls the method of the same name