Ruby has four kinds of equality¶
Level: 201 · anyone who has typed == in Ruby and wondered what eql? and === are for
One line: equal? is identity, == is value, eql? is value without type conversion (the test Hash keys use), and === is "does this pattern match that value?" (the test case/when runs); Python has is, == and isinstance, and its == does the job of eql? too, so 1 == 1.0, hash(1) == hash(1.0) and {1: "a"}[1.0] all say yes.
Ruby gives every object four methods for four different questions. a.equal?(b) asks whether they are the same object; you never override it. a == b asks whether they have the same value; you override it in your own classes. a.eql?(b) asks for the same value and the same type, and Hash and uniq use it together with hash — which is why 1 and 1.0 are two different hash keys in Ruby. a === b asks whether a, read as a pattern, matches b: a class matches its instances, a regexp its matches, a range its members, a proc whatever it returns truthy for — and case/when calls it on every when. Python has three of the four and folds the third into the second. The program prints six numbered groups, and the Python twin prints the same rows.
Verified output of four_kinds_of_equality_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. equal? asks: the same object?
a = [1]; b = [1]; a.equal?(b) false
a.equal?(a) true
1.equal?(1) true
:s.equal?(:s) true
"xy".equal?("xy") false
2. == asks: the same value?
a == b true
"x" == "x" true
1 == 1.0 true
"a" == :a false
nil == false false
3. eql? asks: the same value and type? (Hash uses it)
1.eql?(1.0) false
1.hash == 1.0.hash false
{1 => :int}[1.0] nil
{1 => :int}[1] :int
[1, 1.0].uniq [1, 1.0]
4. === asks: does the pattern match the value?
Integer === 1 true
Integer === 1.0 false
/a/ === "cat" true
(1..3) === 2 true
->(x) { x > 1 } === 2 true
"cat" === "cat" true
5. case/when calls ===
case 2 when Integer :int
case "cat" when /a/ :match
case 2 when 1..3 :in_range
case 7 when ->(n) { n > 5 } :big
6. your own class: identity until you define ==
Point.new(1, 2) == Point.new(1, 2) false
after def ==: the same comparison true
Point.new(1, 2).eql?(Point.new(1, 2)) false
Point.new(1, 2) === Point.new(1, 2) true
{p1 => :a, p2 => :b}.size (equal points) 2
Reading the output¶
- Row 1.
equal?is identity. Two[1]literals are two objects; a variable isequal?to itself;1and:sareequal?to themselves because there is only one of each, while two"xy"literals are two strings. - Row 2.
==is value. The two arrays are==, and so are1and1.0—Integer#==converts across numeric types."a" == :aisfalse(a symbol is not a string) andnil == falseisfalse. - Row 3.
eql?is==without the conversion:1.eql?(1.0)isfalse, theirhashvalues differ, so{1 => :int}[1.0]isniland[1, 1.0].uniqkeeps both. Defineeql?andhashtogether when your objects become hash keys — that page shows what happens if you define only==. - Row 4.
===reads its left side as a pattern. A class matches only its instances, soInteger === 1.0isfalse; a regexp matches the strings it matches; a range matches the values it covers; a lambda matches what it returns truthy for; and a plain value such as"cat"matches what it is==to. - Row 5.
case x when pevaluatesp === x, which is how onecasecan dispatch on a class, a regexp, a range and a lambda without spelling any of those tests out. - Row 6. A class you write starts with identity for all four. After
def ==,==and===agree (the default===calls==), buteql?still saysfalse, so two equal points are still two hash keys.
Compared with Python¶
Python has is for identity and == for value, and the two rows agree with Ruby's — with one implementation detail worth knowing: CPython caches small integers and interns identifier-like string literals, so x = 1; y = 1; x is y and s = "s"; t = "s"; s is t are True where Ruby says two literals are two strings, and a string built at run time with join is a separate object (row 1). There is no eql? because == and hash() already agree across numeric types: hash(1) == hash(1.0) is True, {1: "int"}[1.0] finds the entry, {1: "int"}[True] finds it too since bool is an int, and the set {1, 1.0} has one element (row 3) — the Python library's page on comparing int and float ↗ measures that side in depth. There is no ===; each of Ruby's patterns has its own spelling — isinstance, re.search, in, a call (row 4) — and match/case reproduces case/when with class patterns for the isinstance cases and guards for the rest (row 5). A class starts with identity == here too; defining __eq__ makes the objects unhashable (__hash__ becomes None) until you define __hash__ as well, where Ruby quietly keeps identity hashing and gives you two keys (row 6).
Verified output of four_kinds_of_equality_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. is asks: the same object?
a = [1]; b = [1]; a is b False
a is a True
x = 1; y = 1; x is y (cached small int) True
s = "s"; t = "s"; s is t (interned literal) True
u = "".join(["x", "y"]); v = the same; u is v False
2. == asks: the same value?
a == b True
"x" == "x" True
1 == 1.0 True
"a" == "a" (no symbol to differ from) True
None == False False
3. no eql?: == and hash() agree across int and float
type(1) is type(1.0) and 1 == 1.0 False
hash(1) == hash(1.0) True
{1: "int"}[1.0] 'int'
{1: "int"}[True] 'int'
len({1, 1.0}) 1
4. no ===: isinstance, re.search, in, a call
isinstance(1, int) True
isinstance(1.0, int) False
re.search("a", "cat") is not None True
2 in range(1, 4) True
(lambda x: x > 1)(2) True
"cat" == "cat" True
5. match/case: isinstance for class patterns, guards for the rest
match 2: case int(): 'int'
match "cat": case str() as t if re.search: 'match'
match 2: case n if 1 <= n <= 3: 'in_range'
match 7: case n if n > 5: 'big'
6. your own class: identity until you define __eq__
Point(1, 2) == Point(1, 2) False
after def __eq__: the same comparison True
Point.__hash__ is None (unhashable now) True
isinstance(Point(1, 2), Point) True
{p1: 'a', p2: 'b'} (equal points) TypeError
Try it¶
- Print one table with four columns —
equal?,==,eql?,===— for the pairs[1, 1],[1, 1.0],["a", "a"],[:a, :a],[[1], [1]],[Integer, 1],[1..3, 2]and[/a/, "cat"]. Which column is the only one that is evertruewhen==isfalse? - Write a
Moneyclass withamountandcurrency. Define==so that two equal amounts in the same currency compare equal, then show that a hash still treats them as two keys; defineeql?andhashand show the hash now sees one key. What doesMoney.new(1, "USD") == 1return, and why must==check the class of its argument first?
Solution to 1
four_kinds_of_equality_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: one table, four columns -- equal?, ==, eql? and === -- for eight pairs.
# Read it row by row: only equal? cares about identity, only eql? cares about
# the type, and only === treats the left side as a pattern.
pairs = [
[1, 1], [1, 1.0], ["a", "a"], [:a, :a], [[1], [1]],
[Integer, 1], [1..3, 2], [/a/, "cat"]
]
puts " a b equal? == eql? ==="
pairs.each do |a, b|
cells = [a.equal?(b), a == b, a.eql?(b), a === b].map { |v| v.to_s.ljust(8) }
puts " #{a.inspect.ljust(9)} #{b.inspect.ljust(9)} #{cells.join(' ')}".rstrip
end
Verified output of four_kinds_of_equality_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- A symbol is a name, not a string — why
:s.equal?(:s)istruein row 1 while two string literals are not - Only
nilandfalseare falsy —nil == falseisfalsein row 2, and the two are different classes - Comparing
intandfloat— row 3 in depth:1 == 1.0,eql?,hash,uniqand<=> case/whenuses===— row 5 as a whole page: classes, ranges, regexps, lambdas and splats inwheneql?andhashfor hash keys — what row 6'sPointneeds before it can be a key- Comparing
intandfloatin Python ↗ — the Python side of row 3 - Object#=== ↗ — the docs entry that names the four methods
- Duck typing asks
respond_to?, notclass— Ruby code asks an object what it can do - Procs and lambdas differ in arity and
return— Every proc and lambda is aProc, but a lambda checks its argument count andreturns from itself, while… - Define
<=>and Comparable gives you the rest —<=>answers-1,0,1ornil("these do not compare");include Comparableturns that one method… - Twenty-three Ruby traps for a Python programmer — The twenty-three places where a Python reflex gives the wrong answer in Ruby
- Twenty-three Python traps for a Ruby programmer — The twenty-three places where a Ruby reflex gives the wrong answer in Python