Have you ever had to implement arithmetic operations for an object? If so, there is one little method you probably should be aware of, and it’s called
As an example let’s imagine we are building a system that manages money transactions, and for that we want to have a
Money class to handle all the intricacies of dealing with different currencies, cents, etc…
To simplify let’s build a class that can only handle one currency (euros) so that we don’t have to care about conversions. In order to create an instance of such a class, we need only to pass in the amount in cents.
class Money attr_reader :amount_in_cents def initialize(amount_in_cents) @amount_in_cents = amount_in_cents.to_i end def amount amount_in_cents / 100.0 end def inspect "%.2f €" % [amount] end end
That’s enough for us to create an print
Money instances, but we cannot do anything else with them, like sum or multiply, which was kind of the point.
Ruby makes it extremely easy to reimplement seemingly core methods, such as
* . That’s just what we’ll do.
class Money ... def +(other) Money.new(amount_in_cents + other.amount_in_cents) end def -(other) Money.new(amount_in_cents - other.amount_in_cents) end def *(factor) Money.new(amount_in_cents * factor) end def /(dividend) Money.new(amount_in_cents / dividend) end end
Note : that the
- methods take an instance of
Money , but the
/ don’t. This will be important for a minute.
This is now a fully (kind of) working implementation of
Money , and here’s how you’d use it:
Money.new(100) + Money.new(3000) # => 31.00 € Money.new(100) * 3 # => 3.00 €
That’s great! At this point, you might be wondering what does
coerce have to do with this, we’ve implemented all of this without even thinking about it. That’s true, but what happens if we reverse the order of the operands in our example above? The commutative property of both the sum and the multiplication should apply, right?
Money.new(3000) + Money.new(100) # => 31.00 € 3 * Money.new(100) # => Money can't be coerced into Fixnum (TypeError)
Well, it turns out it doesn’t, and the error we get is that
Fixnum is trying to coerce the instance of
Money but can’t. How can we solve this?
Enter coercion, more specifically type coercion. As per the Wikipedia definition, type coercion is:
In computer science, type conversion, typecasting, and coercion are different ways of, implicitly or explicitly, changing an entity of one data type into another.
Plainly put it’s a way of taking a data type and converting it to another, usually when this is done implicitly by the compiler or interpreter of a language. If it is done explicitly by the programmer it’s usually referred to as conversion or casting.
Getting back to our example, what this means is that we need to let
Fixnum know how to interact with
Money to perform these operations. That is done by implementing the
class Money ... def coerce(num) [self, num] end end
In this situation,
num represents the
Fixnum and we’re telling it that in order to call
* , it need to reverse the order of the operands, so in reality when you call
3 * Money , it’s being interpreted as
Money * 3 , which we know works. So, if we re-run the examples everything should work.
Money.new(3000) + Money.new(100) # => 31.00 € 3 * Money.new(100) # => 3.00 €
Now that you know what it does, a little more on how it does it. In Ruby, the
coerce method should always return an array, in which the first element is an object that knows how to do the operation on the argument, and the second is the argument itself. For both the object and the argument, you can substitute them with equivalents that do know how to perform the operations. For instance, you can cast a
String into a
Fixnum in order to perform the operation.
Numeric objects Ruby calls for this coercion if it cannot do anything else to try and accomplish the operation. However, you can call for other classes to have
coerce implemented in order to interact with your class.
def *(arg) if (arg is not recognized) self_equiv, arg_equiv = arg.coerce(self) self_equiv * arg_equiv end end
There are a lot of examples of places where
coerce is used, in the Real World™:
This is probably not a problem you will face very often, but when you do, it’s a nice tool to have at hand and understand.
More Ruby Bits
If you’ve enjoyed this Ruby Bit you should really subscribe toour newsletter, where other Ruby Bits and more great articles are shared every week.