Comparable
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".
| Case | Means |
|---|---|
Ordering::Less | this value comes first |
Ordering::Equal | neither comes first |
Ordering::Greater | the 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.
// `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
< 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.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
- Find the oldest release in the array, using
IsLess. - Print
old.Compare(fresh).Reverse()throughDescribe, and compare it withfresh.Compare(old). - Implement
Comparableforstruct Date { year: int32; month: int32; day: int32; }and sort out which of two birthdays comes first.
Learn more
- Enum and Match expression — the tools
Orderingis built from - Sort and Tree map — standard code that relies on an
Ordering - Derived operator —
<,>,<=and>=on your own type - The Core package in the API reference