Interfaces · Lesson 12.6

Comparable

Source
Order values of a type by implementing Core's Comparable, whose Compare answers with an Ordering.

Equatable answers "are these the same?". Sorting, searching and finding the largest need a different question: which of two comes first? Comparable, from the Core package, is the interface a type implements to answer it.

Release numbers show why a type may need an order of its own. As text, "1.10.0" sorts before "1.9.4", because the character 1 comes before 9. Compared part by part, as numbers, 1.10.0 is the newer release.

Ordering: three answers in one

Comparable asks for one method. In Core it is declared as:

func Compare(other: &Self) -> Ordering;

The answer is not a bool but an Ordering, an enum from Core with three cases. One call answers "before, same or after?", where a bool would need two questions to tell "after" from "the same".

CaseMeans
Ordering::Lessthis value comes first
Ordering::Equalneither comes first
Ordering::Greaterthe other value comes first

Ordering has small methods that ask about the answer: IsLess, IsEqual, IsGreater, IsLessOrEqual and IsGreaterOrEqual, plus Reverse, which swaps Less and Greater. The program uses two of them. Both names are imported with import Core::{ Comparable, Ordering };.

Comparing part by part

A helper orders two plain numbers:

// Orders two numbers. Release compares its three parts with it.
func CompareNumbers(left: int32, right: int32) -> Ordering {
    if left < right {
        return Ordering::Less;
    }
    if left > right {
        return Ordering::Greater;
    }
    return Ordering::Equal;
}

Release then compares its parts in order of importance, and the first part that differs decides:

extend Release : Comparable {
    // The first part that differs decides; a later part matters only on a tie.
    func Compare(self: &Release, other: &Release) -> Ordering {
        let major = CompareNumbers(self.major, other.major);
        if !major.IsEqual() {
            return major;
        }
        let minor = CompareNumbers(self.minor, other.minor);
        if !minor.IsEqual() {
            return minor;
        }
        return CompareNumbers(self.patch, other.patch);
    }
}
flowchart LR
    major{"major<br/>differs?"} -- "yes" --> a1["that answer"]
    major -- "no, a tie" --> minor{"minor<br/>differs?"}
    minor -- "yes" --> a2["that answer"]
    minor -- "no, a tie" --> patch["compare patch:<br/>its answer is final"]

For 1.9.4 against 1.10.0, the majors tie at 1, and the minors decide: 9 is less than 10, so the answer is Less and patch is never looked at.

Turning an answer into words

A match expression maps each case to a phrase, and the compiler checks that all three are covered:

func Describe(order: Ordering) -> char8[..] {
    return match order {
        .Less => "older than",
        .Equal => "the same as",
        .Greater => "newer than"
    };
}

Finding the newest

var newest = releases[0];
for each in releases {
    if each.Compare(newest).IsGreater() {
        newest = each;
    }
}

Keep whichever compares greater, and after the loop newest is 2.0.1. Sorting works on the same idea: the Sort lesson's SortBy takes a function that answers with an Ordering.

The promise

As with Equatable, implementing Comparable is a promise. For any pair exactly one of the three answers holds; swapping the two sides swaps Less and Greater; and if a comes before b and b before c, then a comes before c. A type that implements both interfaces should also keep them in step: Compare says Equal exactly when Equals says true.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// `Comparable`, from Core, is the interface a type implements to say how its values are ordered:
// which of two comes first. It asks for one method:
//     func Compare(other: &Self) -> Ordering
// The answer is not a `bool` but an `Ordering`, an enum from Core with three cases: `Less`,
// `Equal` and `Greater`. One call answers "before, same or after?", where a `bool` would need two
// questions to tell "after" from "the same".
//
// Release numbers show why a type may need its own order. As text, "1.10.0" sorts before "1.9.4",
// because the character 1 comes before 9. Compared part by part, as numbers, 1.10.0 is newer.
//
// The promise that comes with Comparable: exactly one of the three answers holds for any pair,
// swapping the two sides swaps `Less` and `Greater`, and if a < b and b < c then a < c.
import Core::{ Comparable, Ordering };
import Io::PrintLine;

struct Release {
    major: int32;
    minor: int32;
    patch: int32;
}

// Orders two numbers. Release compares its three parts with it.
func CompareNumbers(left: int32, right: int32) -> Ordering {
    if left < right {
        return Ordering::Less;
    }
    if left > right {
        return Ordering::Greater;
    }
    return Ordering::Equal;
}

extend Release : Comparable {
    // The first part that differs decides; a later part matters only on a tie.
    func Compare(self: &Release, other: &Release) -> Ordering {
        let major = CompareNumbers(self.major, other.major);
        if !major.IsEqual() {
            return major;
        }
        let minor = CompareNumbers(self.minor, other.minor);
        if !minor.IsEqual() {
            return minor;
        }
        return CompareNumbers(self.patch, other.patch);
    }
}

func Describe(order: Ordering) -> char8[..] {
    return match order {
        .Less => "older than",
        .Equal => "the same as",
        .Greater => "newer than"
    };
}

func Main() -> int {
    let old = Release { major: 1, minor: 9, patch: 4 };
    let fresh = Release { major: 1, minor: 10, patch: 0 };

    PrintLine("1.9.4 is {} 1.10.0", Describe(old.Compare(fresh)));
    PrintLine("1.10.0 is {} 1.9.4", Describe(fresh.Compare(old)));
    PrintLine("1.9.4 is {} 1.9.4", Describe(old.Compare(old)));

    // Finding the newest: keep whichever compares greater.
    let releases: Release[4] = [old, Release { major: 2, minor: 0, patch: 1 }, fresh,
                                Release { major: 2, minor: 0, patch: 0 }];
    var newest = releases[0];
    for each in releases {
        if each.Compare(newest).IsGreater() {
            newest = each;
        }
    }
    PrintLine("newest: {}.{}.{}", newest.major, newest.minor, newest.patch);
    return 0;
}

Besides Io, its Rux.toml lists Core under [Dependencies].

Run it

cd Examples/Interfaces/Comparable
rux run
1.9.4 is older than 1.10.0
1.10.0 is newer than 1.9.4
1.9.4 is the same as 1.9.4
newest: 2.0.1

Common mistakes

Expecting < after implementing Comparable.
Compare is a method, and implementing Comparable adds no operators. old < fresh fails with error: operator '<' is not defined for 'Release', and the compiler adds the note "a struct is compared through the operators it declares, never by its representation". Use old.Compare(fresh).IsLess(), or declare < as in Operator overload.
Comparing the parts in the wrong order.
The compiler cannot catch this one. Compare patch first and 1.9.4 comes out newer than 1.10.0, because 4 is greater than 0. The most important part must be compared first, and a later part consulted only on a tie.

Try it yourself

  1. Find the oldest release in the array, using IsLess.
  2. Print old.Compare(fresh).Reverse() through Describe, and compare it with fresh.Compare(old).
  3. Implement Comparable for struct Date { year: int32; month: int32; day: int32; } and sort out which of two birthdays comes first.

Learn more