Joy of Elixir

6. Pattern matching

Back in Chapter 2 you saw that the equals sign (=) made the computer remember things.

iex> sentence = "A really long and complex ⏎
sentence we'd rather not repeat."

"A really long and complex ⏎
sentence we'd rather not repeat."

iex> score = 2 / 5 * 100
40

While this is indeed still true, the equals sign (=) can do more than just set one value at a time. (Izzy double-takes at the last sentence, while the crowd murmurs.) There's a hidden feature of Elixir that we haven't shown yet, and that feature is called pattern matching. You'll use this feature quite a lot when programming with Elixir — just as much as functions! — so we'll spend a while talking about it here in this chapter too.

Equals is not just for equality

The equals sign isn't just about assigning things to make the computer remember them, but it can also be used for matching things. You can think of it like the equals sign in mathematics, where the left-hand-side must equal (or "match") the right-hand-side for the equation to be valid.

For instance, if we tried to make 2 + 2 = 5, much like Nineteen Eighty Four's Party would want us to believe, Elixir would not have a bar of it:

iex> 5 = 2 + 2
** (MatchError) no match of right hand side value: 4

Unlike the famous Mr. Winston Smith, Elixir cannot ever be coerced into disbelieving reality. Here, Elixir is telling us that 2 + 2 is indeed not 5. In Elixir, the left-hand-side has to evaluate to the same as the right-hand-side. This will make the computer happy:

iex> 4 = 2 + 2
4

Similarly, having two identical strings on either side of the equals sign will also make the computer happy:

iex> "dog" = "dog"
"dog"

Let's do something more complex than having the same thing on both sides of the equals sign. Let's take a look at how we can pattern match on lists.

Pattern matching with lists

Let's say we have a list of all the people assembled here, like we had back at the end of Chapter 4:

iex> those_who_are_assembled = [
...> %{age: "30ish", gender: "Female", name: "Izzy"},
...> %{age: "30ish", gender: "Male", name: "The Author"},
...> %{age: "56", gender: "Male", name: "Roberto"},
...> %{age: "38", gender: "Female", name: "Juliet"},
...> %{age: "21", gender: "Female", name: "Mary"},
...> %{age: "67", gender: "Female", name: "Bobalina"},
...> %{age: "54", gender: "Male", name: "Charlie"},
...> %{age: "10", gender: "Male", name: "Charlie (no relation)"},
...> ]

And let's also say that we wanted to grab first 3 people in this list, but then ignore the remainder -- given that the first three are clearly the most important people here. We can use some pattern matching on this list:

iex> [first, second, third | others] = those_who_are_assembled

With this code, we're telling Elixir to assign the first, second and third items from the list to the variables first, second and third. We're also telling it to assign the remainder of the list to the others variable, and we specify that by using the pipe symbol (|). In this code we've selected the first 3 items from the list, but we could also just select the very first one, or the first 5. It doesn't have to be exactly 3.

We can check what this has done exactly in iex by looking at the values of each of these variables:

iex> first
%{age: "30ish", gender: "Female", name: "Izzy"}
iex> second
%{age: "30ish", gender: "Male", name: "The Author"}
iex> third
%{age: "56", gender: "Male", name: "Roberto"}
iex> others
[%{age: "38", gender: "Female", name: "Juliet"}, ...]

"Does this mean that I could do the same for the others list to get the next 3 people?", Izzy asks. Yes it does mean that you can do that:

iex> [first, second, third | remainder] = others
[%{age: "38", gender: "Female", name: "Juliet"}, ...]

Now when we check the values of first, second and third they'll be the names of the next 3 people in the list:

iex> first
%{age: "38", gender: "Female", name: "Juliet"},
iex> second
%{age: "21", gender: "Female", name: "Mary"},
iex> third
%{age: "67", gender: "Female", name: "Bobalina"},

And our remainder variable will be the remaining names in the list, which are just the two Charlies:

iex> remainder
[
  %{age: "54", gender: "Male", name: "Charlie"},
  %{age: "10", gender: "Male", name: "Charlie (no relation)"},
]

If we now try to pull out the next 3 people in the list, Elixir won't be able to do that because there are only two names left in the remainder list. I've shortened the output in the below example, but I think you'll get the gist:

iex> [first, second, third | those_still_remaining] = remainder
** (MatchError) no match of right hand side value: ... ⏎
   [%{name: "Charlie"}, %{name: "Charlie (no relation)"}]

It's always a good idea to be careful here with pattern matching lists with the right number of expected items to avoid MatchErrors like these. Normally when we would work through each item of the list, we would do so not in groups of three, but one at a time.

We'll see two examples of working through each item in a list one-at-a-time, once in Chapter 9 and once within Chapter 10. We'll need to build up to those, so let's not worry too much about those yet.

That's enough about lists for now. We'll revisit pattern matching them a little later on in this book. Let's look at how we can work with maps.

Pattern matching with maps

Let's say that we have a map containing a person's information:

