stdlib
stdlib copied to clipboard
add an order function to help chain comparisons
When writing a comparison function, a common pattern is to compare one thing, then if its equal compare another thing and so on. e.g.
fn compare_cat(a: Cat, b: Cat) -> Order {
case string.compare(a.name, b.name) {
Eq ->
case int.compare(a.age, b.age) {
Eq -> int.compare(a.cuteness, b.cuteness)
x -> x
}
x -> x
}
}
We could add some sort of "chain" function to make this nicer
fn compare_cat2(a: Cat, b: Cat) -> Order {
string.compare(a.name, b.name)
|> chain(int.compare(a.age, b.age))
|> chain(int.compare(a.cuteness, b.cuteness))
}
fn chain(first: Order, then: Order) {
case first {
Eq -> then
x -> x
}
}
Not sure if this name and/or api is ideal, but I think a function similar to this would be helpful
Hello! I remember feeling the need for something similar in the past, I called it break_ties, you can achieve something similar using bool.guard:
fn compare_cat2(a: Cat, b: Cat) -> Order {
let name_order = string.compare(a.name, b.name)
use <- bool.guard(when: name_order != Eq, return: name_order)
// ^ if the name comparison is not `Eq`, we return it immediately
// otherwise we go on and compare the rest.
let age_order = int.compare(a.age, b.age)
use <- bool.guard(when: age_order != Eq, return: age_order)
// ^ if the age comparison is not `Eq`, we return it immediately
// otherwise we go on and compare the rest
int.compare(a.cuteness, b.cuteness)
// ^ if everything else is equal, the comparison on `cuteness`
// will decide the outcome of the comparison
}
Sounds useful! A lazy variant would be useful too.
I don't feel very strongly about the name in either direction. What other options do we have? Just see if there's anything better we can go with before this
How about a lazy implementation named then to mirror Option.then and Result.then?
pub fn then(order: Order, compare: fn(a, a) -> Order, a1: a, to a2: a) -> Order {
case order {
Eq -> compare(a1, a2)
_ -> order
}
}
or, using bool.guard:
pub fn then(order: Order, compare: fn(a, a) -> Order, a1: a, to a2: a) -> Order {
use <- bool.guard(when: order != Eq, return: order)
compare(a1, a2)
}
Then we might compare cats like so:
pub fn compare_cat(a: Cat, b: Cat) -> Order {
string.compare(a.name, b.name)
|> order.then(int.compare, a.age, to: b.age)
|> order.then(int.compare, a.cuteness, to: b.cuteness)
}
As an aside, I see that string.compare does not name its 2nd parameter, but int.compare names it with. Here, I've named it to (as I personally tend to use the phrase "compare to", not "compare with", but that's a personal preference). In any case all, of the compare functions should be consistent with each other.
What's the advantage here of making it a higher order function over working with 3 orders?
I think it'd be easier to use one that works directly with an order:
pub fn break_tie(in order: Order, with comparison: Order) -> Order {
case order {
Lt | Gt -> order
Eq -> comparison
}
}
pub fn lazy_break_tie(in order: Order, with comparison: fn() -> Order) -> Order {
case order {
Lt | Gt -> order
Eq -> comparison()
}
}
That could then be used like this:
// Lazy version that plays nicely with `use`
use <- order.lazy_break_tie(in: string.compare(a.name, b.name))
use <- order.lazy_break_tie(in: int.compare(a.age, b.age))
int.compare(a.cuteness, b.cuteness)
// Non lazy version
string.compare(a.name, b.name)
|> order.break_tie(with: int.compare(a.age, b.age))
|> order.break_tie(with: int.compare(a.cuteness, b.cuteness))
I'm not really sure what a good name would be here, I'm just going with break_tie out of habit :)
break_tie sounds nice and descriptive to me.
I wonder if there's another word that means this. Perhaps in the context of a competition or tournament. I can't think of anything right now