Skip to content

Pattern matching

Inko supports pattern matching against a variety of types. Pattern matching is performed using the match keyword:

match numbers.get(3) {
  case Some(value) -> value * 2
  case None -> 0
}

Pattern matching is exhaustive: given a value to match against, the compiler ensures all possible cases are handled. It's a compile-time error to not cover all cases.

match expressions are compiled to decision trees. Inko's implementation is based on this algorithm. If you're interested in implementing pattern matching yourself, we suggest using this project as a reference.

For a list of available patterns and their syntax, refer to the pattern matching syntax section.

Literals and constants

Pattern matching against literals is supported for values of type Int, Bool, and String. We can also use constants to specify the pattern:

match 3 {
  case 0 -> 'foo'
  case 1 -> 'bar'
  case _ -> 'baz'
}

match 3 {
  case ZERO -> 'foo'
  case ONE -> 'bar'
  case _ -> 'baz'
}

match true {
  case true -> 'foo'
  case false -> 'bar'
}

match 'world' {
  case 'hello' -> 'foo'
  case 'world' -> 'bar'
  case _ -> 'baz'
}

Pattern matching expressions against these types are compiled to if/else if chains. This means performance of such patterns in O(n) where n is the number of patterns to test for.

When matching against values of these types, Inko treats the values as having an infinite number of possibilities, thus requiring the use of a wildcard pattern (_) to make the match exhaustive. Booleans are an exception to this, as they only have two possible values (true and false).

Enum patterns

If the value matched against is an enum class, we can match against its variants:

match Option.Some(42) {
  case Some(42) -> 'yay'
  case None -> 'nay'
}

In this case the compiler knows about all possible variants, and the use of wildcard patterns isn't needed when all variants are covered explicitly.

When specifying the variant pattern only the name of the variant is needed, the name of the type it belongs to isn't needed. This means case Option.Some(42) isn't valid.

Matching against variants is performed using a jump table generated by the compiler. This means performance is O(1), regardless of the number of variants to match against.

Class patterns

Pattern matching can also be performed against regular classes using class literal patterns:

class Person {
  let @name: String
  let @age: Int
}

let person = Person { @name = 'Alice', @age = 42 }

match person {
  case { @name = name, @age = 42 } -> name
  case { @name = name, @age = age } -> name
}

Any fields left out are treated as wildcard patterns, meaning the following two match expressions are the same:

match person {
  case { @name = name } -> name
}

match person {
  case { @name = name, @age = _ } -> name
}

Class literal patterns are only available for regular classes.

Tuple patterns

Pattern matching against tuples is also supported:

match (10, 'testing') {
  case (num, 'testing') -> num * 2
  case (_, _) -> 0
}

OR patterns

Multiple patterns can be specified at once using the or keyword:

match number {
  case 10 or 20 or 30 -> 'yay'
  case _ -> 'nay'
}

This is the same as the following match:

match number {
  case 10 -> 'yay'
  case 20 -> 'yay'
  case 30 -> 'yay'
  case _ -> 'nay'
}

Bindings

Patterns can bind sub values to variables:

match number {
  case 10 -> 20
  case num -> num * 2
}

Nested patterns

Nested patterns are also supported:

match Option.Some((10, 'testing')) {
  case Some((10, 'testing')) -> 'foo'
  case Some((num, _)) -> 'bar'
  case None -> 'baz'
}

Guards

Each pattern can specify an extra condition to evaluate before considering the pattern to be matched, known as a "guard":

match (10, 'testing') {
  case (num, 'testing') if num > 10 and num < 20 -> 'yay'
  case (_, _) -> 'nay'
}

Guards are evaluated after the initial pattern matched, and must return a value of type Boolean.

Typing rules

For a given match expression, all pattern bodies must return a value of which the type is compatible with the type of the first pattern's body:

match 42 {
  case 42 -> 'foo'
  case 50 -> 'bar'
  case _ -> 'baz'
}

Here the first pattern's body returns 'foo', a value of type String, and thus all other pattern bodies must return value compatible with this type.

If pattern bodies return different types but you don't care about them, you can use nil like so:

match 42 {
  case 42 -> {
    foo
    nil
  }
  case 50 -> {
    bar
    nil
  }
  case _ -> {
    baz
    nil
  }
}

Ownership and moves

When pattern matching against an owned value, the value is moved into the match expression. When matching the value's components, the input value is destructured into those components. Consider this example:

let input = Option.Some((10, 'testing'))

match input {
  case Some((num, string)) -> num
  case _ -> 0
}

When the Some((num, string)) pattern matches, the value 10 is moved into num, and the value testing is moved into string. The variable input is no longer available. When num or string is dropped, so is the value it points to.

If the value matched against is a reference, the match is performed against the reference. In this case any (sub) values bound are exposed as references:

let input = Option.Some((10, 'testing'))

match ref input {
  # Here `string` is of type `ref String`, not `String`, because the input is a
  # ref and not an owned value.
  case Some((num, string)) -> string
  case _ -> 0
}

Drop semantics

When pattern matching, any bindings introduced as part of a pattern are dropped at the end of the pattern's body (unless they are moved before then). When matching against an owned value and the value is destructured, Inko performs a "partial drop" of the outer value before entering the pattern body. A partial drop is a regular drop that doesn't invoke the type's destructor, and doesn't drop any fields:

match Option.Some(42) {
  case Some(value) -> { # <- The Option is dropped here, but not the value
                        #    wrapped by the Some.
    value
  }
  case None -> 0
}

For references we just drop the reference before entering the pattern body:

match values.get(4) {
  case Some(42) -> {
    # This is valid because the `ref Option[Int]` returned by `values.get(4)` is
    # dropped before we enter this body. If we didn't, the line below would
    # panic, because we'd try to drop the old value of `values.get(4)` while a
    # reference to it still exists.
    values.set(4, Option.Some(0))
  }
  case _ -> nil
}

Limitations

  • Range patterns aren't supported, instead you can use pattern guards.
  • Types defining custom destructors can't be matched against.
  • async classes can't be matched against.
  • Matching against Float isn't supported, as you'll likely run into precision/rounding errors.