Assignment-like methods and the returned value (in Ruby)

Première publication : 2010-09-03
Tags : ruby

In Ruby, when you create a class with some instance variable, you usually want to have some ways to get or set the content of those variables, because you can’t acess them from outside the instance.

If you don’t know about attr_accessor you’d define a getter and a setter :

class Foo
  def initialize(name = nil)
    self.name = name
  end

  def name
    @name
  end

  def name=(value)
    @name = value
  end
end

But you can do this in a much more concise way, like this

class Foo
  attr_accessor :name

  def initialize(name = nil)
    self.name = name
  end
end

With both approches, you can do this :

foo = Foo.new('Jérémy Lecour')

foo.name
#=> "Jérémy Lecour"

foo.name = 'John Appleseed'
#=> "John Appleseed"

You can dig a little deeper and get to know attr_reader and attr_writer to make only the getter or only the setter methods. The official documentation is the first place to be.

Well, that’s very good, predictable… but as it happens, I needed to do something a little different and I’ve hit the wall.

I needed to apply some type casting on the values passed to the setter methods.

So I redefined the method for some attributes on my model. Let’s say I want to have my name capitalized :

class Foo
  attr_reader :name

  def initialize(name = nil)
    self.name = name
  end

  def name=(value)
    @name = value.upcase
  end
end

But I’ve seen something that I didn’t expect :

foo = Foo.new('Jérémy Lecour')

foo.name
#=> "JÉRÉMY LECOUR"

foo.name = 'John Appleseed'
#=> "John Appleseed"

That’s right, the setter method return the original value, not the implicit “return” that should (says me) return the capitalized verison of the parameter.

I’ve tried to change alittle bit and tried this :

class Foo
attr_reader :name

  def initialize(name = nil)
    self.set_name name
  end

  def set_name(value)
    @name = value.upcase
  end
end

foo = Foo.new('Jérémy Lecour')
foo.name
#=> "JÉRÉMY LECOUR"

OK, that’s weird, the name of the method make the method behave differently, even if the “content” of the method is exactly the same.

With a little enlightment from the #ruby-lang people on IRC, I’ve learnt the concept of RHS (Right-Hand Side).

If you define an “assignment-like” method (with an equal sign at the end), Ruby will execute the method when you call it, but will always return the supplied parameter and never the result of the method.

As a result you can’t chain those methods, but it’s actually a good thing. For example :

foo = Foo.new()
bar = foo.name = 'John Doe'

foo.name
#=> "JOHN DOE"

bar
#=> "John Doe"

And by only reading the chained assignments, the result would not be obvious.

The lesson here has 2 points :

  1. you can’t chain assignent-like methods, so if you need such a thing, do it differently ;
  2. it is not a bug, it is a feature, and actually a good one when you understand the stakes.

This has eaten at least 3 hours yesterday, so I thought it might benefit someone else.