OxL Reference

Step 4. Pattern matching

Why#

match is an if/else that can take data apart and that the compiler checks for completeness. If you forget a variant of an enum or a type of a union, the program does not build. That is why match is the main way in OxL to work with an enum, with results that may be an error and with optional values.

Patterns#

oxl✓ checked in CI
enum Cmd {
  Move(dx: i32, dy: i32)
  Say(text: str)
  Quit
}

data User { name: str; age: i32 }

fn classify(n: i32) -> str =>
  match n {
    0 => "zero"
    1..=9 => "digit"  // a range
    x if x < 0 => "negative"  // a guard: a pattern plus a condition
    _ => "big"  // _ matches everything else
  }

fn run(c: Cmd) -> str =>
  match c {
    Move(0, 0) => "stay"  // literals inside a variant
    Move(dx, dy) => "move {dx},{dy}"
    Say(t) => "say {t}"
    Quit => "bye"
  }

fn first(xs: List<i32>) -> str =>
  match xs {
    [] => "empty"  // list patterns
    [x] => "one {x}"
    [x, ..rest] => "{x} and {rest.len()} more"
  }

fn main() io {
  for n in [0, 5, -3, 100] { print("{classify(n)}") }
  for c in [Cmd.Move(0, 0), Cmd.Move(2, 3), Cmd.Say("hi"), Cmd.Quit] { print(run(c)) }
  print("{first([])} | {first([7])} | {first([1, 2, 3])}")
  let u = User{ name: "Ann", age: 31 }
  if let User{ name, age, .. } = u {  // if let destructures a data value when the pattern fits
    print("{name} is {age}")
  }
  let (a, _, c) = (1, 2, 3)
  print("{a + c}")
}
Output
zero
digit
negative
big
stay
move 2,3
say hi
bye
empty | one 7 | 1 and 2 more
Ann is 31
4

Walkthrough#

  • Patterns are tried top to bottom. A literal (0), a range (1..=9), the wildcard _, a name binding (x) and a guard x if x < 0 are all patterns.
  • Move(0, 0) is a variant pattern with literals inside; Move(dx, dy) binds the fields. Patterns can be nested.
  • [], [x], [x, ..rest] are list patterns: empty, one element, "head and tail".
  • if let User{ name, age, .. } = u { … } runs the block only if the pattern matched; .. skips the remaining fields.
  • A match must be exhaustive: a final _ branch or a full enumeration of the variants.

Unions and optional values#

The type A | B is a value of one of the types. T? is shorthand for T | none. A union can be taken apart with match and a type pattern (i32 n) or tested with the is operator: after if r is TooBig the variable r already has type TooBig inside the branch.

oxl✓ checked in CI
type Port = u16 where 1..=65535  // a refined type: the range is checked on creation

@error data TooBig { max: i32 }

fn describe(x: i32 | str | bool) -> str =>  // a union: a value of one of the types
  match x {
    i32 n => "int {n}"  // a type pattern with a binding
    str s => "text {s}"
    bool b => "flag {b}"
  }

fn limit(n: i32) -> i32 | TooBig {
  if n > 100 { fail TooBig{ max: 100 } }
  n
}

fn main() io {
  print(describe(5))
  print(describe("hi"))
  print(describe(true))
  let r = limit(500)
  if r is TooBig {  // is narrows the type inside the branch
    print("too big, max {r.max}")
  }
  let maybe: i32? = none
  print("{maybe ?? 0} {maybe is none}")
  let name: str? = "Ann"
  print("{name?.len() ?? 0}")  // ?. calls only if there is a value
  match Port.from(80) {
    Port => print("port ok")
    InvalidValue => print("port rejected")
  }
  match Port.from(0) {
    Port => print("port ok")
    InvalidValue => print("port rejected")
  }
}
Output
int 5
text hi
flag true
too big, max 100
0 true
3
port ok
port rejected

What is new here:

  • type Port = u16 where 1..=65535 is a refined type: the value is checked on creation. Port.from(80) returns Port | InvalidValue, and Port.from(0) is InvalidValue.
  • name?.len() is a safe call: if there is no value the result is none, and ?? 0 substitutes zero.
  • Errors are ordinary types in a union too (i32 | TooBig); more in the next step.

Common mistake: an incomplete match#

oxl✓ checked in CI
enum Role { User, Support, Admin }

fn label(r: Role) -> str {
  match r {
    Role.User => "user"
    Role.Support => "support"
  }
}
error
non-exhaustive

The compiler tells which variant is not covered: non-exhaustive. The fix is to add a branch Role.Admin => … or _ => …. Prefer explicit branches: then when a new variant is added you get a compile error instead of silently falling into _. A branch that can never run is flagged as unreachable-arm.

Remember#

  • match is an expression and is always exhaustive.
  • Patterns: literals, ranges, variants, data, tuples, lists, alternatives A | B, guards if.
  • is and match narrow the type; ?. and ?? work with T?.

Next: errors.

See also

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides