Pattern matching part 1

We've seen how to conditionally evaluate programs with theif true then "this" else "that"syntax.With pattern matching we can change thecontrol flowof our program based on a richer set of conditions. We'll explore how to pattern match onliteral 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 syntaxmatch f withidentifies the target value that the pattern match will make its comparisons against. After thewithkeyword, Unison expects a series of arrow->separated cases. In the example above, the valuefwill 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 offmatches 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:

use Universal
magicNumber : Nat -> Text
magicNumber guess = match guess with
  42 -> "magic 🪄"
  n -> toText n ++ " is not the magic number. Guess again."

Herenis a variable that will bind to any value of typeNat.It's also functioning as a fallback value, and in the example above whatever value it has can be used to produce a desiredTextresult.

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 typeBoolean.If the value to the right side of the pipe|istrue,the block on the right of the arrow will be executed.

use Universal
use Text
matchNum : Nat -> Text
matchNum num = match a 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 variableoneTwowithout the guard pattern would match anything of typeNat,but we can specify in the pattern guard that the variableoneTwoshould either be theliteralvalue1or2.

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 istrue.The last case| otherwise -> in this syntax is a pass-through to catch all other cases. In the context of this pattern match form,otherwisesimply meanstrue.

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 thecasesshorthand 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 themyMatchfunction, but when we see thecaseskeyword, 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 thecasessyntax, but when multiple arguments are being tested, they arecomma separatedin the case statements.

twoCases : Nat -> Nat -> Text
twoCases = cases
  n1, n2 | n1 === n2 -> "same value"
  _, _ -> "different values"

📌

Summary:

  • Pattern matching allows us to inspect both the content the structure of our data.
  • Features like guard patterns and variable patterns allow us to apply conditions which may be local to the structure of that data to direct the flow of a program.
  • Guard patterns are expressed with a single pipe,|,with a boolean expression on the right.
  • Thematch ... withsyntax can by simplified withcaseswhen matching on the last arguments to a function.