Skip to content

Pattern matching

Inko doesn't have a switch expression. Instead, it has a match expression. The match expression provides a limited form of pattern matching, similar to that of Kotlin's when expression.

The syntax for pattern matching is as follows:

match(let variable = input) {
  pattern -> { body }
  else -> { body }
}

Here input is the input to apply patterns to. The use of parentheses is required. let variable = ... binds the input to the variable variable, which is only available inside the match (called a match binding). pattern -> { body } is how a pattern is specified, with body being the code to run if the pattern matches. We call this a case (as in "pattern case"). else -> { body } is a fallback for when no patterns match.

When a pattern is matched, all other patterns are skipped.

Tip

When using pattern matching, the else case is required. In the future this may no longer be necessary, if the compiler can determine the patterns are exhaustive.

Matching using expressions

Let's say our input is a number. We don't know what the number is, and we only want to act on the numbers 1, 2, and 3. We can use pattern matching for this as follows:

import std::stdio::stdout

def example(input: Integer) {
  match(input) {
    1 -> { stdout.print('one') }
    2 -> { stdout.print('two') }
    3 -> { stdout.print('three') }
    else -> { stdout.print('Something else') }
  }
}

Here 1, 2 and 3 are patterns to test. For the Integer type, a pattern matches if it equals the input number. If the number is not 1, 2 or 3, the above example prints 'Something else' to STDOUT.

If different expressions specify the same code to run when they match, you can combine multiple patterns:

import std::stdio::stdout

def example(input: Integer) {
  match(input) {
    1, 2, 3 -> { stdout.print(input) }
    else -> { stdout.print('Something else') }
  }
}

To use a type as a pattern, it must implement the trait std::operators::Match. For some types the implementation is simple: if the pattern equals the input, it's considered a match. For other types, matching may involve a bit more work. For example, the Range type matches an input if the input is covered by the range:

import std::stdio::stdout

match(42) {
  1..100 -> { stdout.print("It's a match!") }
  else -> { stdout.print('No match') }
}

Here the output is "It's a match!", because 42 is covered by the range 1..100.

Note

When matching input using expressions, the input can't be of type Any.

Matching using types

Besides matching using expressions, we can also match using types:

import std::stdio::stdout

def example(input: Object) {
  match(input) {
    as Integer -> { stdout.print('An integer') }
    else -> { stdout.print('Something else') }
  }
}

Here as Integer is a pattern that matches if input is an instance of Integer. The type used in these type patterns can be either an object or a trait:

import std::conversion::ToString
import std::stdio::stdout

def example(input: Object) {
  match(input) {
    as ToString -> { stdout.print('Something to conver to a String') }
    else -> { stdout.print('Something else') }
  }
}

There is one limitation: due to Inko applying type erasure, you can't specify type parameters in the pattern, as type parameters and their assignments aren't known at runtime.

Matching input according to types can be combined with a match binding:

import std::stdio::stdout

def example(input: Object) {
  match(let matched = input) {
    as Integer -> { stdout.print('An integer') }
    else -> { stdout.print('Something else') }
  }
}

Within every case, matched is typed according to the pattern, removing the need for explicit type casts:

import std::stdio::stdout

def example(input: Object) {
  match(let matched = input) {
    as Integer -> {
      matched # typed as Integer
    }
    else -> {
      matched # typed as Object
    }
  }
}

Pattern guards

When specifying patterns, you can add an additional condition to evaluate called a "pattern" guard. These are specified as follows:

match(input) {
  foo when bar -> { foo_bar }
  foo when baz -> { foo_baz }
  else -> { quix }
}

First the foo pattern is applied. If it matches, the bar expression is evaluated. If this expression returns True, the condition (foo_bar) is evaluated. If the guard returns False, the next pattern is tried.

For a given pattern, only a single pattern guard can be specified. You can't specify a pattern guard for the else pattern.

Returns types

The return type of a match is the return type of the first case specified.

let result = match(input) {
  1 -> { 10 }
  2 -> { 20 }
  else -> { 30 }
}

result # typed as Integer

If additional cases are specified with different return types, the return type of match is Any:

let result = match(input) {
  1 -> { 10 }
  2 -> { 20 }
  else -> { 'foo' }
}

result # typed as Any