Optionals · Lesson 8.6

Nested optional

Source
Tell two kinds of absence apart with int??, an optional of an optional.
You'll need: Presence, Coalesce

A player who has not played yet has no score, so this lesson's team sheet stores each score as an int?. Now look a player up by number. The answer can be missing for two quite different reasons — nobody wears that number, or the player exists but has no score yet. An optional of an optional, int??, keeps the two apart.

Three answers, one type

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

A lookup that returns the player's score must also be able to say "not found", so its result wraps the int? in one more optional:

flowchart TD
    r["int??"] --> n["none<br/>nobody wears that number"]
    r --> s[".Some(…) — the player exists"]
    s --> sn[".Some(none)<br/>no score yet"]
    s --> ss[".Some(.Some(score))<br/>has scored"]

Rux never squashes the two levels into one. In some languages "a missing missing value" collapses into plain "missing", and the difference is lost; here the two kinds of "nothing" stay different, and you can tell them apart.

Building one

player.score is an int?. Returned where an int?? is expected it becomes a present int?? on its own, even when the score inside is none — the same rule that turned an int into a present int? in Optional. Only the none at the bottom means "not found":

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

Opening one, level by level

Each ? in a pattern opens one level:

match ScoreOf(players, number) {
    score?? => PrintLine("#{} scored {}", number, score),
    none? => PrintLine("#{} has not played yet", number),
    none => PrintLine("#{} is not on the team", number)
}
ShortLongMeaning
score??.Some(.Some(score))a present, present score
none?.Some(none)a present none: found, but no score yet
nonenonethe outer absence: not on the team

The long spelling, used in ReportLong, shows the nesting plainly; the short one is the usual one.

?? opens one level too

?? removes exactly one level, so on an int?? its result is still an int?:

let once = ScoreOf(players, 9) ?? 0;
let twice = once ?? 0;

For #9 the outer level is present, so the fallback is not used, and once is the inner none. A second ?? deals with that one, and twice is a plain int, 0.

That first ?? 0 looks as if it gives an int, and it does compile — but only because the 0 is made into a present int? to match the inner level. once is an int?, which is why the program compares it with none instead of printing it.

The program

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

Src/Main.rux
// A player who has not played yet has no score, so the team sheet stores each
// score as an `int?`. Now look a player up by number. The answer can be
// missing for two different reasons:
//
//     none                   nobody wears that number
//     .Some(none)            the player exists, but has no score yet
//     .Some(.Some(score))    the player exists and has scored
//
// The type that holds all three is `int??`, an optional of an optional. Rux
// never squashes the two levels into one, so the two kinds of "nothing" stay
// different and you can tell them apart.
import Io::PrintLine;

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

// `player.score` is an `int?`. Returned from here it becomes a present `int??`
// on its own, even when the score inside is `none`. Only the `none` at the
// bottom means "not found".
func ScoreOf(players: Player[..], number: int) -> int?? {
    for player in players {
        if player.number == number {
            return player.score;
        }
    }
    return none;
}

// Each `?` in a pattern opens one level. `score??` is "a present, present
// score". `none?` is "a present `none`": the player was found but has no
// score. Plain `none` is the outer absence.
func Report(players: Player[..], number: int) {
    match ScoreOf(players, number) {
        score?? => PrintLine("#{} scored {}", number, score),
        none? => PrintLine("#{} has not played yet", number),
        none => PrintLine("#{} is not on the team", number)
    }
}

// The same three cases in the long spelling.
func ReportLong(players: Player[..], number: int) {
    match ScoreOf(players, number) {
        .Some(.Some(score)) => PrintLine("#{} scored {}", number, score),
        .Some(none) => PrintLine("#{} has not played yet", number),
        none => PrintLine("#{} is not on the team", number)
    }
}

func Main() -> int {
    let players = [
        Player { number: 7, score: 12 },
        Player { number: 9, score: none }
    ];

    Report(players, 7);
    Report(players, 9);
    Report(players, 3);
    ReportLong(players, 9);

    // `??` opens just one level too. For #9 the outer level is present, so
    // the fallback is not used and `once` is the inner `none`, still an
    // `int?`. A second `??` deals with that one.
    let once = ScoreOf(players, 9) ?? 0;
    let twice = once ?? 0;
    PrintLine("#9 opened once is still none: {}", once == none);
    PrintLine("#9 opened twice: {}", twice);

    // Watch out: that first `?? 0` looks as if it gives an `int`, and it does
    // compile, but only because the `0` is made into a present `int?` to
    // match the inner level. Try printing `once` directly: the compiler
    // refuses, because an `int?` is not something `PrintLine` can show.
    return 0;
}

Run it

cd Examples/Optionals/NestedOptional
rux run
#7 scored 12
#9 has not played yet
#3 is not on the team
#9 has not played yet
#9 opened once is still none: true
#9 opened twice: 0

Common mistakes

Treating the result of one ?? as plain.
PrintLine("{}", once); fails with error: argument 2 to 'PrintLine' has type 'int?', but variadic parameter 'args' requires 'Display'. One ?? opened only the outer level; open the inner one too.
Chaining the two fallbacks without parentheses.
?? groups from the right, so ScoreOf(players, 9) ?? 0 ?? 0 means ScoreOf(players, 9) ?? (0 ?? 0) and fails with error: operator '??' requires an optional left operand, but found 'int'. Write (ScoreOf(players, 9) ?? 0) ?? 0, or use two lets as the lesson does.
Forgetting the middle case.
A match with only score?? and none arms is not exhaustive: error: match on 'int??' is not exhaustive; missing .Some(none). The player who exists but has no score needs its own arm.

Try it yourself

  1. Add a player Player { number: 11, score: 0 } and check that #11 is reported as having scored 0, not as "has not played yet".
  2. Write Played(players: Player[..], number: int) -> bool? that is none for an unknown number, and true or false otherwise.
  3. Replace the three-arm match with ??: print -1 for "not on the team" and 0 for "has not played yet".

Learn more

  • Presence — the one-level patterns score? and none
  • Optional pointer — the same care with (*T)? and *T?, in the Memory part
  • Nested fallible — an optional inside a fallible, int? ! E, in the Errors part