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
#> 14

Matching 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
#> 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

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
#> 67
command = "ping www.google.com"
"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.

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
  ...
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
    a* b
end
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
        a* b
    end

    def area({:square, a}) do
        a* a
    end

    def area({:circle, r}) do
        r * r * 3.14
    end
end
iex(1)> Geometry.area({:rectangle, 4, 5})
#> 20
iex(2)> Geometry.area({:square, 5})
#> 25
iex(3)> Geometry.area({:circle, 4})
#> 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
        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
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:

iex(1)> TestNum.test(-1)
#> :negative
iex(2)> TestNum.test(0)
#> :zero
iex(3)> TestNum.test(1)
#> :positive

Surprisingly enough, calling this function with a non-number yields strange results:

iex(4)> TestNum.test(:not_a_number)
#> :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:

iex(1)> TestNum.test(-1)
#> :negative
iex(2)> TestNum.test(:not_a_number)
#> ** (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.

3.3.1 Lambda functions

We can also have lambda functions with guards.

test_num =
    fn
        x when is_number(x) and x < 0 -> :negative

        0 -> :zero

        x when is_number(x) and x > 0 -> :positive
    end