We've seen how to conditionally evaluate programs with the if true then "this" else "that"
syntax. With pattern matching we can change the control flow of our program based on a richer set of conditions. We'll explore how to pattern match on literal values, add "guards" for more granular conditions for our pattern, and explore structural pattern matching based on data types.
Pattern matching on literals
In some circumstances you might see pattern matching being used as a concise chain of if / else statements.
foodUnit : Text -> Text
foodUnit f =
"๐ compare f with the following..."
match f with
"Pie" -> "slice"
"Coffee" -> "cup"
"Soup" -> "bowl"
"Pancake" -> "stack"
_ -> "???"
The first part of the pattern match syntax match f with
identifies the target value that the pattern match will make its comparisons against. After the with
keyword, Unison expects a series of arrow ->
separated cases. In the example above, the value f
will be compared against each value to the left of the arrow, starting from the top. The order in which the cases appear is important for the pattern match because if the value of f
matches something on the left, Unison will evaluate the right side of the arrow.
In our example, we've provided a "catch-all" case at the end of the pattern match. It starts with an underscore: _ -> "fallback value"
๐จ It's a common practice to provide a catch-all. The following code will currently typecheck, but willlikely be a type error in a future version of Unison:
mySoupCount : Text -> Nat
mySoupCount name =
"๐ no fallback value"
match name with
"Chicken Noodle" -> 4
"Miso" -> 7
"Borscht" -> 5
"Chowder" -> 5
mySoupCount "Gazpacho"
The error returned by the UCM reads:
๐๐ฅ
I've encountered a pattern match failure in function `mySoupCount` while scrutinizing:
"Gazpacho"
This happens when calling a function that doesn't handle all possible inputs
I'm sorry this message doesn't have more detail about the location of the failure. My makers plan
to fix this in a future release.
Variables in pattern matching
Let's say we want to save a value on the left-hand side of the case statements for use when evaluating the expression on the right-hand side. Let's do that below:
magicNumber : Nat -> Text
magicNumber guess = match guess with
42 -> "magic ๐ช"
n -> toText n ++ " is not the magic number. Guess again."
Here n
is a variable that will bind to any value of type Nat
. It's also functioning as a fallback value, and in the example above whatever value it has can be used to produce a desired Text
result.
Guard patterns
Guard patterns are a way we can build further conditions for the match expression. They can reference variables in the case statement to further refine if the right side of the arrow should be run. A guard pattern is started with pipe character |
where the right of the |
needs to return something of type Boolean
. If the value to the right side of the pipe |
is true
, the block on the right of the arrow will be executed.
matchNum : Nat -> Text
matchNum num = match num with
oneTwo | (oneTwo === 1) || (oneTwo === 2) -> "one or two"
threeFour | (threeFour === 3) || (threeFour === 4) -> "three or four"
fiveSix | (fiveSix === 5) || (fiveSix === 6) -> "five or six"
_ -> "no match"
We've combined variables with guard patterns and boolean operators in our pattern match statement. The variable oneTwo
without the guard pattern would match anything of type Nat
, but we can specify in the pattern guard that the variable oneTwo
should either be the literal value 1
or 2
.
You might see multiple guard patterns being used for the same match expression case. For example:
myMatch : Nat -> Text
myMatch num = match num with
n
| n < 3 -> "small number"
| n > 100 -> "big number"
| otherwise -> "medium number"
Each pipe |
is followed by a boolean expression and an expression to execute if the boolean is true
. The last case | otherwise ->
in this syntax is a pass-through to catch all other cases. In the context of this pattern match form, otherwise
simply means true
.
Cases syntax
When writing a pattern match where the last value or set of values are being compared against successive cases, or "scrutinized", you can use the cases
shorthand rather than writing out the full match statement.
So a match expression with one argument might look like this when fully written out:
myMatch : Nat -> Text
myMatch nat = match nat with
n | n < 3 -> "small number"
_ -> "big number"
But it can also be expressed as:
myMatch : Nat -> Text
myMatch = cases
n | n < 3 -> "small number"
_ -> "big number"
At first glance it might appear that there is no argument to the myMatch
function, but when we see the cases
keyword, we know that there is at least one inferred argument being tested against the cases in our match expression.
Two or more arguments can also be scrutinized with the cases
syntax, but when multiple arguments are being tested, they are comma separated in the case statements.
twoCases : Nat -> Nat -> Text
twoCases = cases
n1, n2 | n1 === n2 -> "same value"
_, _ -> "different values"