Equatable
Two values can be "the same" in more than one sense. The fractions 1/2 and 2/4 are written differently, yet they stand for the same number, and a program that calls them different will give wrong answers. Equatable, from the Core package, is the interface a type implements to say what equality means for it — once, in one place, so that every search and every comparison agrees.
The Equatable interface
Equatable asks for one method. In Core it is declared as:
func Equals(other: &Self) -> bool;
Self stands for whichever type is implementing it, so a Fraction is only ever compared with another Fraction. The lesson imports it with import Core::Equatable;, and its Rux.toml lists Core under [Dependencies].
extend Fraction : Equatable {
// Cross-multiplying compares the two numbers without dividing, so no remainder is lost:
// 1/2 and 2/4 are equal because 1 * 4 == 2 * 2.
func Equals(self: &Fraction, other: &Fraction) -> bool {
return self.top * other.bottom == other.top * self.bottom;
}
}
With integers, dividing would make 1/2 and 1/3 both 0 — and so "equal". Cross-multiplying keeps everything whole: two fractions a/b and c/d are equal exactly when a × d equals c × b.
PrintLine("1/2 equals 2/4: {}", half.Equals(twoQuarters));
PrintLine("1/2 equals 1/3: {}", half.Equals(third));
Three promises
Implementing Equatable is also a promise about how Equals behaves, and any code relying on it is entitled to assume all three:
| Promise | In words | Checked in the program by |
|---|---|---|
| Every value equals itself | 1/2 is 1/2 | half.Equals(half) |
| The order does not matter | if 1/2 equals 2/4, then 2/4 equals 1/2 | half.Equals(twoQuarters) == twoQuarters.Equals(half) |
| Equality passes along a chain | 1/2 = 2/4 and 2/4 = 4/8, so 1/2 = 4/8 | the three Equals calls joined with && |
A search, a duplicate check or a hash table relies on these without asking. A type that cannot keep them should not implement Equatable at all. Core's own documentation gives the example of a floating-point NaN, which does not even equal itself.
One meaning, used everywhere
Because the meaning is settled once, every loop that asks agrees with it:
let pile: Fraction[4] = [third, twoQuarters, fourEighths, half];
var halves = 0;
for each in pile {
if each.Equals(half) {
halves += 1;
}
}
twoQuarters, fourEighths and half itself all count, so the pile holds 3 halves.
Equals is not ==
Equals is a method, not the == operator, and implementing Equatable leaves == exactly as it was. That is easy to trip over: half == twoQuarters still compiles, but it compares the fields one by one — 1 against 2, 2 against 4 — and answers false. The Structural equality lesson explains that field-by-field ==, and Operator overload shows how a type gives == a meaning of its own.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Two values can be "the same" in more than one sense. The fractions 1/2 and 2/4 are written
// differently, yet they stand for the same number, and a program that calls them different will
// give wrong answers.
//
// `Equatable`, from Core, is the interface a type implements to say what equality means for it.
// It asks for one method, `Equals(other: &Self) -> bool`. `Self` stands for whichever type is
// implementing it, so a Fraction is only ever compared with another Fraction.
//
// Implementing it is also a promise about how `Equals` behaves, which any code relying on it is
// entitled to assume:
// - every value equals itself;
// - `a.Equals(b)` and `b.Equals(a)` always agree;
// - if a equals b, and b equals c, then a equals c.
import Core::Equatable;
import Io::PrintLine;
struct Fraction {
top: int32;
bottom: int32;
}
extend Fraction : Equatable {
// Cross-multiplying compares the two numbers without dividing, so no remainder is lost:
// 1/2 and 2/4 are equal because 1 * 4 == 2 * 2.
func Equals(self: &Fraction, other: &Fraction) -> bool {
return self.top * other.bottom == other.top * self.bottom;
}
}
func Main() -> int {
let half = Fraction { top: 1, bottom: 2 };
let twoQuarters = Fraction { top: 2, bottom: 4 };
let fourEighths = Fraction { top: 4, bottom: 8 };
let third = Fraction { top: 1, bottom: 3 };
PrintLine("1/2 equals 2/4: {}", half.Equals(twoQuarters));
PrintLine("1/2 equals 1/3: {}", half.Equals(third));
// The three promises, checked on these values.
PrintLine("itself: {}", half.Equals(half));
PrintLine("either way: {}", half.Equals(twoQuarters) == twoQuarters.Equals(half));
PrintLine("in a chain: {}", half.Equals(twoQuarters) && twoQuarters.Equals(fourEighths)
&& half.Equals(fourEighths));
// Because the meaning is settled once, in one place, every search agrees with it.
let pile: Fraction[4] = [third, twoQuarters, fourEighths, half];
var halves = 0;
for each in pile {
if each.Equals(half) {
halves += 1;
}
}
PrintLine("halves in the pile: {}", halves);
// `Equals` is a method, not the `==` operator. Implementing Equatable leaves `==` exactly as
// it was; the lessons on structural equality and operator overloading cover `==`.
return 0;
}
Besides Io, its Rux.toml lists Core under [Dependencies].
Run it
cd Examples/Interfaces/Equatable
rux run
1/2 equals 2/4: true
1/2 equals 1/3: false
itself: true
either way: true
in a chain: true
halves in the pile: 3
Common mistakes
== to call Equals.half == twoQuarters is false, while half.Equals(twoQuarters) is true. The operator compares fields; the method uses your definition. Call Equals when you mean it.Without
import Core::Equatable;, the extend line fails with error: interface 'Equatable' is not defined. Core must also be listed in Rux.toml.Self means a Fraction compares only with a Fraction. half.Equals(5) fails with error: argument 1 to 'Equals' has type 'int', but parameter 'other' requires '&Fraction'.Try it yourself
- Is
Fraction { top: -1, bottom: -2 }equal tohalf? Work it out by cross-multiplying, then check. - Consider
Fraction { top: 0, bottom: 0 }. Compare it withhalfand withthird. Which of the three promises does it break? - Count how many fractions in
pileequalthird.
Learn more
- Comparable — the next step: not just "equal?", but "which comes first?"
- Structural equality — what
==does on a struct - Reference — the
&inother: &Self - The Core package in the API reference