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 thehead
andtail
and recursively calls itself passing just the remaining part of the list, thetail
. 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.