In Ruby, how does Numeric#coerce let 6 * price work when price is a custom Money object, and what fails without it?
answer
- the left operand asks the right
- a two-element array comes back
- the operator is retried on the pair
- can't be coerced into Integer
- swapping is safe only when commutative
basics
~10 sInteger#* does not know Money, so it calls price.coerce(6), expects a two-element array [x, y], and evaluates x * y. Without coerce, Ruby raises TypeError: Money can't be coerced into Integer.
solid answer
~40 sNumeric operators such as `Integer#+` and `Integer#*` handle numeric operands themselves. For anything else they call `other.coerce(self)` and expect a two-element array `[x, y]`, then evaluate `x.op(y)`. So `6 * price` becomes `price.coerce(6)`, and if `Money#coerce(n)` returns `[self, n]`, Ruby evaluates `price * 6`. Swapping operands is correct only for commutative operators; for `-` or `/` the order changes the result, so there `coerce` should wrap the number or raise `TypeError`. Without `coerce`, Ruby raises `TypeError` with `Money can't be coerced into Integer`, the same mechanism behind `1 + nil` failing with `nil can't be coerced into Integer`. A `coerce` returning anything but a two-element array raises `coerce must return [x, y]`, and a failed comparison such as `6 < price` raises `ArgumentError`.
code
ruby · 21 linesclass Money
attr_reader :cents
def initialize(cents)
@cents = cents
end
def *(factor)
raise TypeError, "can only multiply Money by an Integer" unless factor.is_a?(Integer)
Money.new(cents * factor)
end
def coerce(number)
[self, number] # safe: multiplication is commutative
end
end
price = Money.new(249)
(price * 6).cents # => 1494
(6 * price).cents # => 1494, via price.coerce(6) then price * 6go deeper
Recall that 1 + nil fails with a can't be coerced into Integer TypeError, because the number asks nil to coerce itself.
Explain the three steps: the operator calls other.coerce(self), receives [x, y], and re-sends the operator to x with y.
Implement coerce on a numeric value object, keep operand order correct for non-commutative operators, and raise TypeError for unsupported types.
Judge when a domain type deserves numeric interoperability through coerce, and when explicit conversion at the call site keeps the model clearer.
## The problem: the left operand's method runs In Ruby, `6 * price` is the method call `6.*(price)`. `Integer#*` knows how to multiply by an `Integer`, `Float`, `Rational` or `Complex`, but it has never heard of your `Money` class. You could reopen `Integer` to teach it, but that changes a core class for every library in the process. Ruby provides a cooperative protocol instead: **coerce**. ## How the coerce protocol works When a numeric binary operator meets an operand it does not recognise: 1. It calls `other.coerce(self)`, here `price.coerce(6)`. 2. `coerce` must return a two-element array `[x, y]`. 3. The operator is re-sent as `x.op(y)`, here `x * y`. Core numeric classes use the same protocol among themselves: `2.coerce(3.0)` returns `[3.0, 2.0]`, converting both to a common type. The first element is the new receiver, the second the new argument. ## Implementing coerce on a value class A grocery-order importer multiplies quantities by unit prices, and prices are `Money` objects holding integer cents. `Money#*` accepts a number, so `price * 6` already works. To make `6 * price` work as well: - **Return `[self, n]`**: Ruby then evaluates `price * 6`. This is correct because multiplication is commutative. - **Do not swap for non-commutative operators.** `6 - price` would become `price - 6`, silently reversing the sign. If your class supports such operators with numbers, return `[Money.new(n), self]`, a wrapped number in the original position, so the original order survives. - **Raise `TypeError`** for operand types you do not support, so the caller sees a clear failure. ## Errors you will see | Situation | Result | |---|---| | `6 * price` and `Money` has no `coerce` | `TypeError`: `Money can't be coerced into Integer` | | `1 + nil` | `TypeError`: `nil can't be coerced into Integer` | | `5 + "3"` | `TypeError`: `String can't be coerced into Integer` | | `coerce` returns something other than a two-element array | `TypeError`: `coerce must return [x, y]` | | `6 < price` when coercion fails | `ArgumentError`: `comparison of Integer with Money failed` | The second row is the version most developers meet first: a blank quantity arrives as `nil`, and `subtotal + nil` fails with a message about coercion rather than about nil. ## When coerce is and is not the right tool - **Good fit**: numeric-like value objects such as money, measurements or vectors, where `number op object` has a clear meaning. - **Poor fit**: objects that are not numbers. Giving a `LineItem` a `coerce` method to make `total + item` work hides a conversion that should be explicit, like `total + item.subtotal`. - **Comparisons**: `6 < price` runs the same protocol and evaluates `x < y` on the returned pair, so a swapping `coerce` silently reverses the comparison. Either return the wrapped number in its original position or leave comparisons against bare numbers unsupported. ## Testing a coerce implementation Because `coerce` is called by core code rather than by your own, its bugs show up only in expressions with the number on the left. A short checklist of cases to exercise: 1. `price * 6` and `6 * price` return equal results. 2. `6.0 * price`, an unsupported left operand when only whole quantities are allowed, raises `TypeError` from your `*` after the swap. 3. Every non-commutative operator you support, if any, keeps the original operand order. 4. `6 * Object.new` still raises the standard `can't be coerced` error, proving your class did not change behaviour for anything else. ## Relation to the conversion protocols `coerce` is Ruby's numeric counterpart to implicit conversion methods like `to_str`: it lets core code accept a foreign object without changing core classes. The difference is that `coerce` negotiates a *pair* of operands rather than converting one value, which is why it returns two elements and why the receiver may change.
- Why is returning [self, number] from coerce wrong for subtraction?Ruby evaluates `x - y` with the pair `coerce` returns. For `6 - price`, returning `[self, 6]` computes `price - 6`, reversing the operands and the sign of the result. Return a wrapped number in the original position, `[Money.new(6), self]`, or raise `TypeError` if the operation is meaningless.
- What does 6 < price raise when Money cannot be coerced?Comparison operators use the same protocol, but a failed coercion is reported as `ArgumentError` with `comparison of Integer with Money failed` rather than as `TypeError`. When `coerce` succeeds, Ruby evaluates `x < y` on the returned pair, so a `coerce` that swaps operands turns `6 < price` into `price < 6`.
saying these in an interview costs you the question
- coerce converts the Money object into an Integer
- Integer#* calls to_int on unknown operands
- Returning [self, number] from coerce is correct for every operator
- The only fix for 6 * price is reopening Integer
- 1 + nil raises NoMethodError on nil