Last Updated: May 07, 2018
· jodosha

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

So that, to calculate the InvoiceValue total you'd use:

def total
  @entries.inject(0) {|result, e| result += e.subtotal }

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:

def coerce(other)
  [other, subtotal]

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, :+)

3 Responses
Add your response

Nice tip! It's also possible to use this syntax:


no need to pass the first value (0 in this case), because it's already used by default.

over 1 year ago ·

@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.

over 1 year ago ·

If you have to call a method to coerce it it is not implicit...

over 1 year ago ·