Guide

Cases and matching

Declare a type whose value is one of several cases, and take it apart with a match that has to handle every case.

A type can list cases: the shapes its value can take. This covers what other languages call an enum, a sealed class or a tagged union.

Goal#

At the end of this page you can declare a type with cases and write a match that handles all of them.

Declare cases#

type Shape {
  case Circle(radius: Float)
  case Rectangle(width: Float, height: Float)
  case Empty

  fn area(): Float {
    match self {
      .Circle(radius) => 3.0 * radius * radius
      .Rectangle(width, height) => width * height
      .Empty => 0.0
    }
  }
}

const shapes = [Shape.Circle(2.0), Shape.Rectangle(2.0, 3.0), Shape.Empty]
print shapes.map({ _.area() }).toList()
// prints [12.0, 6.0, 0.0]

A case can carry values, like Circle(radius: Float), or none, like Empty. Write it with its type, Shape.Circle, or with a dot, .Circle, where the type is already clear. A case is never bare unless the file imports it, and every file imports Some, None, Ok and Fail.

A match handles every case#

Does not compile
type Shape {
  case Circle(radius: Float)
  case Empty
}

fn area(shape: Shape): Float {
  match shape {
    .Circle(radius) => 3.0 * radius * radius
  }
}
// error: `match` does not handle `.Empty`

A match is an expression, and it must handle every case. So when you add a case later, the compiler lists every match that has to learn about it.

More than the case#

fn describe(value: Int): String {
  match value {
    0 => "zero"
    1 | 2 | 3 => "small"
    4..=9 => "medium"
    number if number < 0 => "negative"
    _ => "large"
  }
}

print describe(-5)
print describe(7)
// prints negative
// prints medium

A pattern can be a literal, several joined with |, a range, or a name with a condition after if. A lowercase name takes the value, and _ matches anything and keeps nothing. A name that the arm never uses is an error: write _.

Next#

Esc