skip to content

In Ruby, how do you define +, unary minus and [] on a Temperature class, and which operators can you not define?

level: middleimportance: should knowfreq 42%

answer

  1. a + b is a.+(b)
  2. left operand decides
  3. -@ and +@ for unary
  4. += comes free from +
  5. && and || are not methods

basics

~20 s

Operators are methods: def +(other), def -@ for unary minus and def . Binary operators take one argument, and += comes free from +. Logical operators like && and ||, plain = and compound assignment cannot be defined.

solid answer

~40 s

In Ruby `a + b` is a method call, `a.+(b)`, so the **left operand's class** decides what happens. You define binary operators with one parameter (`def +(other)`), unary minus and plus with an `@` suffix and no parameters (`def -@`), and element access with `def [](scale)` and `def []=(scale, value)`, which may take any number of arguments. You never define `+=`: `t += other` expands to `t = t + other`, so defining `+` is enough, and it should return a **new** object rather than mutate `self`. What you cannot define: `&&`, `||`, `and`, `or`, plain `=`, the compound assignments and the ternary; `not obj` does call your `!`. Precedence cannot be changed either.

code

ruby · 16 lines
ruby
class Temperature
  attr_reader :celsius

  def initialize(celsius) = @celsius = celsius

  def +(other)
    raise TypeError, "not a Temperature" unless other.is_a?(Temperature)

    Temperature.new(celsius + other.celsius)
  end
end

t = Temperature.new(20)
t += Temperature.new(5)   # t = t + other; no += method exists
t.celsius                 # => 25
t.+(Temperature.new(1)).celsius  # => 26, the same call spelled out

go deeper

for a junior

Recall that a + b calls a.+(b), so defining def +(other) in a class gives that class the operator.

for a middle

Explain the @ suffix for unary operators, [] and []= arities, why += comes free from +, and which operators are syntax rather than methods.

for a senior

Argue for immutable results from arithmetic operators, explicit TypeError on unsupported operands, and restraint in overloading so domain code stays readable.

for a principal

Decide where operator overloading helps a domain model, such as money or units, and where named methods communicate intent better across a large codebase.

## Operators are method calls Most Ruby operators are **syntactic sugar for method calls**. `a + b` sends the message `+` to `a` with `b` as its argument; you can even write `a.+(b)`. Consequences: - The **left operand's class** decides the behaviour. `temp + other` runs `Temperature#+`, while `5 + temp` runs `Integer#+`. - You overload an operator by defining a method with that name in your class. - Parsing does not change: **precedence and associativity are fixed** by the grammar, so a custom `*` still binds tighter than your `+`. ## Defining the common ones ```ruby class Temperature attr_reader :celsius def initialize(celsius) @celsius = celsius end def +(other) Temperature.new(celsius + other.celsius) end def -@ Temperature.new(-celsius) end def [](scale) scale == :f ? celsius * 9 / 5.0 + 32 : celsius end def to_s = "#{celsius} C" end a = Temperature.new(20) puts a + Temperature.new(5) # 25 C puts(-a) # -20 C a[:f] # => 68.0 ``` | Operator | Method to define | Parameters | |---|---|---| | `a + b`, `a - b`, `a * b`, `a / b`, `a % b`, `a ** b` | `+`, `-`, `*`, `/`, `%`, `**` | exactly one | | `a << b`, `a & b`, `a \| b`, `a ^ b` | `<<`, `&`, `\|`, `^` | exactly one | | `-a`, `+a` | `-@`, `+@` | none | | `!a`, `~a` | `!`, `~` (the `@` suffix is optional) | none | | `a[k]` | `[]` | zero or more | | `a[k] = v` | `[]=` | one or more, value last | The `@` suffix exists only to tell unary minus and plus apart from binary minus and plus. Defining `def -` with no parameters does not make `-temp` work; `-temp` looks for `-@`. ## What you get for free, and what you cannot define 1. **Compound assignment is derived.** `t += other` is rewritten to `t = t + other`, so `+=` works as soon as `+` exists; `def +=` is a syntax error. The same holds for `-=`, `*=`, `<<=` and the rest. 2. **Element assignment follows assignment rules.** `a[:c] = 21` evaluates to `21` whatever `[]=` returns. 3. **Logical operators are not methods.** `&&`, `||`, `and` and `or` short-circuit inside the parser and cannot be redefined. `not obj` is different: it calls `obj.!`, so a custom `!` affects it. 4. **Plain assignment, the ternary `?:` and `defined?` are syntax**, not methods. ## Element reference and core examples `[]` and `[]=` are the most flexible operator methods because their arity is yours to choose: - `def [](row, col)` makes `grid[2, 3]` work, and `def []` with no parameters makes `obj[]` legal. - `def []=(row, col, value)` receives the assigned value **last**, so `grid[2, 3] = :x` calls it with `(2, 3, :x)`. - The expression `grid[2, 3] = :x` evaluates to `:x`, whatever `[]=` returns, because it is assignment syntax. Core classes use the same mechanism. `String` defines unary `-@` and `+@`, and `Array#[]` accepts an index, a start and length, or a range, all through one method. Nothing about operators is reserved to built-in types. ## Design guidance - **Return a new object from arithmetic operators.** Callers expect `a + b` to leave `a` unchanged, and `a += b` depends on rebinding a variable, not on mutation. - **Keep the meaning unsurprising.** Ruby's documentation warns that giving `+` a meaning other than adding confuses readers. - **Check the argument's type** when mixing types, and raise `TypeError` for operands you do not support rather than failing deep inside. - **Reversed operands are a separate problem.** `5 + temp` never reaches `Temperature#+`; making that work goes through the numeric conversion protocol, not through defining another operator. - Comparison and equality operators (`==`, `<=>`, `<`) are also definable methods, with their own contracts around `Comparable` and hash keys.

  • In Ruby, what happens when a Temperature class defines `def -` with no parameters and code writes `-temp`?
    It raises `NoMethodError` for `-@`, because unary minus dispatches to the method named `-@`, not `-`. A binary `temp - other` would then fail with `ArgumentError`, since that `-` takes no arguments. Define `-@` for negation and `-(other)` for subtraction.
  • Why should `Temperature#+` return a new object instead of changing `self` in Ruby?
    Callers read `a + b` as a pure expression and keep using `a`. `a += b` also works by rebinding `a` to the result, so mutation adds nothing and surprises any other reference to the same object. Mutating operations belong in named methods, often with a `!` suffix.

saying these in an interview costs you the question

  • To support t += other you must define a += method.
  • Unary minus is defined as def -(other) with an ignored argument.
  • You can overload && to combine two Temperature objects.
  • Defining * in your class can make it bind looser than +.
  • 5 + temp calls Temperature#+ because Ruby tries both operands.