4.9

Pattern Matching

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.

weekday dayInNumber =
    case dayInNumber of
        0 ->
            "Sunday"

        1 ->
            "Monday"

        2 ->
            "Tuesday"

        3 ->
            "Wednesday"

        4 ->
            "Thursday"

        5 ->
            "Friday"

        6 ->
            "Saturday"

        _ ->
            "Unknown day"

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:

map2 : (a -> b -> value) -> Result x a -> Result x b -> Result x value
map2 func ra rb =
    case ( ra, rb ) of
        ( Ok a, Ok b ) ->
            Ok (func a b)

        ( Err x, _ ) ->
            Err x

        ( _, Err x ) ->
            Err x

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.

> Result.map2 (*) (String.toInt "2") (String.toInt "3")
Ok 6 : Result.Result String Int

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.

> Result.map2 (*) (String.toInt "y") (String.toInt "3")
Err "could not convert string 'y' to an Int" : Result.Result String Int

Case #3: If the second result contains Err, the first result gets ignored, and the Err from the second result gets returned.

> Result.map2 (*) (String.toInt "2") (String.toInt "i")
Err "could not convert string 'i' to an Int" : Result.Result String Int

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 that interests you 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 contained in List.elm, and the code for Maybe is contained in Maybe.elm. Once you’re in the right file, search for the function, type, or value you’re looking for and settle in for some interesting reading.

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:

map2 : (a -> b -> value) -> Result x a -> Result x b -> Result x value
map2 func ra rb =
    case ra of
        Ok a ->
            case rb of
                Ok b ->
                    Ok (func a b)

                Err x ->
                    Err x

        Err x ->
            Err x

It’s a bit difficult to understand the code above due to too much nesting. In contrast, the implementation we saw earlier with tuples is much more elegant and easy to read. Here’s a more compelling case for using tuples in a case expression:

map5 : (a -> b -> c -> d -> e -> value) -> Result x a -> Result x b -> Result x c -> Result x d -> Result x e -> Result x value
map5 func ra rb rc rd re =
    case (ra,rb,rc,rd,re) of
      (Ok a, Ok b, Ok c, Ok d, Ok e) -> Ok (func a b c d e)
      (Err x, _, _, _, _) -> Err x
      (_, Err x, _, _, _) -> Err x
      (_, _, Err x, _, _) -> Err x
      (_, _, _, Err x, _) -> Err x
      (_, _, _, _, Err x) -> Err x

The code above is the implementation of Result.map5 which takes a function and five results as arguments. Imagine what its implementation would look like if it didn’t use tuples to match patterns. Here’s an example of how to use map5:

resultMap5Example : Result String Int
resultMap5Example =
    Result.map5 addFiveNumbers
        (String.toInt "1")
        (String.toInt "2")
        (String.toInt "3")
        (String.toInt "4")
        (String.toInt "5")


addFiveNumbers : Int -> Int -> Int -> Int -> Int -> Int
addFiveNumbers a b c d e =
    a + b + c + d + e


main =
    ...

Add the above code right above the main function in the Playground.elm file located in beginning-elm/elm-examples. After that you can type the function name in the repl to see the result.

> import Playground exposing (..)

> resultMap5Example
Ok 15 : Result.Result String Int

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.

escapeEarth : Float -> Float -> String
escapeEarth velocity speed =
    if velocity > 11.186 then
        "Godspeed"
    else if speed == 7.67 then
        "Stay in orbit"
    else
        "Come back"
escapeEarthWithCase : Float -> Float -> String
escapeEarthWithCase velocity speed =
    case (velocity, speed) of
        (velocity > 11.186) ->
            "Godspeed"

        (speed == 7.67) ->
            "Stay in orbit"

        _ ->
            "Come back"

When we ran the escapeEarthWithCase function we received the following error.

We received a syntax error because we tried to perform computations on velocity and speed 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.

escapeEarthWithCase : Float -> Float -> String
escapeEarthWithCase velocity speed =
    let
        escapeVelocity =
            velocity > 11.186

        orbitalSpeed =
            speed == 7.67
    in
        case ( escapeVelocity, orbitalSpeed ) of
            ( True, _ ) ->
                "Godspeed"

            ( _, True ) ->
                "Stay in orbit"

            _ ->
                "Come back"

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 pattern matching to simplify code that involve list manipulation. In the Easier Code Organization section, we saw how the List module implements the isEmpty function.

