Assignment-like methods and the returned value (in 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 :
- you can’t chain assignent-like methods, so if you need such a thing, do it differently ;
- 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.