Min and max
"Which is smaller?" is a question about two values, and it always has an answer. "Which is the smallest?" is a question about a slice, and it has none when the slice is empty. The Algorithms package answers the two questions with different functions, and the difference is in their return types.
The smaller of two
Min and Max take two values and return one of them, unchanged:
PrintLine("Min(3, 8) = {}", Min(3, 8));
PrintLine("Max(3, 8) = {}", Max(3, 8));
PrintLine("Max(-2.5, -7.0) = {}", Max(-2.5, -7.0));
They are generic, so they work for any type with < — integers, floats, characters, and your own types once they declare <. Both arguments must have the same type, which is why the last line writes -7.0 and not -7: a lone -7 is an int, and Max cannot take an int and a float64 at once.
A third function in the family, Clamp(value, low, high), keeps a value inside a range: it returns low for anything below it, high for anything above, and the value itself otherwise.
The smallest of many
Over a slice there may be nothing to compare. Rather than invent an answer for an empty slice, MinIndex and MaxIndex return an optional index, uint?, which is none when the slice is empty:
func Report(temperatures: int[..]) {
match MinIndex(temperatures) {
day? => PrintLine("coldest: day {} at {}", day, temperatures[day]),
none => PrintLine("coldest: no readings")
}
match MaxIndex(temperatures) {
day? => PrintLine("warmest: day {} at {}", day, temperatures[day]),
none => PrintLine("warmest: no readings")
}
}
They return the position, not the value. The position is the more useful answer — it says which day was coldest, not just how cold — and the value is one index away: temperatures[day].
flowchart LR
s["MinIndex(temperatures)"] --> q{"is the slice empty?"}
q -- "yes" --> n["none:<br/>no readings"]
q -- "no" --> w["walk the elements,<br/>keeping the first smallest"]
w --> i["day?<br/>its index, a uint"]
i --> v["temperatures[day]<br/>is the value"]| Function | Takes | Returns | Empty input |
|---|---|---|---|
Min(a, b) | two values | T | cannot happen |
Max(a, b) | two values | T | cannot happen |
MinIndex(items) | a slice | uint? | none |
MaxIndex(items) | a slice | uint? | none |
Ties go to the earliest
let week: int[7] = [-3, 4, 11, 0, 11, 6, -3];
Report(week);
Day 4 is as warm as day 2, and day 6 as cold as day 0. Both functions report the earlier day: 0 for the coldest and 2 for the warmest. That is a promise, not an accident, so you can rely on it — the first coldest day is always the one you get.
The empty case
Report(week[..0]);
week[..0] is a view of no elements at all — the same array, cut down to nothing. There is nothing to compare, so both matches take their none arm. Without the optional, the function would have to return something for this case: index 0, which does not exist here, or a made-up number the caller could mistake for a real day.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// `Min` and `Max` pick the smaller or larger of two values. They work for any type with `<`,
// numbers included, and return one of the two unchanged.
//
// Over a whole slice the question changes, because a slice may be empty, and an empty slice has
// no smallest element. Rather than invent one, `MinIndex` and `MaxIndex` answer with an optional
// index, `uint?`, which is `none` for an empty slice. They give the position, not the value: the
// position is the more useful answer (it says *which* day, not just how cold), and the value is
// one index away.
//
// When several elements tie, both report the earliest. That is a promise, not an accident.
import Algorithms::{ Max, MaxIndex, Min, MinIndex };
import Io::PrintLine;
func Report(temperatures: int[..]) {
match MinIndex(temperatures) {
day? => PrintLine("coldest: day {} at {}", day, temperatures[day]),
none => PrintLine("coldest: no readings")
}
match MaxIndex(temperatures) {
day? => PrintLine("warmest: day {} at {}", day, temperatures[day]),
none => PrintLine("warmest: no readings")
}
}
func Main() -> int {
PrintLine("Min(3, 8) = {}", Min(3, 8));
PrintLine("Max(3, 8) = {}", Max(3, 8));
PrintLine("Max(-2.5, -7.0) = {}", Max(-2.5, -7.0));
// Day 4 is as warm as day 2, and day 6 as cold as day 0: the earlier day is reported.
let week: int[7] = [-3, 4, 11, 0, 11, 6, -3];
Report(week);
// An empty view of the same array, `[..0]`. Nothing to compare, so `none` both times.
Report(week[..0]);
return 0;
}
Besides Io, its Rux.toml lists Algorithms under [Dependencies].
Run it
cd Examples/Algorithms/MinMax
rux run
Min(3, 8) = 3
Max(3, 8) = 8
Max(-2.5, -7.0) = -2.5
coldest: day 0 at -3
warmest: day 2 at 11
coldest: no readings
warmest: no readings
Common mistakes
Max(-2.5, -7) fails with error: argument 1 to 'Max' has type 'float64', but parameter 'left' requires 'T'. With no typed partner, -7 is an int and -2.5 a float64, so no single T fits both. Write -7.0.let cold: int = MinIndex(week); fails with error: cannot assign 'uint?' to 'int'. The answer is an optional position: match it, then read week[day].Min.Min(week) fails with error: call to 'Min' expects 2 arguments, but 1 was provided. Min compares two values; for the smallest element of a slice, use MinIndex.Try it yourself
- Print
Clamp(15, 0, 10),Clamp(-3, 0, 10)andClamp(7, 0, 10). Remember to importClamp. - Add a second week of readings and print the coldest day of each, then the colder of the two coldest values with
Min. - Report only the first three days with
week[..3]. Which day is the warmest now? - Use
MinandMaxon twochar8values. Which letter is "smaller"?
Learn more
- Search — the other search whose answer may be
none - Presence — matching
day?andnone - Fold — combining every element into one answer, of which the minimum is one example
- Number limit — the smallest and largest values a type can hold, which is a different question