Inside a Ruby Thermostat method, why does target = 22 leave the target unchanged even though attr_accessor :target exists?
answer
- the parser decides, not the runtime
- bare name = value is a local
- writers need an explicit receiver
- self.target = 22
- target += 1 hits nil
basics
~10 sRuby parses a bare target = 22 as assignment to a new local variable, not a call to target=. Writer methods always need a receiver, so inside the class write self.target = 22.
solid answer
~40 sWhen the parser sees `name = value` with no receiver, it creates a local variable named `name`; it never considers a method called `name=`. So inside `Thermostat#boost`, `target = 22` sets a local that vanishes when the method returns, and `@target` is untouched. To call the writer, give it a receiver: `self.target = 22`. From that line on, a bare `target` in the same method also means the local, not the reader. `target += 1` is worse: it expands to `target = target + 1`, the parser has already made `target` a local, and it reads `nil`, raising `NoMethodError` for `+` on `nil`. Assigning `@target` directly works too, but it bypasses any validation a hand-written `target=` performs.
code
ruby · 15 linesclass Thermostat
attr_reader :target
def initialize = @target = 20
def target=(degrees)
raise ArgumentError, "out of range" unless (5..30).cover?(degrees)
@target = degrees
end
def boost_wrong = (target = 22) # local only
def boost_right = (self.target = 22) # runs the validating writer
def boost_bypass = (@target = 99) # skips validation
def bump = (target += 1) # NoMethodError: undefined method '+' for nil
endgo deeper
Recall the rule: inside the class, a writer needs self. in front, otherwise the assignment creates a local variable.
Explain that the parser, not the runtime, decides local versus method, why target += 1 raises NoMethodError on nil, and how a local shadows the reader for the rest of the method.
Show how you keep this out of production: tests asserting object state after each mutator, Lint/UselessAssignment in CI, and routing changes through validating writers rather than raw @target assignment.
Frame the choice between writers and direct instance variable assignment as a team convention that decides where validation lives and how much of it internal code may bypass.
## The silent failure A `Thermostat` class has a target temperature and a method meant to raise it: ```ruby class Thermostat attr_accessor :target def initialize = @target = 20 def boost target = 22 # looks like a writer call; it is not end end t = Thermostat.new t.boost t.target # => 20 ``` Nothing raises and the thermostat keeps its old target. The only hint comes at parse time: under `ruby -w`, Ruby prints `warning: assigned but unused variable - target` when nothing in the method reads the local afterwards. ## Why the parser picks a local Ruby decides what a bare identifier means **while parsing**, before any code runs: 1. `name = value` with no receiver is always **local variable assignment**. The parser creates the local `name` for the rest of the scope. 2. A bare `name` that has not been assigned earlier in the scope is treated as a **method call** on `self`. 3. A bare `name` after an assignment to it is treated as the **local variable**. The Ruby documentation on assignment states the rule directly: when using method assignment you must always have a receiver; without one, Ruby assumes you are assigning to a local variable. So `target = 22` creates a local and `self.target = 22` calls `target=`. ## Three ways to write it, three outcomes | Statement inside `boost` | What it does | |---|---| | `target = 22` | creates a local `target`; `@target` unchanged | | `self.target = 22` | calls `target=`, running any logic in it | | `@target = 22` | assigns the variable directly, skipping `target=` | The third form is fine when the writer is a plain generated one. When `target=` is hand-written to validate or convert, assigning `@target` directly sidesteps that logic, so the class can put itself into a state its own writer would reject. ## The += variant `target += 1` is shorthand for `target = target + 1`. Because the statement contains an assignment to `target`, the parser makes `target` a local for the whole statement, and the right-hand side reads that local, which is still `nil`: - The result is `NoMethodError` with `undefined method '+' for nil`. - The message mentions `nil`, not the missing receiver, which is why this one confuses people. - `self.target += 1` works: it calls the reader, adds one, and calls the writer. ## Shadowing after the mistake Once a method contains `target = something`, every later bare `target` in that method is the local, not the reader. Code below the assignment that expected to read the object's state reads the local instead, so one wrong line can make the rest of a method use a different value than the object holds. ## Reading does not need self The asymmetry trips people up: the **reader** works without a receiver, the **writer** does not. - `target` alone, with no local of that name in scope, calls the reader on `self`. - `self.target` also calls the reader; it is legal, just noisier. - `target = value` alone is never a method call, whatever methods the class has. The reason is that an assignment without a receiver must be able to introduce a local variable; if Ruby looked for a `name=` method first, adding such a method to a class would silently change the meaning of every local variable of that name in its methods. ## A quick diagnostic When a value set inside a method does not stick: 1. Look for a bare `name = value` where a writer was intended. 2. Check whether a later bare `name` in the same method reads that local instead of the reader. 3. Check for `name += ...` or `name ||= ...` without `self.`, which fail or misbehave for the same reason. ## Catching it - **Tests** that check the object's state after calling the method expose it at once. - **Warnings:** `ruby -w` reports `assigned but unused variable - target` for a local that is never read. - **Linters** flag an assignment to a local that is never read: RuboCop's `Lint/UselessAssignment` reports `target = 22` when nothing reads `target` afterwards. - **Habit:** inside a class, write `self.name = value` whenever you mean the writer, and reserve bare `name = value` for genuine locals.
- In Ruby, why does a bare target read the reader method in one method but the local variable in another?The parser decides per scope. If the method has assigned to `target` on an earlier line, even inside a branch that never ran, `target` is a local from there on and reads `nil` if unassigned. Otherwise a bare `target` is a method call on `self`, which reaches the reader.
- In Ruby, when is assigning @target directly inside Thermostat preferable to self.target = ?In `initialize` or internal code where the value is already known to be valid and the writer does nothing extra, direct assignment is simple and clear. When `target=` validates, converts or triggers side effects, go through `self.target =` so every change passes the same checks.
It is like writing a new thermostat setting on a sticky note in your own hand instead of pressing the button on the thermostat: the note says 22, the room stays at 20, and the note goes in the bin when you leave.
saying these in an interview costs you the question
- target = 22 inside the class calls the target= writer
- Ruby raises an error when a local shadows a writer method
- self. is only needed when the writer is public
- target += 1 increments the attribute through the reader and writer
- Assigning @target always runs the logic in target=