In Ruby, when do two Struct or Data instances compare equal with ==, and why can identical-looking values still differ?
answer
- same class, not same shape
- members compared pairwise with ==
- subclass instance is never equal
- 100 == 100.0 holds per member
- plain classes compare identity
basics
~20 sStruct#== and Data#== return true only when both objects have exactly the same class and every member is == to its counterpart. Two classes with the same members, or a subclass and its parent, never compare equal.
solid answer
~40 sThe generated `==` first returns `true` for the same object, then `false` if the other object is not a Struct-family instance or its class is not **exactly** the receiver's class, and only then compares members pairwise with `==`. So `Money[100, "EUR"] == Money[100.0, "EUR"]` is `true`, because `100 == 100.0`. Two separately defined classes with identical members are never equal, a subclass instance never equals a parent-class instance, and a Hash with the same keys and values is not equal either. Because a Struct is mutable, its equality can change after a writer call. A plain class, by contrast, inherits `Object#==`, which compares identity, which is one of the main reasons to generate a class with Struct or Data at all.
code
ruby · 15 linesMoney = Data.define(:cents, :currency)
Price = Data.define(:cents, :currency)
class Tip < Money; end
Money[100, "EUR"] == Money[100.0, "EUR"] # => true (100 == 100.0)
Money[100, "EUR"] == Price[100, "EUR"] # => false (different class)
Money[100, "EUR"] == Tip[100, "EUR"] # => false (exact class only)
Money[100, "EUR"] == {cents: 100, currency: "EUR"} # => false
Fix = Struct.new(:lat, :lng)
a = Fix.new(1, 2)
b = Fix.new(1, 2)
a == b # => true
b.lat = 5
a == b # => falsego deeper
Recall that Struct and Data instances with the same class and equal member values compare equal with ==, unlike plain objects.
Explain the exact-class check, per-member == and why same-shaped classes or subclasses never compare equal.
Spot equality bugs from comparing values of different generated classes, from Hash inputs and from Structs that change after being compared.
Decide when value equality should be built in by generating Data classes and when identity equality of plain classes is the safer model.
## What the generated == checks Both `Struct.new` and `Data.define` classes inherit a **value-based `==`** from CRuby's `struct.c` (Data's `==` is the same C function as Struct's). It runs these steps in order: 1. If the other object **is the receiver**, return `true`. 2. If the other object is **not a Struct-family instance** at all (a Hash, an Array, a plain object), return `false`. 3. If the other object's class is **not exactly** the receiver's class, return `false`. 4. Otherwise compare the members **pairwise with `==`**, in member order, and return `true` only if every pair is equal. The comparison is guarded against self-referencing members, so a cycle does not recurse forever. That is the whole rule. It has three consequences that interviewers like to probe. ## Same shape is not the same class The Ruby documentation for `Data#==` makes the point directly: two classes defined with the same member names produce instances that are **never equal**, even with identical values. | Comparison | Result | Why | |---|---|---| | `Money[100, "EUR"] == Money[100, "EUR"]` | `true` | same class, members equal | | `Money[100, "EUR"] == Price[100, "EUR"]` | `false` | different classes, same shape | | `Money[100, "EUR"] == Tip[100, "EUR"]` where `class Tip < Money` | `false` | class must match exactly, not by `is_a?` | | `Money[100, "EUR"] == {cents: 100, currency: "EUR"}` | `false` | a Hash is not a Struct-family object | | `Money[100, "EUR"] == Money[100.0, "EUR"]` | `true` | `100 == 100.0` | The exact-class rule means equality is **symmetric**: `a == b` and `b == a` always agree, which a `kind_of?`-based check could not promise for a parent and child. ## Members are compared with == Because step 4 uses each member's own `==`, numeric members follow numeric equality: an Integer and a Float with the same value compare equal. That is usually what you want for money read from two sources, but it is also a reminder that `==` is **looser** than the `eql?` used for Hash keys, where `100` and `100.0` differ. Equality contracts for Hash keys are a separate subject; for `==`, remember that each member decides for itself. Nested members recurse naturally: a `Route` Data whose members are two `Coordinate` Data objects compares equal when both coordinates compare equal. ## Mutable Structs change their equality A Struct instance has writers, so the answer to `a == b` is only true **at the moment you ask**: - `a = Fix.new(1, 2)` and `b = Fix.new(1, 2)` compare equal. - After `b.lat = 5`, they no longer do. A Data instance is frozen, so once two Data values are equal they **stay** equal (as long as their members are themselves immutable). ## Why this beats a plain class A hand-written class inherits `Object#==`, which compares **identity**: two `Point.new(1, 2)` objects are different unless you write `==` yourself. Struct and Data give you correct value equality for free, and you should rarely override it: - Overriding `==` alone leaves the built-in `eql?` and `hash` untouched, so Hash lookups and `uniq` would disagree with `==`. - If two related classes must compare equal, question the design first; usually one class with an extra member is simpler than cross-class equality. ## Seeing why two values differ When a comparison unexpectedly returns `false`, three quick checks usually explain it: - Compare `a.class` and `b.class`; `inspect` prints the class name (`#<data Money ...>` vs `#<data Price ...>`), so a same-shape class stands out. - Compare `a.to_h` and `b.to_h` to find the member that differs. - Check for a Struct that was mutated between being stored and being compared. ## Checklist for code review - Comparing values of **different generated classes**? Expect `false`. - Comparing a subclass instance with its parent? Expect `false`. - Comparing against a Hash from JSON? Convert with `to_h` first or build a value object from the Hash. - Relying on equality of a Struct that other code can mutate? Consider `Data` instead.
- Can a Data instance ever be == to a Struct instance with the same member names and values?No. Both are Struct-family objects internally, but the class check requires the other object's class to be exactly the receiver's class. A Data-generated class and a Struct-generated class are unrelated, so `==` returns false before any member is compared.
- Why does the exact-class rule matter for symmetry?If a parent compared equal to any `is_a?` descendant with matching members, `parent == child` could be true while the child, which might add members or override `==`, says false. Requiring identical classes makes `a == b` and `b == a` always agree.
saying these in an interview costs you the question
- Struct equality compares object identity, like a plain Object.
- Two Data classes with the same members give equal instances for equal values.
- A subclass instance equals its parent's instance when the members match.
- Struct#== compares members with eql?, so 100 and 100.0 differ.
- A Struct compares equal to a Hash with the same keys and values.