3 Pattern Matching
3.1 Matching Tuples, Lists, Maps and Strings
Matching tuples
= :calendar.local_time()
{date, time}
date#> {2022, 1, 2}
time#> {14, 44, 35}
Nested match
= :calendar.local_time()
{_, {hour, _, _}}
hour#> 14
Matching lists
= [1, 2, 3] [first, second, third]
Matching lists is more often done by relying on their recursive nature. Recall that each non-empty list is a recursive structure that can be expressed in the form [head | tail]
. You can use pattern matching to put each of these two elements into separate variables:
3)> [head | tail] = [1, 2, 3]
iex(#> [1, 2, 3]
4)> head
iex(#> 1
5)> tail
iex(#> [2, 3]
If you need only one element of the [head, tail] pair, you can use the anonymous variable. Here’s an inefficient way of calculating the smallest element in the list:
6)> [min | _] = Enum.sort([3, 2, 1])
iex(7)> min
iex(#> 1
Match maps
Note that pattern matching with maps does not require listing all the keys. You can match on any key in the map.
age: age2} = %{name: "Bob", age: 25}
%{
age2#> 25
Of course, a match will fail if the pattern contains a key that’s not in the matched term:
age: age, works_at: works_at} = %{name: "Bob", age: 25}
%{#> ** (MatchError) no match of right hand side value
In the following example, we can just pattern match with the key you need. Also order of the clauses are important e.g. if you are trying to match %{"b" => 2}
but you have the following map %{"a" => 1, "b" => 2}
, the key "a"
will match first, because is in the first clause:
defmodule Test do
def my_func(%{"a" => value}), do: {:a, value}
def my_func(%{"b" => value}), do: {:b, value}
def my_func(_), do: :error
end
1)> Test.my_func(%{"a" => 1})
iex(#> {:a, 1}
2)> Test.my_func(%{"b" => 2})
iex(#> {:b, 2}
3)> Test.my_func(%{"a" => 1, "b" => 2})
iex(#> {:a, 1}
Match strings
13)> <<b1, b2, b3>> = "ABC"
iex(#> "ABC"
13)> b1
iex(#> 65
14)> b2
iex(#> 66
15)> b3
iex(#> 67
= "ping www.google.com"
command "ping " <> url = command
url#> www.google.com
Occasionally, you’ll need to match against the contents of the variable. For this pur- pose, the pin operator (^) is provided.
7)> expected_name = "Bob"
iex(
# Matches to the content of the variable expected_name
8)> {^expected_name, _} = {"Bob", 25}
iex(#> {"Bob", 25}
9)> {^expected_name, _} = {"Alice", 30}
iex(#> ** (MatchError) no match of right hand side value: {"Alice", 30}
3.2 Multiclause functions
The pattern-matching mechanism is used in the specification of function arguments. Recall the basic function definition:
def my_fun(arg1, arg2) do
...
end
The argument specifiers arg1
and arg2
are patterns, and you can use standard matching techniques.
For example, if you do a geometry manipulation, you can represent a rectangle with a tuple, {a, b}
, containing the rectangle’s sides. The following listing shows a function that calculates a rectangle’s area.
defmodule Rectangle do
def area( { a, b } ) do
* b
aend
end
Rectangle.area({2, 3})
#> 6
With pattern matching of function arguments, Elixir allows you to overload a function by specifying multiple clauses. A clause is a function definition specified by the def construct. If you provide multiple definitions of the same function with the same arity, it’s said that the function has multiple clauses.
Let’s see this in action. Extending the previous example, let’s say you need to develop a Geometry module that can handle various shapes. You’ll represent shapes with tuples and use the first element of each tuple to indicate which shape it represents:
defmodule Geometry do
def area({:rectangle, a, b}) do
* b
aend
def area({:square, a}) do
* a
aend
def area({:circle, r}) do
* r * 3.14
r end
end
1)> Geometry.area({:rectangle, 4, 5})
iex(#> 20
2)> Geometry.area({:square, 5})
iex(#> 25
3)> Geometry.area({:circle, 4})
iex(#> 50.24
Sometimes you’ll want a function to return a term indicating a failure, rather than raising an error. You can introduce a default clause that always matches. Let’s do this for the area function. The next listing adds a final clause that handles any invalid input.
defmodule Geometry do
def area({:rectangle, a, b}) do
* b
aend
def area({:square, a}) do
* a
aend
def area({:circle, r}) do
* r * 3.14
r end
def area(unknown) do
:error, {:unknown, unknown}}
{end
end
If none of the first three clauses match, the final clause is called. This is because a variable pattern always matches the corresponding term. In this case, you return a two-element tuple {:error, reason}
, to indicate that something has gone wrong.
3.3 Guards
Let’s say you want to write a function that accepts a number and returns an atom :negative, :zero, or :positive, depending on the number’s value. This isn’t possi- ble with the simple pattern matching you’ve seen so far. Elixir gives you a solution for this in the form of guards.
Guards are an extension of the basic pattern-matching mechanism. They allow you to state additional broader expectations that must be satisfied for the entire pattern to match.
A guard can be specified by providing the when clause after the arguments list. This is best illustrated by example. The following code tests whether a given number is positive, negative, or zero.
defmodule TestNum do
def test(x) when x < 0, do
:negative
end
def test(0), do:
:zero
end
def test(x) when x > 0, do
:positive
end
end
The guard is a logical expression that places further conditions on a clause. The first clause will be called only if you pass a negative number, and the last one will be called only if you pass a positive number, as demonstrated in this shell session:
1)> TestNum.test(-1)
iex(#> :negative
2)> TestNum.test(0)
iex(#> :zero
3)> TestNum.test(1)
iex(#> :positive
Surprisingly enough, calling this function with a non-number yields strange results:
4)> TestNum.test(:not_a_number)
iex(#> :positive
The explanation lies in the fact that Elixir terms can be compared with the operators <
d >
, even if they’re not of the same type. In this case, the type ordering determines the result:
number < atom < reference < fun < port < pid < tuple < map < list < bitstring (binary)
A number is smaller than any other type, which is why TestNum.test/1
always returns :positive
if you provide a non-number. To fix this, you have to extend the guard by testing whether the argument is a number, as illustrated next.
defmodule TestNum do
def test(x) when is_number(x) and x < 0 do
:negative
end
def test(0), do: zero
def test(x) when is_number(x) and x > 0 do
:positive
end
end
This code uses the function Kernel.is_number/1
to test whether the argument is a number. Now TestNum.test/1 raises an error if you pass a non-number:
1)> TestNum.test(-1)
iex(#> :negative
2)> TestNum.test(:not_a_number)
iex(#> ** (FunctionClauseError) no function clause matching in TestNum.test/1
When the function body contains only one line, its possible to exclude the end
keyword with an empty guard
def foo(term), do: term
Notice the comma behind foo(term)
which represents an empty, catch-all guard.