Skip to content

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 is equal? to itself; 1 and :s are equal? 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 are 1 and 1.0 — Integer#== converts across numeric types. "a" == :a is false (a symbol is not a string) and nil == false is false.
  • Row 3. eql? is == without the conversion: 1.eql?(1.0) is false, their hash values differ, so {1 => :int}[1.0] is nil and [1, 1.0].uniq keeps both. Define eql? and hash together 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, so Integer === 1.0 is false; 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 p evaluates p === x, which is how one case can 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 ==), but eql? still says false, 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

  1. 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 ever true when == is false?
  2. Write a Money class with amount and currency. Define == so that two equal amounts in the same currency compare equal, then show that a hash still treats them as two keys; define eql? and hash and show the hash now sees one key. What does Money.new(1, "USD") == 1 return, 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.

   a         b         equal?   ==       eql?     ===
   1         1         true     true     true     true
   1         1.0       false    true     false    true
   "a"       "a"       false    true     true     true
   :a        :a        true     true     true     true
   [1]       [1]       false    true     true     true
   Integer   1         false    false    false    true
   1..3      2         false    false    false    true
   /a/       "cat"     false    false    false    true

See also