3 Pattern Matching
3.1 Matching Tuples, Lists, Maps and Strings
Matching tuples
{date, time} = :calendar.local_time()
date
#> {2022, 1, 2}
time
#> {14, 44, 35}Nested match
{_, {hour, _, _}} = :calendar.local_time()
hour
#> 14Matching lists
[first, second, third] = [1, 2, 3]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:
iex(3)> [head | tail] = [1, 2, 3]
#> [1, 2, 3]
iex(4)> head
#> 1
iex(5)> tail
#> [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:
iex(6)> [min | _] = Enum.sort([3, 2, 1])
iex(7)> min
#> 1Match 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
#> 25Of 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 valueIn 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
iex(1)> Test.my_func(%{"a" => 1})
#> {:a, 1}
iex(2)> Test.my_func(%{"b" => 2})
#> {:b, 2}
iex(3)> Test.my_func(%{"a" => 1, "b" => 2})
#> {:a, 1}Match strings
iex(13)> <<b1, b2, b3>> = "ABC"
#> "ABC"
iex(13)> b1
#> 65
iex(14)> b2
#> 66
iex(15)> b3
#> 67command = "ping www.google.com"
"ping " <> url = command
url
#> www.google.comOccasionally, you’ll need to match against the contents of the variable. For this pur- pose, the pin operator (^) is provided.
iex(7)> expected_name = "Bob"
# Matches to the content of the variable expected_name
iex(8)> {^expected_name, _} = {"Bob", 25}
#> {"Bob", 25}
iex(9)> {^expected_name, _} = {"Alice", 30}
#> ** (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
...
endThe 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
a* b
end
end
Rectangle.area({2, 3})
#> 6With 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
a* b
end
def area({:square, a}) do
a* a
end
def area({:circle, r}) do
r * r * 3.14
end
endiex(1)> Geometry.area({:rectangle, 4, 5})
#> 20
iex(2)> Geometry.area({:square, 5})
#> 25
iex(3)> Geometry.area({:circle, 4})
#> 50.24Sometimes 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
a* b
end
def area({:square, a}) do
a* a
end
def area({:circle, r}) do
r * r * 3.14
end
def area(unknown) do
{:error, {:unknown, unknown}}
end
endIf 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
endThe 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:
iex(1)> TestNum.test(-1)
#> :negative
iex(2)> TestNum.test(0)
#> :zero
iex(3)> TestNum.test(1)
#> :positiveSurprisingly enough, calling this function with a non-number yields strange results:
iex(4)> TestNum.test(:not_a_number)
#> :positiveThe 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
endThis 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:
iex(1)> TestNum.test(-1)
#> :negative
iex(2)> TestNum.test(:not_a_number)
#> ** (FunctionClauseError) no function clause matching in TestNum.test/1When the function body contains only one line, its possible to exclude the end keyword with an empty guard
def foo(term), do: termNotice the comma behind foo(term) which represents an empty, catch-all guard.