Skip to content
A value that may be missing

A value that may be missing

Looking something up can fail to find it. qdx has no null, so a function that may have nothing to return says so in its type, with a union of two cases.

record result { player: bytes, points: int }

union results { no_results(), result_then(first: result, rest: results) }

union lookup { missing(), found(points: int) }

fn points_of(player: bytes, all: results) -> lookup {
  match all {
    no_results() => missing(),
    result_then(first, rest) =>
      if bytes_equal(first.player, player) { found(points: first.points) }
      else { points_of(player, rest) }
  }
}

fn standing(player: bytes, all: results) -> bytes {
  match points_of(player, all) {
    missing() => concat(player, " has not played"),
    found(points) => concat3(player, " has ", int_to_text(points))
  }
}

fn main() -> int {
  let sheet = result_then(first: result(player: "mira", points: 17), rest: no_results());
  print(concat4(standing("mira", sheet), "\n", standing("ines", sheet), "\n"))
}
bootstrap/qdx run std/*.qdx lookup.qdx
mira has 17
ines has not played

Why this is better than a special number

Returning -1 for “not found” works until -1 is a real score, and nothing makes the caller check. With a union, the caller cannot reach the points without a match, and the match cannot leave out the missing case.

When to stop instead

Use a union when a missing value is an ordinary outcome that the caller will handle. Use panic when it means the program or its input is broken and there is nothing sensible to do next. The score program in the guide stops on a malformed line for that reason.

One union per kind of answer

Without generic types, each kind of value needs its own union: lookup here holds an int, and a lookup that returned bytes would need another, with differently named cases.