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.