Operators are methods, except the ones that are not¶
Level: 201 · a reader who has written __add__ in Python
One line: +, -@, [], []=, <=>, << and even ! are ordinary methods you define with def, and 1 + 2 is 1.+(2) — but &&, ||, =, ?:, .., and, or and not are syntax and cannot be defined or looked up.
When Ruby sees a + b it sends the message + to a with b as the argument; the operator table is just spelling rules for method names. So a class defines + with def +(other), unary minus as def -@, indexing as def [] and def []=, and gets the whole family of comparisons by defining <=> and including Comparable. Even ! is a method (BasicObject#!), which is why !obj and not obj can be overridden. The exceptions are the operators whose meaning depends on not evaluating an operand or on the parser: short-circuit &&/|| (and their word forms), assignment =, the conditional ?:, and ranges ... Those are not messages; 1.method(:"&&") is a NameError and def && does not parse. One more thing to know from Python: a reflected operation (2 * vec) does not look for a __rmul__; Ruby's Integer#* asks the argument for coerce instead.
Verified output of operators_are_methods_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. an operator is a call: 1 + 2 -> 3, 1.+(2) -> 3, 1.send(:+, 2) -> 3, 1.method(:+).owner -> Integer
2. def +(other): v + w -> Vec(4, 6), v.+(w) -> Vec(4, 6)
3. def -@ (unary minus): -v -> Vec(-1, -2)
4. def [] and def []=: v[0] -> 1; v[1] = 9 evaluates to 9, not the body's :ignored; v is Vec(1, 9)
5. def <=> plus Comparable: v < w true, v == Vec.new(1, 2) true, v != w true, [w, v].min -> Vec(1, 2), v.clamp(w, w) -> Vec(3, 4)
6. def <<: v << 10 -> Vec(11, 12)
7. def ! is a method: !v -> "bang called on Vec(1, 2)", (not v) -> "bang called on Vec(1, 2)", v.! -> "bang called on Vec(1, 2)"
every object has one: 1.method(:!).owner -> BasicObject, !1 -> false
8. def * but no coerce: v * 2 -> Vec(2, 4); 2 * v -> TypeError: Vec can't be coerced into Integer
9. && is not a method: 1.method(:"&&") -> NameError: undefined method '&&' for class 'Integer'
def &&(other); end -> SyntaxError
10. Integer responds to: + - * / % ** == != < <= > >= <=> << >> & | ^ ~ ! -@ +@ [] ===
syntax, not methods: && || .. and or not =
Reading the output¶
Row 1 is the claim at its plainest: 1 + 2, 1.+(2) and 1.send(:+, 2) are one call, owned by Integer. Rows 2, 3 and 6 are Vec's own +, -@ and <<. Row 4 is [] and []= — with a detail from the setters lesson: v[1] = 9 evaluates to 9 no matter that the body returned :ignored. Row 5 is the payoff of <=>: one definition plus include Comparable gives <, ==, !=, min, clamp and the rest. Row 7 shows ! really is a method: !v, not v and the explicit v.! all run our definition and return a String, which no Python not could do.
Row 8 is the reflected case: v * 2 works because Vec#* exists, but 2 * v fails with TypeError: Vec can't be coerced into Integer — Integer#* does not know about Vec and asks it for coerce, which the numbers chapter covers. Row 9 is the boundary: && is not a method name — looking it up is a NameError, defining it is a SyntaxError. Row 10 sorts a fixed list of operator spellings by 1.respond_to?: everything from + to === is a method on Integer; && || .. and or not = are not.
Compared with Python¶
Python's model is the same one with different spelling: an operator is a call to a "dunder" hook — __add__, __neg__, __getitem__, __setitem__, __eq__, __lt__, __lshift__ — and row 1 shows 1 + 2 as (1).__add__(2). The differences are in the corners. Row 4: v[1] = 9 is a statement in Python, so __setitem__'s return value simply vanishes; Ruby's assignment is an expression that evaluates to the right-hand side. Row 5: there is no <=>; __eq__ and __lt__ plus functools.total_ordering give the family, and without __lt__ a < is a TypeError. Row 7: not v calls __bool__, but can only ever produce a bool — Ruby's ! is a real method that returned a String. Row 8 is the reflected operation Ruby lacks: 2 * v finds __rmul__. Rows 9 and 10 draw the same boundary as Ruby: and, or, not and = are keywords and syntax with no hook, while & has __and__.
Verified output of operators_are_methods_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. an operator is a call: 1 + 2 -> 3 , (1).__add__(2) -> 3 , int.__add__(1, 2) -> 3 , type((1).__add__).__name__ -> method-wrapper
2. def __add__: v + w -> Vec(4, 6) , v.__add__(w) -> Vec(4, 6)
3. def __neg__: -v -> Vec(-1, -2)
4. __getitem__ and __setitem__: v[0] -> 1 ; v[1] = 9 is a statement with no value (the 'ignored' return is discarded); v is Vec(1, 9)
5. __eq__ and __lt__ (+ total_ordering): v < w True , v == Vec(1, 2) True , v != w True , min([w, v]) -> Vec(1, 2) , v > w False
without __lt__: NoLt() < NoLt() -> TypeError
6. def __lshift__: v << 10 -> Vec(11, 12)
7. not calls __bool__: not v -> True (__bool__ ran 1 time and could only return a bool)
8. __mul__ and __rmul__: v * 2 -> Vec(2, 4) ; 2 * v -> Vec(2, 4) (the reflected hook answers)
9. and is not a hook: b & b -> bitwise & called ; b and 'second' -> 'second' (__and__ not called)
keywords, not methods: and True , or True , not True
10. int has a hook for: + - * / % ** == != < <= > >= << >> & | ^ ~ -@ +@
no hook (syntax, or not Python): <=> ! [] === && || .. and or not =
Try it¶
- Write a
Moneyclass holding cents, with+, unary-,<=>andto_s, andinclude Comparable. Print a sum, a negation, a sorted array,max,<,==,!=,between?andclamp— and print which module owns the<you never defined. - Add
[]toMoneyso thatm[:dollars]andm[:cents]split the amount, then[]=so thatm[:cents] = 5changes it. Print whatm[:cents] = 5evaluates to when the body of[]=returns:nope. Finally trydef &&(other)inside the class and read theSyntaxError.
Solution to 1
operators_are_methods_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: a Money class whose +, unary -, and <=> are methods, with Comparable
# supplying <, ==, between? and clamp from the one <=> definition.
class Money
include Comparable
attr_reader :cents
def initialize(cents) = @cents = cents
def +(other) = Money.new(cents + other.cents)
def -@ = Money.new(-cents)
def <=>(other) = cents <=> other.cents
def to_s = format("$%.2f", cents / 100.0)
def inspect = to_s
end
a, b, c = Money.new(500), Money.new(1250), Money.new(99)
puts "1. a + b -> #{a + b}"
puts "2. -a -> #{-a}"
puts "3. [b, a, c].sort -> #{[b, a, c].sort.inspect}"
puts "4. [b, a, c].max -> #{[b, a, c].max}"
puts "5. a < b, a == Money.new(500), a != b -> #{a < b}, #{a == Money.new(500)}, #{a != b}"
puts "6. c.between?(a, b) -> #{c.between?(a, b)}; c.clamp(a, b) -> #{c.clamp(a, b)}"
puts "7. Money.instance_method(:<).owner -> #{Money.instance_method(:<).owner} (Comparable wrote < from <=>)"
Verified output of operators_are_methods_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- A setter returns its argument, not its body — why
v[1] = 9in row 4 evaluates to 9 and not to:ignored - Bang means dangerous, question mark means predicate —
!as a method here,!as a naming signal there - Everything is an object —
1.+(2)as the first proof that a literal has methods - Comparable and the spaceship —
<=>,Comparable, and what happens when it returns nil - Numeric coercion — row 8's
coerce, which makes2 * vwork eql?andhashfor Hash keys — the two methods==does not give you- Rust: Operators are traits ↗ — the same idea as
impl Add for Vec - Rust: The comparison traits ↗ —
PartialOrdandOrdbeside<=>andComparable - Python data model: emulating numeric types ↗ — the full table of hooks, including the reflected
__r*__family - Duck typing asks
respond_to?, notclass— Ruby code asks an object what it can do and/orbind lower than=— Ruby has two pairs of boolean operatorscase/whenasks===, and every class answers it its own way —case x when ycallsy === x, and===is an ordinary method: a class answers withis_a?, a Range…- A method is a value once you ask for it —
obj.method(:name)hands back aMethodbound toobj, andKlass.instance_method(:name)an… - Ruby shows you its bytecode and its syntax tree —
RubyVM::InstructionSequence.compile(src).disasmlists the YARV instructions the interpreter will execute…