skip to content

In Ruby, why must a LibraryLoan method write self.due_date = day instead of due_date = day to call its due_date= setter?

level: middleimportance: should knowfreq 48%

answer

  1. = at the end of the name
  2. no receiver means local variable
  3. self works for private setters
  4. assignment returns the right side
  5. no endless setters

basics

~20 s

Inside a method, name = value without a receiver always assigns a local variable, so the due_date= setter never runs. An explicit receiver, self.due_date = day, makes Ruby call the method, and self works even when the setter is private.

solid answer

~40 s

A method whose name ends in `=`, such as `due_date=`, is a **setter**: `loan.due_date = day` calls it with `day`. The parser treats `x = value` as a method call only when there is an **explicit receiver**; a bare `due_date = day` inside another method is always **local-variable assignment**, so the setter is silently skipped and `@due_date` keeps its old value. Writing `self.due_date = day` fixes it, and it works even when the setter is `private`, because private methods may be called with a literal `self` receiver. Two more rules follow from setters being assignment syntax: the expression always evaluates to the **right-hand side**, whatever the setter returns, and a setter cannot be defined as an endless method (`def due_date=(day) = ...` is a `SyntaxError`).

code

ruby · 27 lines
ruby
class LibraryLoan
  attr_reader :due_date

  def initialize(due_date)
    @due_date = due_date
  end

  def extend_broken
    due_date = @due_date + 14   # local variable; setter skipped
  end

  def extend_fixed
    self.due_date = @due_date + 14
  end

  private

  def due_date=(day)
    @due_date = day
  end
end

loan = LibraryLoan.new(10)
loan.extend_broken
p loan.due_date # => 10
loan.extend_fixed
p loan.due_date # => 24

go deeper

for a junior

Remember that a setter is a method ending in = and that inside the class you call it as self.name = value.

for a middle

Explain the parser rule that a receiver-less assignment is always a local, why self works for private setters, and that assignment returns the right-hand side.

for a senior

Recognise the silent bug in review, since nothing raises, and keep setters free of side effects callers would need to check.

for a principal

Weigh setters against explicit command methods in public APIs, favouring names that make side effects and failure modes visible.

## Setters are ordinary methods with = in the name Ruby lets a method name end in `=`. Such a method is called a **setter** (or assignment method), and Ruby lets you call it with assignment syntax: ```ruby class LibraryLoan def due_date=(day) @due_date = day end end loan = LibraryLoan.new loan.due_date = 20 # calls due_date=(20) ``` The space before `=` is allowed; `loan.due_date = 20` and `loan.due_date=(20)` call the same method. ## Why due_date = day never calls the setter The parser must decide what `due_date = day` means **without an explicit receiver**. Its rule is fixed: a bare `name = value` is always a **local variable assignment**. 1. Inside `extend_loan`, the line `due_date = @due_date + 14` creates a local variable named `due_date`. 2. The local disappears when the method returns. 3. The setter never runs, so `@due_date` keeps its old value, and no error or warning points at the bug. The fix is to give the call a receiver: `self.due_date = @due_date + 14`. ## Receivers and visibility | Call form | Public `due_date=` | Private `due_date=` | |---|---|---| | `loan.due_date = 20` from outside | calls the setter | `NoMethodError` (private method called) | | `self.due_date = 20` inside the class | calls the setter | calls the setter | | `due_date = 20` inside the class | assigns a local variable | assigns a local variable | Private methods may be called either without a receiver or with the **literal** `self` as receiver. That exception is what keeps private setters usable: without a receiver they would be parsed as locals. ## The value of a setter call An assignment expression **always evaluates to the right-hand side**. Even if the setter returns something else, the caller sees the assigned value: - `def due_date=(day); @due_date = day; :ok; end` still makes `loan.due_date = 7` evaluate to `7`. - Returning a status from a setter is therefore pointless; use a normal method, such as `renew`, when the caller needs a result. ## Other rules and conventions - **No endless setters.** `def due_date=(day) = @due_date = day` fails with `invalid method name; a setter method cannot be defined in an endless method definition`. - **Compound assignment calls both methods.** `loan.renewals += 1` calls `renewals` to read the value, then `renewals=` with the sum. - **Multiple assignment works with setters.** `loan.due_date, loan.renewals = 30, 0` calls `due_date=` and then `renewals=`. - **Name setters after the attribute.** `due_date=` sets a due date; an action with side effects, such as extending a loan, reads better as a verb (`renew`, `renew!`) than as a setter. ## Finding the silent bug Because nothing raises, the symptom is only that the value does not change. Three checks find it quickly: 1. **Run with `ruby -w`.** When the stray local is never read, the parser warns `assigned but unused variable - due_date` at that line. 2. **Search for receiver-less assignments** whose name matches a setter in the class; each one should read `self.name =`. 3. **Watch for shadowing.** After `due_date = ...`, a later bare `due_date` in the same method reads the local, not the reader method, so even the code that checks the value can be fooled. A test that calls the method and then reads the attribute through its public reader catches the bug where reading the code does not. ## How to answer in an interview Explain the parser rule first (no receiver means local variable), show the silent failure, give the `self.` fix, and add that it works for private setters. Mentioning that assignment returns the right-hand side shows you know setters are syntax, not ordinary calls.

  • What does loan.renewals += 1 call?
    It expands to `loan.renewals = loan.renewals + 1`, so it calls the reader `renewals`, adds 1, and passes the sum to the setter `renewals=`. Both methods must exist, and inside the class the same rule applies: `self.renewals += 1` calls the methods, while a bare `renewals += 1` works on a local variable.
  • Why can a private setter be called as self.due_date = day when other private methods reject an explicit receiver?
    Ruby allows a private method to be called with the literal `self` as its receiver. Without that allowance, a private setter would be unusable: the only receiver-less form, `due_date = day`, is parsed as a local variable assignment. Any receiver other than the literal `self`, such as a variable that happens to hold the same object, still raises `NoMethodError`.

saying these in an interview costs you the question

  • due_date = day inside the class calls the setter because the method exists
  • self.due_date = day raises NoMethodError when the setter is private
  • loan.due_date = day evaluates to whatever the setter method returns
  • A setter can be written as a one-line endless method like any other
  • self. on the left of = is needed only when the setter is private