CodeGnome CodeGnome - 1 month ago 9
Ruby Question

Why is a Ruby setter method returning a String rather than a Symbol as the last expression evaluated?

Unexpected Return Value from Method: Expecting Symbol



I have the following Ticket class:

class Ticket
VALID_STATES = %i[open closed invalid wontfix]
attr_reader :status
def status= new_state
new_state = new_state.to_sym
@status = new_state
end
end


When passed a String rather than a Symbol, the setter method unexpectedly returns a String, even though the correct value is being returned by the getter method. For example:

t = Ticket.new
t.status = 'closed'
#=> "closed"

t.status
#=> :closed


It looks like the correct value is being stored as a Symbol, but I'm at a loss as to why the method is returning
"closed"
at the REPL when the last expression evaluated should return
:closed
. My expectation is that the expression in question ought to resolve as
@status = :closed
, and therefore should return a Symbol.

Can anyone explain why I'm getting a String rather than a Symbol as the return value from the setter method?

Caveats and Bike-Shedding Prevention




  1. I know this example could just use
    @status = new_state.to_sym
    rather than assigning back to new_state, but there's intermediate code that was removed to create this minimal example. I didn't want to change the code too much, as that defeats the point of showing what my real code is doing. It doesn't seem to make a difference for this specific problem anyway; I've tried it both ways.

  2. I tried this with Ruby 2.3.1, 2.4.0-preview2, and JRuby 9.1.4.0, so it's not version-specific.

  3. Various debugging attempts ran afoul of other issues specific to the REPL's toplevel in both Pry and IRB, which I will open as a separate question. The point here is that trying to debug with alternative abstractions like
    def foo=(str); @foo = str.to_sym; end
    leads further down the rabbit hole.

  4. It's extremely possible that the problem exists between the keyboard and the chair, but the focus of the question is really about why the return value is not of the expected class.


Answer

It's expected. From the documentation:

Note that for assignment methods the return value will always be ignored. Instead, the argument will be returned:

def a=(value)
  return 1 + value
end

p(a = 5) # prints 5

Ruby allows you to chain assignments:

foo = bar = 'closed'

The above assigns "closed" to both, foo and bar.

Returning the argument and ignoring the method's return value lets you replace bar with a method call:

foo = t.status = 'closed'

IMO it would be quite surprising if the above would assign :closed to foo.

If you really want the return value, use send or public_send:

def a=(value)
  return 1 + value
end

p(a = 5)        # prints 5
p(send(:a=, 5)) # prints 6