Pattern Matching
Edit on GitHubPattern matching is a powerful tool to work with data structures. It’s like a switch statement in other languages, but with a bit more pizazz. Each case of a match
statement defines the shape, or pattern, of the data that will match the case.
Matching Enum Types
Let’s start by looking at a simple enum type.
This example prints out a fun message depending on which pizza topping is bound to topping
. Each case is separated by a comma.
You can also surround the body of your match
case with curly braces to include more statements.
Default Cases
If we only care about some of the cases, we can use an underscore pattern to match all other possible cases.
If we were to omit the last underscore “catch-all” case, the Grain compiler would complain that we’re missing cases for Pepperoni
, Peppers
, and Pineapple
.
Nested Match Patterns
We can nest match patterns as deeply as we’d like. If we sold both one-topping pizzas and calzones, we may want to single out a particular pizza/topping combo:
We can also use an underscore anywhere within a pattern to match remaining cases.
Bindings in Match Patterns
We can bind portions of a match
pattern to a name and use that bound value in the body the corresponding case.
One common use of pattern matching and binding is working with Option
and Result
enums.
The first example will print I'm a match
because the Option
has a value.
The second example will print This is an error
because the Result
contains an Err
.
Matching Record Types
Like most Grain data structures, pattern matching can also be done on records.
1 | record Person { name: String, age: Number } |
If we don’t care about some of the record fields, we can use an underscore to tell the Grain compiler that we’ve intentionally left those fields out.
1 | record Person { name: String, age: Number } |
Nested Matching Within Records
Things are a bit more interesting when we have data structures nested within records.
Matching Tuples
Pattern matching can also be performed on tuples.
Matching Lists
Often with lists, we want to do something with the first element of a list and then do some further processing with the rest of the list.
In this example, we define an add2
function to increment each value in a list of numbers by 2.
1 | let rec add2 = (list) => { |
Let’s break this down.
The [first, ...rest]
pattern creates bindings for the first element in the list, and a list containing the rest of the elements. In the body of this case, the [first + 2, ...add2(rest)]
expression creates a new list. The first element of the result is the sum of 2 and the first element of the original list. The remaining elements of the list are the result of a recursive call to the add2
function on the rest of the original list.
We can also match on multiple elements at the beginning of a list:
1 | let list = [1, 2, 3] |
Finally, matches can also be performed on lists with specific lengths.
Match Guards
Sometimes you want to be more specific about conditions for matching. You can use a match guard to place more specific limitations on a match
case.
You can think of a match guard as a combination of a match
and an if
. A guard allows you to add an additional statement to qualify whether the case matches.
1 | let myNumber = Some(123) |
In the example above, myNumber
is an Option
whose value is Some(123)
. But our match
statement has three cases instead of two. The first case adds a guard: Some(val) when val > 100
. This case will only be matched when the Option
is Some()
and the value inside that option is a number greater than 100
.
The next case, Some(val)
, will match any Some()
value that is not matched by the guarded version.
Order is important when using guards. The first case to match is the one that will be used. For example, we could change the above example to this:
1 | let myNumber = Some(123) |
In this version, regardless of the size of myNumber
, it would always match the first case.
Even default cases can have guards.
1 | let myNumber = Some(99) |
In the example above, if myNumber
is greater than 100
(which it is not), then the program prints Greater than 100
. Otherwise, if isTuesday
is true
, then it prints It's Tuesday
(regardless of whether myNumber
is a Some()
or a None
). Finally, if nothing else matches, the program prints Nothing else matched
.