iex> person = %{name: "Izzy", age: "30ish"}
%{name: "Izzy", age: "30ish"}

We've seen before that we could pull out the value attached to name: or age: at whim by using this syntax:

iex> person.name
"Izzy"
iex> person.age
"30ish"

But what if we wanted to pull out both of these values at the same time? Or even in the shortest possible code? Well, for that we have this pattern matching thing that I've been banging on about for over a page. Let's take a look at how pattern matching can help get both the name and the age out of the person map.

iex> %{name: name, age: age} = person

"Hey, what gives? The left-hand-side here is clearly not the same as the right hand side!", cries Izzy. Yes, you're absolutely right. On the right hand side here we have a map which has a "name" key which points to a value of "Izzy", but on the left hand side that "name" key points to a value of name. This is a trick of pattern matching: the left-hand-side can be used to assign multiple variables; it doesn't have to match the right-hand-side exactly.

If we check the value of name and age here, we'll see that those values are the values from our map.

iex> name
"Izzy"
iex> age
"30ish"

Well, would you look at that? We were able to pull out these two values at the same time. The crowd cheers as if we've just performed a magic trick. Just wait until you see our next trick!

Let's look at our previous example, where we had maps inside of a list:

iex> those_who_are_assembled = [
...> %{age: "30ish", gender: "Female", name: "Izzy"},
...> %{gender: "Male", name: "The Author", age: "30ish"},
...> %{name: "The Reader", gender: "Unknowable", age: "Unknowable"},
...> ]

Let's say that we wanted to get the name of the first person from this list. We can combine both the list pattern matching and the map pattern matching together:

[first_person = %{name: first_name} | others] = those_who_are_assembled

Here we're matching the first item from the list, and putting the rest in an others variable. We're grabbing just the first person from the list of those who are assembled. Inside that match, we're assigning that first person to first_person. We're then expecting that the first item of this list to match the pattern of %{name: first_name}. This will set the variable of first_name to be the value of the first item's name key. Phew, that was a long description. Let's go and see what we now have in the console:

iex> first_person
%{age: "30ish", gender: "Female", name: "Izzy"}
iex> first_name
"Izzy"

This can be tricky to wrap your head around at first since there's a lot going on here. It might take a few tries to read it and understand. It did for me when I first read an example like this! Take your time, it's OK to not get it first try.

What we're doing here is pulling out 3 distinct values from this one single line:

Figure 6.1: Complex pattern matching within a list
  • The first_person variable, which contains all the information we have on Izzy in map form.
  • The first_name variable, which has stored the string "Izzy"
  • The others variable, which stored the remaining people in the list.

As you can see from this short example, pattern matching is very flexible and allows you to match more than one thing at a time, and also allows you to set more than one variable. Programmers often refer to this sort of thing as destructuring: you're looking into the structure of the data and then pulling out only the things you want.

Pattern matching can be used for even more things than picking out the keys of a map or the items out of a list. We can also use it inside of functions!

Pattern matching inside functions

We can use pattern matching inside our functions to make them respond differently depending on the arguments passed in. For instance, we could define a function which took the kind of road that we took; either the "high" road or the "low" road, and get it to respond differently depending on which was passed.

iex> road = fn
  "high" -> "You take the high road!"
  "low" -> "I'll take the low road! (and I'll get there before you)"
end

When we call this function with the "high" argument, that argument will match the first function line here, and "You take the high road" will be returned. Similarly, when we give it "low" it will return "I'll take the low road! (and I'll get there before you)". Each line inside the function here is called a clause. We could keep talking about the theory behind this, or we could actually try it in our iex prompt:

iex> road.("high")
"You take the high road!"
iex> road.("low")
"I'll take the low road! (and I'll get there before you)"

This works because of how we've defined the road function. In that function, we've defined two separate function clauses. The first function clause says that when the argument is "high" then the function should output the line about the "high road". The second function clause says that when we supply the "low" argument then it should output the line about the "low road".

Think of it like this: Elixir is pattern matching the value of the argument against the clauses of the function. If Elixir can see that the argument is equal to "high" then it will use the first function. If it isn't equal to "high", then Elixir will try matching against "low". If the argument is "low" then the second clause will be used.

But what happens if it's neither? We can find out with a touch of experimentation in our iex prompt:

iex> road.("middle")
** (FunctionClauseError) no function clause matching in :erl_eval."-inside-an-interpreted-fun-"/1

    The following arguments were given to :erl_eval."-inside-an-interpreted-fun-"/1:

        # 1
        "middle"

Elixir here is showing us an error that we've never seen before: a FunctionClauseError. This error happens when we call a function giving it arguments that it doesn't expect. In the case of our road function, it's expecting either "high" or "low", but we gave it "middle". Both clauses of the function don't match the value of "middle", and so we see this error. To be extra helpful, Elixir is showing us the argument that we passed here.

Matching on strings in functions is great, but as we saw earlier with the equals sign (=) we can match on more than just strings.

