Object Equality & Comparable

Ruby Intermediate/Advanced — Object Equality & Comparable
Ruby Intermediate/Advanced
Course 2 · Chapter 3 · Object Equality & Comparable

⚖️ Object Equality & Comparable

Ruby splits "are these two things equal?" into more distinct questions than PHP or Python ask — and splits "which one comes first?" into a single method that, paired with a module you already know from Course 1, gives you an object's entire comparison operator set for free.

🟰 == — Overridable Value Equality

class Money attr_reader :cents def initialize(cents) @cents = cents end def ==(other) other.is_a?(Money) && cents == other.cents end end Money.new(500) == Money.new(500) # => true -- same value, different objects

By default, == on a custom class checks object identity — two Money.new(500) instances would be considered different without an override, since they're separate objects in memory. Overriding == yourself (as shown above) is exactly like overriding __eq__ in Python, or the loose comparison behavior PHP's == already applies automatically to objects with matching properties.

🆔 equal? — True Identity, Never Overridable

a = Money.new(500) b = Money.new(500) a == b # => true -- uses our overridden == (value equality) a.equal?(b) # => false -- different objects in memory, always a.equal?(a) # => true -- same object

equal? always checks true identity — the same object in memory — and, unlike ==, it cannot be meaningfully overridden. This is the direct equivalent of Python's is operator, or PHP's === when comparing two object variables.

🔑 eql? — Stricter, Type-Sensitive Equality for Hash Keys

1 == 1.0 # => true -- == treats them as equal 1.eql?(1.0) # => false -- eql? is stricter: different types, not equal 1.eql?(1) # => true
⚠ Hash and Set use eql? + hash, not ==

When Ruby looks up a key in a Hash or checks membership in a Set, it uses eql? combined with hash — not ==. This is why { 1 => "int" }[1.0] returns nil even though 1 == 1.0 is true: 1 and 1.0 are eql?-different, so they hash to different Hash buckets. Neither PHP nor Python has a directly equivalent second equality method — Python's dicts get similarly close behavior from requiring __eq__ and __hash__ to agree with each other, which is the same underlying idea Ruby's eql?/hash pairing enforces explicitly.

#️⃣ hash — Required Alongside eql?

class Money attr_reader :cents def initialize(cents) @cents = cents end def ==(other) other.is_a?(Money) && cents == other.cents end alias eql? == def hash cents.hash # delegate to the underlying value's own hash end end totals = { Money.new(500) => "Order A" } totals[Money.new(500)] # => "Order A" -- works correctly, because eql? + hash both agree

Any time a custom class overrides eql? (directly, or — as shown here — by aliasing it to a custom ==), it must also override hash so that two eql?-equal objects always produce the same hash value. Skip this, and using instances of the class as Hash keys or Set members produces confusing, inconsistent lookup behavior — a subtle bug that's easy to introduce and hard to spot.

🚀 <=> — The Spaceship Operator

3 <=> 5 # => -1 -- 3 is less than 5 5 <=> 5 # => 0 -- equal 5 <=> 3 # => 1 -- 5 is greater than 3 5 <=> "x" # => nil -- not comparable at all

The spaceship operator does a single three-way comparison: -1, 0, 1, or nil if the two values can't be meaningfully compared. PHP 7 actually adopted the exact same <=> symbol and semantics, directly inspired by Perl and Ruby — a rare case of syntax converging across languages. Python has no single spaceship operator; it implements __lt__, __gt__, __eq__, etc. separately, or uses functools.total_ordering to derive the rest from just one or two of them.

📐 The Comparable Module — This Chapter's Payoff

Course 1 Chapter 7 introduced modules as a way to share behavior across unrelated classes. Comparable is one of Ruby's own built-in modules, and it's the biggest payoff yet: define <=> once, include Comparable, and get <, >, <=, >=, ==, between?, and clamp all for free:

class Version include Comparable attr_reader :major, :minor def initialize(major, minor) @major = major @minor = minor end def <=>(other) [major, minor] <=> [other.major, other.minor] # Array <=> compares element by element end end v1 = Version.new(2, 5) v2 = Version.new(3, 0) v1 < v2 # => true -- generated by Comparable, from our <=> v1.between?(Version.new(1,0), v2) # => true -- also generated, for free [v2, v1].sort # => [v1, v2] -- Array#sort uses <=> automatically

This is the exact same mixin mechanism Enumerable used in Course 1 Chapter 5 and 7 — one module method (<=> here, a block for Enumerable) unlocks an entire family of related behavior, without writing <, >, and every other comparison operator by hand.

📜 Equality Concepts, Side by Side

PHP vs Python vs Ruby

ConceptPHPPythonRuby
Value equality (overridable)== (auto for objects)__eq__==
Identity (never overridable)===isequal?
Hash/dict key equalitySame as == (no separate method)__eq__ + __hash__ togethereql? + hash together
Three-way comparison<=> (PHP 7+, same symbol)No single operator — __lt__/__gt__/etc.<=>
Get all comparisons from one methodNo direct equivalent@total_ordering decoratorinclude Comparable

💻 Coding Challenges

Challenge 1: Correct Hash Keys

Write a Point class with x and y attributes. Override == for value equality, alias eql? to it, and override hash to delegate to [x, y].hash. Prove it works by using two separately-constructed but equal Point instances as Hash keys and confirming a lookup with a third equal instance succeeds.

Goal: Practice the full eql?/hash pairing this chapter's warning box calls out as easy to get wrong.

→ Solution

Challenge 2: Comparable in Practice

Write a Temperature class storing degrees in Celsius, implement <=> based on that value, and include Comparable. Demonstrate <, >, and between? all working correctly despite never being written explicitly.

Goal: Feel the payoff of defining one method and getting an entire comparison API for free.

→ Solution

Challenge 3: Sorting Custom Objects

Reusing the Temperature class from Challenge 2, build an array of five Temperature instances in random order and call .sort on the array with no block or extra arguments. Print the sorted result to confirm Array#sort picked up your <=> automatically.

Goal: See <=> and Comparable integrate transparently with built-in Enumerable methods, not just your own code.

→ Solution

💎 The Rule to Remember

If a custom class ever needs to work correctly as a Hash key, in a Set, or with Array#uniq, the rule is simple and non-negotiable: override eql? and hash together, or not at all. Overriding just one of the two is worse than overriding neither — it produces an object that looks like it supports equality-based lookup but silently doesn't, which is a much harder bug to catch than an object that obviously uses default identity comparison.

🎯 What's Next

Next chapter: Iterators & Enumerable Deep Dive — building your own class that includes Enumerable by implementing a single each method, and lazy evaluation for working with sequences too large to load into memory all at once.