skip to content

In Ruby, if an Amount class for restaurant bills defines + and ==, what happens with += and !=, and which operators can never be overloaded?

level: seniorimportance: should knowfreq 32%

answer

  1. a + b is a.+(b)
  2. += expands to a = a + b
  3. BasicObject#!= negates ==
  4. &&, ||, =, ?: are syntax
  5. precedence cannot be changed

basics

~20 s

Most Ruby operators are methods on the left operand: defining + makes += work, since it expands to a = a + b, and BasicObject#!= negates ==. Assignment, &&, ||, and, or, ?:, &. and :: are syntax, not overloadable.

solid answer

~50 s

In Ruby `a + b` is the call `a.+(b)`, so a class overloads an operator by defining a method with that name: `+`, `-`, `*`, `/`, `%`, `**`, comparisons such as `<` and `>=`, `==`, `[]`, `[]=`, unary `-@` and `+@`, `!` and `~`. Some you get for free: `total += tip` expands to `total = total + tip`, so it works once `+` exists and cannot be defined separately, and `BasicObject#!=` calls `==` and negates the result. `&&`, `||`, `and`, `or`, `=`, the ternary, `&.`, `::`, `..` and `defined?` are syntax, not methods. Precedence and associativity are fixed by the parser, and the left operand's class decides the behaviour, so `2 * amount` calls `Integer#*` rather than yours. Value-object operators should return new objects, because `+=` rebinds the variable while other references keep the old one.

code

ruby · 22 lines
ruby
class Amount
  attr_reader :cents

  def initialize(cents)
    @cents = cents
  end

  def +(other)
    Amount.new(cents + other.cents)
  end

  def ==(other)
    other.is_a?(Amount) && cents == other.cents
  end
end

total = Amount.new(4_500)
share = total
total += Amount.new(1_250)   # total = total + ..., a new object
share.cents                  # => 4500, untouched
total != Amount.new(5_750)   # => false, via BasicObject#!=
3.+(4)                       # => 7, the call behind 3 + 4

go deeper

for a junior

Recall that a + b is a method call a.+(b), so a class adds operator support by defining a method named after the operator.

for a middle

Explain which forms come for free (+= from +, != from ==, not from !) and which operators are syntax that cannot be overloaded.

for a senior

Design value-object operators that return new instances, reject foreign operands clearly and keep == and != consistent, and plan for the reversed 2 * amount case.

for a principal

Decide when operator overloading improves a domain API and when named methods are clearer, and hold the codebase to unsurprising meanings.