Matching maps inside functions

We can also match on the keys contained within a map and get the code to act differently depending on what keys are present. Let's take our greeting function from Chapter 5 and modify it slightly so that behaves differently depending on what kind of map we pass it:

iex> greeting = fn
  %{name: name} -> "Hello, #{name}!"
  %{} -> "Hello, Anonymous Stranger!"
end

"Oooh that's fancy! What is the empty map is for?", Izzy asks. Soon, Izzy. Soon. Let's see what happens if we call this greeting function with a map which has a "name" key:

iex> greeting.(%{name: "Izzy"})
"Hello, Izzy!"

Here, the first function clause is matching because the map we're supplying contains a key which is "name", and that's what the first function clause (highlighted below in green) expects too: a map which has a key called "name". So when we call this function with this map with a "name" key, we see the string "Hello, Izzy!" output from the function.

iex> greeting = fn
  %{name: name} -> "Hello, #{name}!"
  %{} -> "Hello, Anonymous Stranger!"
end

Now let's see what happens if we call this function with an empty map:

iex> greeting.(%{})
"Hello, Anonymous Stranger!"

Elixir is still acting as we would expect it to: we supplied an empty map and the second function clause matches an empty map, and so that's the clause that will be used here instead.

iex> greeting = fn
  %{name: name} -> "Hello, #{name}!"
  %{} -> "Hello, Anonymous Stranger!"
end

Ok, so what would you expect to happen here if you supplied neither a map with a "name" key or an empty map, but a map with a different key in it? "Based on the string test, I would expect it to fail with a FunctionClauseError!", Izzy proudly proclaims. Looks like someone has been paying attention. Dear Izzy, that is what I expected to happen too when I learned Elixir. However, maps are matched differently to strings in Elixir. Let's look:

iex> greeting.(%{age: "30ish"})
"Hello, Anonymous Stranger!"

The greeting function still displays "Hello, Anonymous Stranger!" So what gives here?

Well, in Elixir when you match two maps together it will always match on subset of the map. Let's take a look using our trusty equals sign (=) again:

iex> %{} = %{name: "Izzy"}
%{"name" => "Izzy"}

Just like in the second clause from the function above, we're comparing an empty map on the left-hand-side to a map from the right hand side. When pattern matching maps like this, it's helpful to think of the left-hand-side showing the keys that are absolutely required for the match to work. The right-hand-side must contain the same keys as the left-hand-side, but the right-hand-side can contain more keys than what's on the left.

This match will succeed because there are no keys required by the left-hand-side of this match. This story is different if we've got a map on the left-hand-side with keys, as we've seen before with the first clause of our greeting function:

iex> greeting = fn
  %{name: name} -> "Hello, #{name}!"
  %{} -> "Hello, Anonymous Stranger!"
end

In the first clause's case, it will only match if the argument passed to the greeting function is a map which contains a "name" key; this key is required by the match. If the map does not contain a "name" key then this clause will not match. The second clause matches any map, and so that is the clause that will be used for any map not containing a "name" key.

Matching anything

Now one more thing I wanted to show you is how to avoid those pesky FunctionClauseErrors when all the function clauses inside a function don't match. Let's take a look at the road function again:

iex> road = fn
  "high" -> "You take the high road!"
  "low" -> "I'll take the low road! (and I'll get there before you)"
end

If the argument supplied to this function is neither "high" nor "low" then Elixir shows us a FunctionClauseError:

iex> road.("middle")
** (FunctionClauseError) no function clause matching in ⏎
:erl_eval."-inside-an-interpreted-fun-"/1

What if instead of this error we could get the function to tell whoever was running it that they have to supply either "high" or "low" as the argument? Elixir allows us to do this by using an underscore (_) as the argument in a new function clause:

iex> road = fn
  "high" -> "You take the high road!"
  "low" -> "I'll take the low road! (and I'll get there before you)"
  _ -> "Take the 'high' road or the 'low' road, thanks!"
end

This underscore (_) matches anything and is often used in cases like this where we want to show a message if no other function clause matches. Let's see this in action:

iex> road.("middle")
"Take the 'high' road or the 'low' road, thanks!"

This underscore doesn't just match strings, but it will match any other argument that we can pass to the function:

iex> road.(%{})
"Take the 'high' road or the 'low' road, thanks!"
iex> road.(["high", "low"])
"Take the 'high' road or the 'low' road, thanks!"

What this does is skip the first two clauses of the function, and so the third clause will be used instead:

iex> road = fn
  "high" -> "You take the high road!"
  "low" -> "I'll take the low road! (and I'll get there before you)"
  _ -> "Take the 'high' road or the 'low' road, thanks!"
end

This helps keep people in line when it comes to providing the right arguments to the function, without Elixir blowing up in their face when they provide the wrong ones.

Exercises

  • Make a function that takes either a map containing a "name" and "age", or just a map containing "name". Change the output depending on if "age" is present. What happens if you switch the order of the function clauses? What can you learn from this?