Object Equality & Comparable
⚖️ Object Equality & Comparable
🟰 == — Overridable Value Equality
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
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
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?
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
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:
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
| Concept | PHP | Python | Ruby |
|---|---|---|---|
| Value equality (overridable) | == (auto for objects) | __eq__ | == |
| Identity (never overridable) | === | is | equal? |
| Hash/dict key equality | Same as == (no separate method) | __eq__ + __hash__ together | eql? + hash together |
| Three-way comparison | <=> (PHP 7+, same symbol) | No single operator — __lt__/__gt__/etc. | <=> |
| Get all comparisons from one method | No direct equivalent | @total_ordering decorator | include 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.
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.
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.
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.