This article was originally included in the September issue of the Engine Yard Newsletter. To read more posts like this one, subscribe to the __Engine Yard Newsletter_._
In this series, Evan Phoenix, Rubinius creator and Ruby expert, presents tips and tricks to help you improve your knowledge of Ruby.
Ruby’s numeric classes form a full numeric tower, providing many kinds of representations of numbers and numerical representations. It contains at its core a very elegant pattern that allows classes to participate in the tower easily.
Lets say we want to add a new numeric class called Money, which contains the number of dollars and cents:
class Money def initialize(dollars, cents=0) @dollars = dollars @cents = cents end attr_reader :dollars, :cents end
Now, lets say we’d like to have Money be able to interact with all integers nicely, with an integer representing a number of whole dollars. It’s not too hard add a + method to do that:
class Money def +(other) case other when Money Money.new(@dollars + other.dollars, @cents + other.cents) when Integer Money.new(@dollars + other.to_i, @cents) else raise ArgumentError, "Unknown type!" end end end
but we’d also like to be able to do:
(allowance = Money.new(5)more = 1 + allowance)
Trying this straight away, you’ll receive a message about Money not being able to be coerced to a Fixnum. This gives you a hint as to how to allow Money to interact with Fixnum better. We need to teach Money how to interact with the rest of the numeric tower, which we do with just one method:
class Money def coerce(other) [self, Money.new(other.to_i)] end end
now we can do
more = 1 + allowance and we see that we get
Wonderful! Fixnum#+, seeing the argument isn’t a Fixnum, uses the coerce protocol. This is a simple double dispatch protocol, which gives the argument the ability to change the values being operated on, then call the original method again. We simply return an array of the new values to use, here we convert the argument to a Money object, and then + is called again on the first element in the Array, passing the second as the argument.
Lets say we’d like “1 + allowance” to return 6 instead. Easy!
class Money def coerce(other) [@dollars, other.to_i] end end
Now, for your homework, make Money work also with Floats! See you next time…