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…