Pattern matching is the act of checking one or more inputs against a pre-defined pattern and seeing if they match. In Elm, there’s only a fixed set of patterns we can match against, so pattern matching has limited application. However, it’s still an important feature of the language. In this section, we’ll go through a few examples of pattern matching to understand how it works in Elm.
Pattern Matching in Case Expression
The case
expression works by matching an expression to a pattern. When a match is found, it evaluates the expression to the right of ->
and returns whatever value is produced. We saw a simple example of pattern matching in the Case Expression section.
In this example, dayInNumber
is the expression and the pattern is the numbers 0
through 6
, with a catchall _
at the end. Here’s a slightly more complex example:
The code above shows the implementation of the Result.map2
function which accepts three arguments: a function and two results. The map2
function uses the case
expression to determine what to do based on what’s inside the result arguments.
Case #1: If both results are Ok
, it applies the function to the payloads contained inside each Ok
.
Case #2: If the first result contains Err
, the second result gets ignored through the use of _
and the Err
from the first result gets returned.
Case #3: If the second result contains Err
, the first result gets ignored, and the Err
from the second result gets returned.
Elm allows us to reach into a data structure and match patterns directly instead of writing nested if
and case
expressions to get to the values. This enables us to write compact yet readable code.
- Learning From The Standard Library
- One of the best ways to learn Elm is to read Elm’s standard library. A large portion of the code contained in the standard library is written in Elm. It’s good code written by experienced programmers some of whom were responsible for creating Elm itself.
-
Just pick a module you like and browse through some of the functions, types, and values listed in that module. After that, head on over to Github and look for the filename that contains the code for the module you’re interested in. For example, the code for the
List
module is insrc/List.elm
, and the code forMaybe
is insrc/Maybe.elm
. Once you’re in the right file, search for the value you’re looking for and try to understand how it’s implemented.
Using Tuples To Pattern Match in Case Expression
One of the reasons the implementation of the map2
function is so compact is because Elm allows us to use tuples to match complex patterns in a case
expression. If the map2
function didn’t use a tuple in the case
expression above, here is what its implementation would look:
The code above isn’t easy to understand due to too much nesting. In contrast, the implementation with tuples we saw earlier is much easier to read. Unfortunately, Elm doesn’t allow more than three elements in a tuple. Otherwise, we would have been able to write code like this:
The code above is a fictitious implementation of Result.map5
which takes a function and five results as arguments. Since we can’t put more than three elements in a tuple, the actual implementation of that function looks something like this:
It’s not as easy to read as the previous implementation. Here’s an example of how to use map5
, in case you are curious:
Add that code right above main
in the Playground.elm
file located in beginning-elm/src
. After that import the Json.Decode
module and expose resultMap5Example
in the module definition.
Now enter the following code in elm repl
.
Pattern Matching Can’t Do Computation
It’s important to realize that pattern matching can only look at the structure of data. It can’t do any computation on the data itself. In the Case Expression section, we tried to rewrite the following if
expression with case
.
When we ran the escapeEarthWithCase
function we received the following error.
We received a syntax error because we tried to perform computations on myVelocity
and mySpeed
while matching patterns. Elm doesn’t allow that. If we do need to perform computations before matching patterns, we can do so inside a let
expression.
Pattern Matching Lists
Pattern matching could be used to reach into values contained inside almost any data structure. Let’s explore how we can take advantage of it to simplify code that involve list manipulation. In the Creating a Module section, we saw how the List
module implements the isEmpty
function.
The case
expression uses []
to match an empty list. When pattern matching a list, you’ll often see x
being used to match a single element and xs
to match several. The reason behind that is, x
is a common variable name in mathematics and xs
is treated as the plural form of x
. Here’s a slightly more complex example of pattern matching in a list:
The code above is the implementation of the foldl
function we went through in the List section. foldl
extracts the first element from a given list, adds it to the accumulator, and applies itself recursively to the remaining items in the list. Eventually, the list runs out of items. When that happens it simply returns the accumulator. It then starts accumulating results from each invocation. Let’s use an example to further explore the above implementation.
We used foldl
to compute the sum of all elements in a list. Like any other recursive function, foldl
exhibits three important characteristics:
#1 Reducing the problem: foldl
reduces the original problem to smaller sub-problems by taking the first element out from the given list.
This is the part we’re most interested in as far as pattern matching is concerned.
Behind the scenes, the [ 1, 2, 3, 4 ]
list is constructed like this:
That’s why we’re able to pattern match a list using the cons (::
) operator.
#2 Providing a base case: foldl
simplifies the problem with each invocation so that it will eventually reach the following base case.
#3 Combining results from each sub-problem: Once the base case is reached, foldl
starts the process of combining results from each invocation. It uses the following code to accomplish that.
It’s not easy to figure out how the accumulation of results happens just by looking at that code. So let’s visualize each step in the invocation.
Pattern Matching Function Arguments
We can pattern match on function arguments too. Here’s an example from the Tuples section that computes the perimeter of a triangle:
The pattern we are checking for in the above example is a tuple that contains three, and only three, numbers. When we apply the function, Elm reaches into the tuple that represents the sole argument of the trianglePerimeter
function and binds a
to 5
, b
to 4
, and c
to 6
. Let’s say we want to ignore the second argument. Not sure why we’d want to do that in this case, but if we do, we could use _
instead of an actual parameter.
Notice how the trianglePerimeter
function’s type got changed with the use of _
. This ability to pattern match function arguments came in handy when we used a tuple
fuzzer to write tests in the Fuzz Testing section.
When the tuple
fuzzer generates two integers, it binds them to constants — num1
and num2
— contained inside the tuple parameter. That’s what enables us to use num1
and num2
in the function body directly without having to use the Tuple.first
and Tuple.second
functions.
List.unzip
The implementation of the List.unzip
function is another good example of matching patterns on function arguments.
The unzip
function converts a list of tuples into a tuple of lists. Here’s an example:
The first list in the output contains the first item from each tuple in the original list, and the second list contains the second items. The private function step
in unzip
’s implementation uses pattern matching to break the pairs apart and put them into separate lists. Look how succinct the code inside the step
function is. Without pattern matching it would take a lot more code to accomplish the same task.
Pattern Matching Records
In the Using Custom Types section, we created a simple function that checked the isLoggedIn
flag to return an appropriate welcome message.
Let’s say we want to return a personalized welcome message that contains the user’s name. We can accomplish that by adding a second parameter. Add the following function definition right above main
in Playground.elm
.
And expose welcomeMessage
in the module definition.
Now enter the following code in elm repl
.
Instead of passing each user data individually, we can actually pass an entire user record to welcomeMessage
through pattern matching. Modify the welcomeMessage
function like this:
Next, add the following type alias right above main
in Playground.elm
.
And expose User
in the module definition.
Now we can create a User
record using the constructor function and pass that to welcomeMessage
.
What will happen if we pass a record that contains only name and logged in status?
That works! How about this:
That works too. Basically, the welcomeMessage
function works with any record that contains name
and isLoggedIn
. This was made possible by applying pattern matching to the function argument. The pattern matching version of welcomeMessage
has an interesting type annotation:
This is called extensible record syntax. It says the argument can be any record (represented by a
) as long as it has isLoggedIn
whose type is Bool
and name
whose type is String
. The part that makes our argument an extensible record is this: a |
. If the type annotation were to be this instead:
We wouldn’t be able to pass any record that contains isLoggedIn
and name
because it expects a record that has exactly two elements: isLoggedIn
and name
.
Summary
Although pattern matching is most evident in case
expressions, Elm allows us to use it in other places too. We looked at several examples of how to use tuples to simplify the logic in a function or a case
expression through pattern matching. We also saw an example that showed us how pattern matching on a list could enable us to write compact recursive functions such as foldl
. We learned what extensible records are and how pattern matching makes them flexible. Finally, it’s important to keep in mind that pattern matching can only look at the structure of data. It can’t do any computation on the data itself.