In Ruby, why does `temp.celsius = 30.46` evaluate to 30.46 even when the celsius= method rounds and returns 30.5?
answer
- assignment syntax, not a plain call
- right-hand side is the result
- send and public_send see the real value
- []= follows the same rule
- Lint/ReturnInVoidContext
basics
~10 sRuby's assignment syntax, recv.name = value or recv[k] = value, always evaluates to the right-hand value and discards what the name= method returned. Calling it through public_send(:celsius=, 30.46) returns the method's own value.
solid answer
~40 s`temp.celsius = 30.46` is **assignment syntax**: Ruby calls `celsius=` with `30.46`, throws the method's return value away, and makes the whole expression evaluate to the **right-hand side**, just as `x = 30.46` does. The same holds for `temp[:c] = v` with `[]=`, for `temp.celsius=(30.46)` written with parentheses, and for `temp.celsius += 1`, which evaluates to the computed sum. Only a plain method call such as `temp.public_send(:celsius=, 30.46)` or `send` returns what the method returned. So a setter cannot report a normalised value through its return: callers read it back with the getter. RuboCop's `Lint/ReturnInVoidContext` flags `return value` inside a setter for that reason.
code
ruby · 17 linesclass Temperature
def initialize
@readings = {}
end
def []=(scale, value)
@readings[scale] = value.round(1)
:stored
end
def [](scale) = @readings[scale]
end
temp = Temperature.new
p(temp[:c] = 21.04) # prints 21.04
p temp[:c] # prints 21.0
p temp.public_send(:[]=, :c, 21.04) # prints :storedgo deeper
Remember that obj.attr = value always evaluates to value, even if the attr= method returns something else.
Explain which forms count as assignment syntax (name=, []=, op-assign, parenthesised argument) and why send and public_send behave differently.
Design writers as side-effect methods: raise on invalid input, expose normalised values through readers, and keep Lint/ReturnInVoidContext on so dead returns do not mislead reviewers.
Decide when a domain object should expose writers at all versus methods with ordinary names that can return results, and set that expectation across the team's models.
## Setters are methods, assignment is syntax A **setter** (also called a writer or attribute assignment method) is an ordinary method whose name ends in `=`, such as `celsius=`. When you write `temp.celsius = 30.46`, the parser turns it into an **attribute assignment**: it calls `temp.celsius=(30.46)` and then gives the whole expression the value of the **right-hand side**. Whatever the method returned is discarded. Ruby's own syntax documentation states it directly: for assignment methods, the return value is ignored when using the assignment syntax and the argument is returned instead. This keeps `a = temp.celsius = 30.46` consistent with `a = b = 30.46`: the value flowing leftwards is always the one that was written on the right. ## Where the rule applies, and where it does not | Expression | Value of the expression | |---|---| | `temp.celsius = 30.46` | `30.46` (right-hand side) | | `temp.celsius=(30.46)` | `30.46`, still assignment syntax | | `temp[:c] = 30.46` (calls `[]=`) | `30.46` | | `temp.celsius += 1.26` | the computed sum passed to `celsius=` | | `temp.public_send(:celsius=, 30.46)` | what `celsius=` returned | | `temp.send(:celsius=, 30.46)` | what `celsius=` returned | The last two rows are ordinary method calls: no assignment syntax is involved, so the normal rule applies and the method's last evaluated expression comes back. ## The rounding example traced ```ruby class Temperature attr_reader :celsius def celsius=(value) @celsius = value.round(1) end end temp = Temperature.new p(temp.celsius = 30.46) # prints 30.46 p temp.celsius # prints 30.5 p temp.public_send(:celsius=, 30.46) # prints 30.5 ``` The setter's last expression is the assignment `@celsius = value.round(1)`, so the method itself returns `30.5`. Only the `public_send` line sees that value; the assignment-syntax line reports what the caller wrote. ## Why the language works this way Assignment in Ruby is an expression, and chained assignment only stays predictable if every link yields the same value: - `a = b = 30.46` gives both variables `30.46`. - `a = temp.celsius = 30.46` gives `a` the value `30.46` as well, even though a method ran in the middle. - A condition such as `if (temp.celsius = parse(input))` tests the value that was parsed, not a normalised value or a status from the writer. If the writer's return value leaked through, the same line would mean different things depending on how each class implemented its writer, and hand-written writers would behave differently from the ones `attr_writer` generates. Discarding the return value makes the expression's meaning depend only on the syntax at the call site, which is what readers of the calling code can see. ## Consequences for design 1. **Do not communicate through a setter's return value.** Normalisation, clamping or validation results must be read back with the getter, or exposed through a method with an ordinary name such as `update_celsius(value)` that can return something meaningful. 2. **Do not write `return value` in a setter.** It has no effect under assignment syntax and misleads readers. RuboCop's `Lint/ReturnInVoidContext`, enabled by default, reports a `return` with a value inside a setter or `initialize`; a bare `return` for an early exit is allowed. 3. **Raise instead of returning a status.** A setter that rejects `-300` cannot say so through `false`, because the caller never sees it; raising `ArgumentError` is the channel that works. 4. **Metaprogramming callers see the real value.** Code that calls writers through `public_send` gets the method's return value, so a writer that returns something surprising can still leak it there. ## Setters and the endless def form Ruby 3.0 added one-line endless definitions such as `def fahrenheit = celsius * 9 / 5.0 + 32`, but a setter cannot be written that way. `def celsius=(value) = @celsius = value` is a `SyntaxError` whose message says a setter method cannot be defined in an endless method definition. The restriction avoids a line that reads like a chain of assignments, and it fits the rule above: a setter's value is not what callers see anyway. ## Key points - Assignment syntax evaluates to the right-hand side, whatever the `name=` or `[]=` method returns. - Parentheses around the argument do not turn it into a plain call. - `send` and `public_send` return the method's own value. - Treat setters as side-effect methods; read results back through a getter.
- In Ruby, what does `temp.celsius += 1.26` evaluate to when celsius was 1.0 and the setter rounds to one decimal?It evaluates to `2.26`. Ruby reads `temp.celsius`, calls `+` with `1.26`, passes `2.26` to `celsius=`, and the expression's value is that computed right-hand side. The setter stores `2.3`, which only a later `temp.celsius` shows.
- How should a Ruby setter report that it rejected a value such as -300 degrees Celsius?By raising, typically `ArgumentError` with a clear message. Returning `false` or `nil` is invisible to callers using assignment syntax, because the expression evaluates to the value they passed. A method with an ordinary name can return a status if one is really needed.
saying these in an interview costs you the question
- temp.celsius = 30.46 evaluates to whatever celsius= returns.
- Writing temp.celsius=(30.46) with parentheses makes it a normal call that returns the method's value.
- A setter can signal failure by returning false.
- return value inside a setter changes what the assignment evaluates to.
- public_send(:celsius=, v) also returns the right-hand value.