Ruby Tips: Numeric Classes
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…
Share your thoughts with @engineyard on Twitter