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.

> import Json.Decode exposing (decodeString, int)

> Result.map2 (*) (decodeString int "2") (decodeString int "3")
Ok 6 : Result Json.Decode.Error 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 (*) (decodeString int "y") (decodeString int "3")
Err (Failure ("This is not valid JSON! Unexpected token y in JSON at position 0")  <internals>)
    : Result Json.Decode.Error 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 (*) (decodeString int "2") (decodeString int "i")
Err (Failure ("This is not valid JSON! Unexpected token i in JSON at position 0") <internals>)
    : Result Json.Decode.Error 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 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 in src/List.elm, and the code for Maybe is in src/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:

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

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:

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 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:

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 of
        Err x ->
            Err x

        Ok a ->
            case rb of
                Err x ->
                    Err x

                Ok b ->
                    case rc of
                        Err x ->
                            Err x

                        Ok c ->
                            case rd of
                                Err x ->
                                    Err x

                                Ok d ->
                                    case re of
                                        Err x ->
                                            Err x

                                        Ok e ->
                                            Ok (func a b c d e)

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:

resultMap5Example : Result Json.Decode.Error Int
resultMap5Example =
    Result.map5 addFiveNumbers
        (decodeString int "1")
        (decodeString int "2")
        (decodeString int "3")
        (decodeString int "4")
        (decodeString int "5")


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

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.

module Playground exposing
    .
    .    
    , resultMap5Example
    )
.
.
import Json.Decode exposing (decodeString, int)

Now enter the following code in elm repl.

> import Playground exposing (..)

> resultMap5Example
Ok 15 : Result Json.Decode.Error 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 myVelocity mySpeed =
    if myVelocity > 11.186 then
        "Godspeed"

    else if mySpeed == 7.67 then
        "Stay in orbit"

    else
        "Come back"
escapeEarthWithCase myVelocity mySpeed =
    case (myVelocity, mySpeed) of
        (myVelocity > 11.186) ->
            "Godspeed"

        (mySpeed == 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 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.

escapeEarthWithCase : Float -> Float -> String
escapeEarthWithCase myVelocity mySpeed =
    let
        escapeVelocity =
            myVelocity > 11.186

        orbitalSpeed =
            mySpeed == 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 it to simplify code that involve list manipulation. In the Creating a Module 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 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.

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

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 ), ( "Red", False ) ]
(["Andy","Red"],[True,False])

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.

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

And expose welcomeMessage in the module definition.

module Playground exposing
    .
    .
    , welcomeMessage
    )

Now enter the following code in elm repl.

> 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
    }

And expose User in the module definition.

module Playground exposing
    .
    .
    , User
    )

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
{ age = 42, email = "gob@bluthboat.com", isLoggedIn = True, name = "Gob Bluth" }
    : 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 }
{ isLoggedIn = True, name = "Gob Bluth" }
    : { isLoggedIn : Bool, name : String }

> welcomeMessage user2
"Welcome Gob Bluth!" : String

That works! How about this:

> user3 = { name = "Gob Bluth", magician = True, isLoggedIn = True }
{ isLoggedIn = True, magician = True, name = "Gob Bluth" }
    : { 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 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.

Back to top
Close