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