Are you an advanced rubyist?

7 points by Denzel ↗ HN
Try to do this by memory without using a ruby interpreter.

Given:

  class Test
    def attr
      @attr
    end

    def attr=(value)
      @attr = value * 2
    end
  end
Evaluate:

  t = Test.new
  t.attr = 5 # Returns => ?
  t.attr # Returns => ?
I don't consider myself an advanced rubyist, and I couldn't answer this until today. Nonetheless, the behavior surprised me. I've read a number of books about ruby and never seen this behavior mentioned; in fact, most deem a setter method as just another method. Any explanation for this behavior?

9 comments

[ 3.5 ms ] story [ 34.7 ms ] thread
(comment deleted)
(comment deleted)
It's related to the right-associativity of the assignment operator and its expected semantics. If you write

    a = b = c
then it is interpreted as

    a = (b = c)
When c = 5 then you expect that

    a = b = 5 # which is equivalent to a = (b = 5)
results in a and b set to 5. That's why = must return its right-hand side.
(comment deleted)
Agreed. I understand exactly what you're saying, and I don't dispute the semantics.

I guess my only problem is that in this case "t.attr" isn't really equal to 5, therefore making

  a = t.attr = 5
differ from

  t.attr = 5
  a = t.attr
which to a novice would appear equivalent. 99.9% of the time you wouldn't expect assignment to have side-effects. But in my eyes Ruby gives you so much power to use responsibly, it appears out of character to disallow it in this instance. (Note: I had to forgo a little trick with ActiveRecord that would've reduced duplication because of this.)

In the grand scheme of things, it's not a big deal. But it was very interesting to me. :)

The alternative is making

  a = t.attr = 5
different from

  t.attr = 5
  a = 5
which is most often what someone means when they write a chained assignment. It's all part of the "principle of least surprise" that Rubyists used to tout so much — but unfortunately, different people find different things surprising.
Please also note that t.send(:attr=, 5) returns 10.

When you do t.attr = x Ruby sends :attr= with x as the argument to t and then ignores the return value of the message and returns x instead.

It's not the method that's special, per se, but the `=` operator. The `=` operator always returns the right operand, discarding whatever is returned from the setter method. AFAIK it's because that's the expected behavior when you're doing chained assignments. You wouldn't normally want the leftmost operand in `a = b = c` to get a weird value just because the middle operand internally does some transformation, so the `=` operator yields the right operand unaltered.

If you went directly through the method (e.g. `t.send(:attr=, 5)` or `t.method(:attr=).call 5`), you'd get 10.

Thanks for the explanation chc, and grn. Indeed, invoking the method indirectly with send or method does produce 10. Nice!