Algorithms · Lesson 18.4

Min and max

Source
Pick the smaller or larger of two values with Min and Max, and find the extremes of a slice with MinIndex and MaxIndex, which return none for an empty one.
You'll need: Search, Presence

"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"]
FunctionTakesReturnsEmpty input
Min(a, b)two valuesTcannot happen
Max(a, b)two valuesTcannot happen
MinIndex(items)a sliceuint?none
MaxIndex(items)a sliceuint?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.

Src/Main.rux
// `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

Mixing an integer literal with a float.
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.
Treating the index as the value.
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].
Passing a slice to 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

  1. Print Clamp(15, 0, 10), Clamp(-3, 0, 10) and Clamp(7, 0, 10). Remember to import Clamp.
  2. Add a second week of readings and print the coldest day of each, then the colder of the two coldest values with Min.
  3. Report only the first three days with week[..3]. Which day is the warmest now?
  4. Use Min and Max on two char8 values. Which letter is "smaller"?

Learn more

  • Search — the other search whose answer may be none
  • Presence — matching day? and none
  • 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