Interfaces · Lesson 12.5

Equatable

Source
State what equality means for a type by implementing Core's Equatable and its Equals method.
You'll need: Interface, Reference

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:

PromiseIn wordsChecked in the program by
Every value equals itself1/2 is 1/2half.Equals(half)
The order does not matterif 1/2 equals 2/4, then 2/4 equals 1/2half.Equals(twoQuarters) == twoQuarters.Equals(half)
Equality passes along a chain1/2 = 2/4 and 2/4 = 4/8, so 1/2 = 4/8the 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.

Src/Main.rux
// 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

Expecting == 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.
Forgetting the import.
Without import Core::Equatable;, the extend line fails with error: interface 'Equatable' is not defined. Core must also be listed in Rux.toml.
Comparing with a different type.
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

  1. Is Fraction { top: -1, bottom: -2 } equal to half? Work it out by cross-multiplying, then check.
  2. Consider Fraction { top: 0, bottom: 0 }. Compare it with half and with third. Which of the three promises does it break?
  3. Count how many fractions in pile equal third.

Learn more