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

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