isEmpty : List a -> Bool
isEmpty xs =
    case xs of
        [] ->
            True

        _ ->
            False

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:

foldl : (a -> b -> b) -> b -> List a -> b
foldl func acc list =
    case list of
        [] ->
            acc

        x :: xs ->
            foldl func (func x acc) xs

The code above is the implementation of the foldl function we went through in the List section. foldl extracts the first element from the 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.

> List.foldl (+) 0 [ 1, 2, 3, 4 ]
10 : number

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.

x :: xs

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:

> 4 :: []
[4] : List number

> 3 :: [ 4 ]
[3,4] : List number

> 2 :: [ 3, 4 ]
[2,3,4] : List number

> 1 :: [ 2, 3, 4 ]
[1,2,3,4] : List number

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.

[] ->
    acc

#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.

foldl func (func x acc) xs

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:

> trianglePerimeter ( a, b, c ) = a + b + c
<function> : ( number, number, number ) -> number

> trianglePerimeter ( 5, 4, 6 )
15 : number

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.

> trianglePerimeter ( a, _, c ) = a + c
<function> : ( number, a, number ) -> number

> trianglePerimeter ( 5, 4, 6 )
11 : number

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.

addTests : Test
addTests =
    describe "add"
        [ fuzz (tuple ( int, int )) "adds two given integers" <|
            \( num1, num2 ) ->
                add num1 num2
                    |> Expect.equal (num1 + num2)
        ]

When the tuple fuzzer generates two integers, it binds them to the 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.

The implementation of the List.unzip function is another good example of matching patterns on function arguments.

unzip : List ( a, b ) -> ( List a, List b )
unzip pairs =
    let
        step ( x, y ) ( xs, ys ) =
            ( x :: xs, y :: ys )
    in
        foldr step ( [], [] ) pairs

The unzip function converts a list of tuples into a tuple of lists. Here’s an example:

> List.unzip [ ( "Andy", True ), ( "Hadley", False ), ( "Red", True ) ]
(["Andy","Hadley","Red"],[True,False,True])

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 Creating Our Own Types section, we created a simple function that checked a logged in flag to return an appropriate welcome message.

welcomeMessage : Bool -> String
welcomeMessage isLoggedIn =
    case isLoggedIn of
        True ->
            "Welcome to my awesome site!"

        False ->
            "Please log in."

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.

welcomeMessage : Bool -> String -> String
welcomeMessage isLoggedIn name =
    case isLoggedIn of
        True ->
            "Welcome " ++ name ++ "!"

        False ->
            "Please log in."


main =
    ...
> import Playground exposing (..)

> welcomeMessage True "Gob Bluth"
"Welcome Gob Bluth!" : String

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:

welcomeMessage : { a | isLoggedIn : Bool, name : String } -> String
welcomeMessage { isLoggedIn, name } =
    case isLoggedIn of
        True ->
            "Welcome " ++ name ++ "!"

        False ->
            "Please log in."

Next, add the following type alias right above main in Playground.elm

type alias User =
    { name : String
    , email : String
    , age : Int
    , isLoggedIn : Bool
    }


main =
    ...

Now we can create a User record using the constructor function and pass that to welcomeMessage.

> user = User "Gob Bluth" "gob@bluthboat.com" 42 True
{ name = "Gob Bluth", email = "gob@bluthboat.com", age = 42, isLoggedIn = True }
    : Playground.User

> welcomeMessage user
"Welcome Gob Bluth!" : String

What will happen if we pass a record that contains only name and logged in status?

> user2 = { name = "Gob Bluth", isLoggedIn = True }
{ name = "Gob Bluth", isLoggedIn = True } : { isLoggedIn : Bool, name : String }

> welcomeMessage user2
"Welcome Gob Bluth!" : String

That works! How about this:

> user3 = { name = "Gob Bluth", magician = True, isLoggedIn = True }
{ name = "Gob Bluth", magician = True, isLoggedIn = True }
    : { isLoggedIn : Bool, magician : Bool, name : String }

> welcomeMessage user3
"Welcome Gob Bluth!" : String

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:

welcomeMessage : { a | isLoggedIn : Bool, name : String } -> String

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:

welcomeMessage : { isLoggedIn : Bool, name : String } -> String

We wouldn’t be able to pass any record that contains isLoggedIn and name. 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.

Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close