Operator overloading in Ruby is not a special language feature: it falls out of the rule that **most operators are method calls**. Knowing exactly where that rule stops is what separates a working value object from a surprising one. ## Operators as methods When Ruby sees `a + b` it sends the message `+` to `a` with `b` as the argument - the same as writing `a.+(b)` or `a.public_send(:+, b)`. The same holds for comparison operators: `price < limit` is `price.<(limit)`, answered by the left operand's class. Ruby's method documentation lists the operator names a class may define: - **binary arithmetic and bitwise**: `+`, `-`, `*`, `/`, `%`, `**`, `&`, `|`, `^`, `<<`, `>>`; - **comparison and equality**: `==`, `!=`, `<`, `<=`, `>`, `>=`, `<=>`, `===`, `=~`, `!~`; - **unary**: `-@`, `+@`, `!`, `~`; - **element access**: `[]` and `[]=`. The documentation adds a warning: you cannot change the precedence of these operators, and giving them a surprising meaning confuses readers. ## What you get for free | You define | You also get | Mechanism | |---|---|---| | `+` | `+=` | `a += b` expands to `a = a + b` | | `-`, `*`, `**` and the others | `-=`, `*=`, `**=` | same expansion for each abbreviated assignment | | `==` | `!=` | `BasicObject#!=` calls `==` and negates the result | | `!` | `not` | `not x` calls the `!` method | Two consequences follow. You **cannot** define `+=` itself - it is not a method name. And overriding `!=` separately is possible but almost always a mistake, because it lets `a == b` and `a != b` both be true. ## What is syntax, not a method These cannot be overloaded at all: 1. assignment `=` and multiple assignment; 2. `&&`, `||`, `and`, `or` - they test truthiness directly, with only `nil` and `false` falsy; 3. the ternary `?:` and the `if`/`unless` modifiers; 4. safe navigation `&.` and the scope operator `::`; 5. the range literals `..` and `...`; 6. `defined?`. `!` is the odd one out: it is a method (`BasicObject#!`), so it can be redefined, but `&&` and `||` will ignore the override. ## Designing operators for a value object Consider an `Amount` holding integer cents for splitting restaurant bills: ```ruby class Amount attr_reader :cents def initialize(cents) @cents = cents end def +(other) raise TypeError, "expected Amount, got #{other.class}" unless other.is_a?(Amount) Amount.new(cents + other.cents) end def -@ Amount.new(-cents) end def ==(other) other.is_a?(Amount) && cents == other.cents end end ``` Guidelines this illustrates: - **Return a new object.** `+` must not mutate `self`. Because `total += tip` rebinds `total` to the result, a mutating `+` would also change every other reference to the old amount - for example the per-diner share that was copied from it. - **Name the parameter `other`.** RuboCop's `Naming/BinaryOperatorParameterName` expects it, and readers do too. - **Reject incompatible operands clearly** with `TypeError` in arithmetic; `==` should simply return `false` for foreign types rather than raise. - **Keep meanings unsurprising.** `+` adds, `-@` negates, `<<` appends. `String#%` for formatting is a famous exception that works because it is long established. ## The left operand decides `Amount.new(500) * 2` can be handled by an `Amount#*`, but `2 * Amount.new(500)` calls `Integer#*`, which does not know about `Amount`. Ruby's numeric classes offer a hook for that reverse case, the coercion protocol, which is a topic in its own right. For comparison operators, the usual approach is to define `<=>` and include `Comparable`, which supplies `<`, `<=`, `>`, `>=`, `between?` and `clamp`, rather than writing each comparison by hand. ## Operators on core classes Because operators are ordinary methods, anything that can change a method can change an operator, including reopening a core class. Redefining `Integer#+` or `String#*` globally is a monkey-patch with process-wide effects: every library in the program sees the new behaviour. When a domain needs special arithmetic, put it on its own class such as `Amount` rather than patching `Integer`, so the operator's meaning is local to values that opted in. ## Summary - An operator call is a method call on the left operand. - Defining `+` gives `+=`; defining `==` gives `!=`. - `&&`, `||`, `and`, `or`, `=`, `?:`, `&.`, `::`, `..`, `...` and `defined?` are not overloadable. - Precedence and associativity never change. - Value-object operators return new objects and leave the receiver alone.

  • In Ruby, why is a mutating + on a value object dangerous even though += reassigns the variable?
    `total += tip` evaluates `total + tip` and rebinds `total` to the result. If `+` mutated `total` and returned `self`, every other variable, hash value or array slot holding that same object would change too - for example a share copied from the total before the tip. Returning a new object keeps `+` free of side effects.
  • Can you overload && or || for a Ruby class, and what happens if you redefine ! instead?
    No: `&&`, `||`, `and` and `or` are syntax that test truthiness directly, where only `nil` and `false` are falsy. `!` is a method on `BasicObject`, so redefining it changes `!obj` and `not obj`, but `&&`, `||` and `if obj` still treat the object as truthy, which makes the class inconsistent.

saying these in an interview costs you the question

  • You must define += separately after defining + in Ruby.
  • Defining == does not affect !=; you must write both.
  • && and || can be overloaded like any other operator method.
  • Overloading * lets you change its precedence relative to +.
  • 2 * amount works automatically once Amount defines its own *.