The Beauty of Pattern Matching in Elixir


Transcript

One of the reasons I fell in love with Elixir is pattern matching and how it can be extensively used all over the code.

Match Operator

Let’s start the interactive Elixir console, iex and let’s do what is seems to be a normal assignment

x = 1

In this way we bind the variable x to the value 1, using the match operator =, which tries to match the right part to the left part.

1 = x
1 = 1

These are also a valid expressions, since the two sides match.

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

iex> 2 = x
** (MatchError) no match of right hand side value: 1

Since the two sides don’t match we get a MatchError.

We can re-bind the variable with a new value

iex> x = 2
2
iex> 2 = x
2

Tuples

Let’s consider a tuple of three elements {1, 2, 3}. We want to bind a variable to each element.

iex> {x, y, z} = {1, 2, 3}
iex> x
1
iex> y
2
iex> z
3

The two sides match and each number of the tuple on the right is assigned to a variable on the left.

If we try to match two different tuples, with different number of elements, they obviously don’t match, triggering a MatchError.

iex> {x, y, z} = {1, 2, 3, 4}
** (MatchError) no match of right hand side value: {1, 2, 3, 4}

We can also specify one value, in the left tuple, along with the variables.

iex> {1, y, z} = {1, 2, 3}

In this way we force the first element of the tuple, which needs to match.

iex> {2, y, z} = {1, 2, 3}
** (MatchError) no match of right hand side value: {1, 2, 3}

In this case the first element of the two tuples is not the same, so the two sides don’t match.

^ pin operator

When a variable is bound to a value, and we don’t want to rebind it during a match expression we can use the ^ pin operator

iex> x = 1
iex> {^x, y, z} = {1, 20, 30}

In this way, instead of re-binding the variable x to the value 1, we are forcing a match against its current value.

iex> {^x, y, z} = {10, 20, 30}
** (MatchError) no match of right hand side value: {10, 20, 30}

We see how the last expression leads to a MatchError since is like doing

iex> {1, y, z} = {10, 20, 30}

:ok, :error

This is useful when we want to check if a function returns successfully

iex> {:ok, file} = File.open "hello.txt", [:write]
{:ok, #PID<0.105.0>}

The File.open function returns successfully with a {:ok, ...} tuple, so we match the :ok atom and assign the opened file to the file variable.

When we try to make the File.open to fail, it returns an {:error, error} tuple

iex> {:ok, file} = File.open("/invalid/directory/hello.txt", [:write])
** (MatchError) no match of right hand side value: {:error, :enoent}

and it doesn’t match the {:ok, file} tuple on the left.

Control Flow

With pattern matching we can be really explicit about the cases we want to handle.

case File.open("hello.txt", [:write]) do
  {:ok, file} ->
    IO.write(file,"hello world")
    File.close(file)

  {:error, error} ->  
    IO.puts("Error openings the file: #{inspect error}")
end

We saw previously that the File.open function returns a tuple of two elements.

  • If it opens the file successfully, it returns {:ok, file} and we then close the file after writing a string into it
  • if there was an error opening the file, it returns {:error, error} and we just print the error code

When we ignore a part of the match, and we don’t want to assign that part to any variable, we use the underscore: _

iex> {1, 2, _} = {1, 2, 3}
{1, 2, 3}
iex> {1, 2, _} = {1, 2, 4}
{1, 2, 4}

With _ the matching value is ignored and not assigned to any variable

Lists

We can also pattern match other data structures like Lists. We can do what we did with the tuples but this time with lists

iex> [1, b, 3] = [1, 2, 3]
[1, 2, 3]
iex> b
2

In this case we bind the variable b to the value 2.

The cool thing is that with lists we can use pattern matching to split the head from the tail.

iex> [ head | tail ] = [1, 2, 3]

iex> head
1
iex> tail
[2, 3]

This is pretty useful when we want to go through the whole list of elements recursively.

defmodule Example do
  def next_element([ head | tail ]) do
    IO.puts("#{inspect(head)} : #{inspect(tail)}"
    next_element(tail)
  end

  def next_element([]) do
    IO.puts("finished")
  end
end

This only argument of the next_element function is a list.

  • The first clause of the function, matches when the list is not empty. It extract the head, prints the head and tail and recursively calls itself passing just the remaining part of the list, the tail.
  • next_element([]) matches when the list is empty, and we reached the end of the recursion.
iex> Example.next_element [1, 2, 3, 4, 5]
1 : [2, 3, 4, 5]
2 : [3, 4, 5]
3 : [4, 5]
4 : [5]
5 : []
finished
:ok

Maps

We can obviously use pattern matching also with Maps.

eth_data = %{ "ETH" => 125.0 }
btc_data = %{ "BTC" => 3575.0 }

In this case the two maps represent two HTTP responses from an API. We want now to handle these two cases in two different ways.

We can use pattern matching and multi-clauses function to process both the responses.

defmodule API do

  def process(%{"BTC" => btc}) do
    IO.puts("Bitcoin: #{btc}")
  end

  def process(%{"ETH" => eth}) do
    IO.puts("Ethereum: #{eth}")
  end

  def process(%{"LTC" => ltc}) do
    IO.puts("Litecoin: #{ltc}")
  end

  def process(_) do
    IO.puts("any other response")
  end
end

We see that using pattern matching it’s really easy and clean to define different clauses of the process function where we handle the different maps in different ways.

The last function is then a catchall function that catches all the other cases.

iex> API.process %{"ETH" => 125.0}
Ethereum: 125.0
iex> API.process %{"BTC" => 3575.0}
Bitcoin: 3575.0

iex> API.process %{"invalid key" => "hello"}
any other response

Wrapping up

The are many other ways of using pattern matching in Elixir. This was just an intro and we will see pattern matching with binaries and structs in next episodes.

Leave a comment below to tell me how you find pattern matching in Elixir and to tell me if you started to you pattern matching in your code.

If you have any question please leave a comment in the comments section below. Subscribe to receive updates on new articles and episodes.