Ruby implicit coercion
Let's imagine this scenario: your billing system has an InvoiceValue
class which has n entries, one for each purchased product.
Your InvoiceValue::Entry
, exposes a method subtotal
which is the product of price and quantity.
def subtotal
price * quantity
end
So that, to calculate the InvoiceValue
total you'd use:
def total
@entries.inject(0) {|result, e| result += e.subtotal }
end
Which is a bit weird: if you imagine InvoiceValue::Entry
as a pure value, it should be easy to be used directly in the mathematical expressions, such as +=
.
The first thing you're tempted to do is to override Numeric#+
, in order to handle this special case, but this pollutes a lot the behavior of that method.
Luckily, Ruby is such a beautiful language, which allows implicit coercion between types. All you need to to is to implement a method:
protected
def coerce(other)
[other, subtotal]
end
There are a few things that need an explanation. First of all, #coerce
requires to return an array with two elements: the object received as argument and the coerced value of the same type.
The other
here represents the other object we're trying to cast against (in this case a Fixnum
) and subtotal
is the value we want to use in the arithmetic expressions.
This leads to simplify a lot our implementation:
def total
@entries.inject(0, :+)
end
Written by Luca Guidi
Related protips
3 Responses
Nice tip! It's also possible to use this syntax:
@entries.inject(:+)
no need to pass the first value (0 in this case), because it's already used by default.
@apecox In this case, if you omit 0
, you're forced to implement #+
in InvoiceValue::Entry
. This because the loop don't have an initial value and tries to call #+
on all the entries.
Instead, with that explicit argument the VM tries to do 0.+(entry)
, that means a Fixnum
with an InvoiceValue::Entry
so asks to the last one to coerce itself, and the trick works.
So, there is a win in having that 0
.
If you have to call a method to coerce it it is not implicit...