Optionals · Lesson 8.5

Optional propagate

Source
Pass absence up to the caller with ? in a function that returns an optional.
You'll need: Presence, Coalesce

Sometimes a function cannot do anything useful with a missing value except report that it is missing too. How far ahead of #7 is #10? If either player is unknown there is no answer, and the honest result is none. A postfix ? writes exactly that: "if this is absent, so is my answer".

Two matches, or one ?

With match, Lead would need two nested matches and two none arms that say the same thing. A ? after an optional says it for you:

func Lead(players: Player[..], a: int, b: int) -> int? {
    return ScoreOf(players, a)? - ScoreOf(players, b)?;
}

If the optional is present, ScoreOf(players, a)? is the int inside it. If it is none, the function returns none straight away, and nothing after it runs. The subtraction can therefore be written as if both scores were plain numbers.

flowchart LR
    e["ScoreOf(players, a)?"] --> q{"Present?"}
    q -- "yes" --> v["the int inside;<br/>the expression goes on"]
    q -- "no" --> r["Lead returns none<br/>at once"]
    r --> c["the caller decides:<br/>match or ??"]

Each line produces its value or ends the function

? works anywhere in the function, including in a let inside a loop:

func TeamTotal(players: Player[..], numbers: int[..]) -> int? {
    var total = 0;
    for number in numbers {
        let score = ScoreOf(players, number)?;
        total += score;
    }
    return total;
}

For the starters 4, 7, 10 every lookup succeeds and the total is 45. For 4, 9, 10, #9 is unknown: the ? returns none from TeamTotal in the middle of the loop, and the partial total is never seen.

The caller handles it

? only moves absence up one level. Somewhere a caller has to deal with it, with a match or a fallback:

PrintLine("starters total: {}", TeamTotal(players, starters) ?? -1);
PrintLine("mixed total:    {}", TeamTotal(players, mixed) ?? -1);

? compared with ?? return

The two look alike, and the difference is what the function returns:

WrittenWhen the value is noneThe function must return
ScoreOf(…)?returns nonean optional, such as int?
ScoreOf(…) ?? return 0returns the value you nameanything that value fits

So ? can only appear in a function whose own result is optional: it needs a none it can return. Main returns a plain int, which is why Main opens the results with match and ?? instead.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Sometimes a function cannot do anything useful with a missing value except
// report that it is missing too. How far ahead is #7 of #10? If either player
// is unknown, there is no answer, and the honest result is `none`.
//
// Writing that with `match` takes two nested matches and two `none` arms that
// say the same thing. A `?` after an optional says it for you:
//
//     ScoreOf(players, a)?
//
// If the optional is present, this is the `int` inside it. If it is `none`, the
// function returns `none` straight away, and nothing after it runs. So `?` can
// only appear in a function whose own result is optional: it needs a `none` it
// can return.
import Io::PrintLine;

struct Player {
    number: int;
    score: int;
}

func ScoreOf(players: Player[..], number: int) -> int? {
    for player in players {
        if player.number == number {
            return player.score;
        }
    }
    return none;
}

// Both scores are needed, and either may be missing. The `?`s let the sum be
// written as if they were plain numbers.
func Lead(players: Player[..], a: int, b: int) -> int? {
    return ScoreOf(players, a)? - ScoreOf(players, b)?;
}

// `?` works anywhere in the function, including in a `let`. Each line either
// produces its value or ends the function.
func TeamTotal(players: Player[..], numbers: int[..]) -> int? {
    var total = 0;
    for number in numbers {
        let score = ScoreOf(players, number)?;
        total += score;
    }
    return total;
}

func Main() -> int {
    let players = [
        Player { number: 4, score: 8 },
        Player { number: 7, score: 12 },
        Player { number: 10, score: 25 }
    ];

    // The caller is where absence is finally handled, with `match` or `??`.
    match Lead(players, 10, 7) {
        lead? => PrintLine("#10 leads #7 by {}", lead),
        none => PrintLine("no answer")
    }
    match Lead(players, 10, 3) {
        lead? => PrintLine("#10 leads #3 by {}", lead),
        none => PrintLine("#10 against #3: no answer, #3 is not on the team")
    }

    let starters = [ 4, 7, 10 ];
    let mixed = [ 4, 9, 10 ];
    PrintLine("starters total: {}", TeamTotal(players, starters) ?? -1);
    PrintLine("mixed total:    {}", TeamTotal(players, mixed) ?? -1);

    // Watch out: `?` is not available in `Main`, or in any function that
    // returns a plain `int`. There is no `none` to hand back, so the compiler
    // rejects it. Use `match` or `??` there instead.
    return 0;
}

Run it

cd Examples/Optionals/OptionalPropagate
rux run
#10 leads #7 by 13
#10 against #3: no answer, #3 is not on the team
starters total: 45
mixed total:    -1

Common mistakes

? in a function that cannot return none.
In Main, or any function returning a plain int, let v = ScoreOf(players, 7)?; stops with error: '?' propagates the absence of 'int?', but the enclosing function returns 'int'. The compiler's help says what to do: declare an optional result, as in -> T?, or supply a fallback with ??.
Propagating when a fallback is meant.
? makes the whole function's answer none. If one missing score should simply count as zero, write ScoreOf(players, number) ?? 0 and keep the rest of the total.

Try it yourself

  1. Write Average(players: Player[..], a: int, b: int) -> int? with two ?s, and show it for a known and an unknown pair.
  2. Rewrite Lead with nested matches and compare the length.
  3. Change TeamTotal so that an unknown number is skipped rather than ending the total. Which operator did you use instead of ??

Learn more

  • Presence and Coalesce — the two ways a caller finally handles absence
  • Coalesce exit — ?? return for functions that do not return an optional
  • Propagate — the same ? passing an error up, in the Errors part