Elixir Pattern Matching

Héla Ben Khalfallah
Sep 4 · 10 min read

In this story, we will see together the different aspects of Elixir Pattern Matching: tuples, list, map and multiclause function.

Image for post
Image taken from : http://ravindranaik.com/pattern-matching-in-c-8/

Tuples

Let’s assume that we have a function that return the gps coordinates (latitude, longitude) for a given country : get_gps_coordinates.

defmodule ElixirPatternMatching.Examples do
def get_gps_coordinates(country) do
%{
France: {46.71109, 1.7191036},
Spain: {40.2085, -3.713},
Italy: {41.29246, 12.5736108}
}[country]
end
end

This function return a Tuple : { latitude, longitude }.

Let’s call this function to get the gps coordinates for ‘France’ :

defmodule ElixirPatternMatching do
import ElixirPatternMatching.Examples,
only: [get_gps_coordinates: 1]


{latitude, longitude} = get_gps_coordinates(:France)

IO.inspect("latitude : #{latitude}")
IO.inspect("longitude : #{longitude}")
end

We have destructured tuple elements to individual variables : latitude and longitude (respecting the order).

Image for post
Pattern Matching — Tuple

The variables ‘latitude’ and ‘longitude’ are bound to the corresponding elements of the tuple.

What happens if the right side doesn’t correspond to the pattern ?

Let’s call our function like this :

{latitude, longitude, elevation} = get_gps_coordinates(:France)

The match fails, and an error is raised:

Image for post
Pattern Matching — Tuple — Error

Our function return a Tuple with only two values : latitude and longitude. No right side value matching for elevation.

Tuples are like a statically-sized arrays and are meant to hold a fixed number of elements.

What if we call like this :

{latitude, longitude, elevation} = "46.71109, 1.7191036"

The same thing, the match fails, and an error is raised:

Image for post
Pattern Matching — Tuple — Error

We can handle exception like this :

try do
{latitude, longitude, elevation} = "46.71109, 1.7191036"

IO.inspect("elevation : #{elevation}")
IO.inspect("latitude : #{latitude}")
IO.inspect("longitude : #{longitude}")
catch
type, value ->
IO.puts("Error\n #{inspect(type)}\n #{inspect(value)}")
end

And the result will be :

Image for post
Pattern Matching — Tuple — Catch Error

What if we need only longitude ?

We can use anonymous variable :

{_latitude, longitude} = get_gps_coordinates(:France)

#IO.inspect("latitude : #{latitude}")
IO.inspect("longitude : #{longitude}")

Or :

{_, longitude} = get_gps_coordinates(:France)

#IO.inspect("latitude : #{latitude}")
IO.inspect("longitude : #{longitude}")

Anonymous variable works just like a named variable: it matches any right-side term. But the value of the term isn’t bound to any variable.

Matching can be nested ?

Yes !

user = {
"Hela",
"Ben Khalfallah",
{
"My City",
"123456",
{
46.71109,
1.7191036
}

}
}

{_, _, {_, _, {user_latitude, user_longitude}}} = user

IO.inspect("user_latitude : #{user_latitude}")
IO.inspect("user_longitude : #{user_longitude}")
iex(12)> recompile
Compiling 1 file (.ex)
"longitude : 1.7191036"
"user_latitude : 46.71109"
"user_longitude : 1.7191036"

Matching to the content of the variable

country = "Italy"
{^country, _} = {"Italy", "IT"}

Using ^country in patterns says that we expect the value of the variable country to be in the appropriate position in the right-side term. In this example, it would be the same as if you used the hard-coded pattern {“Italy”,_}.

Doing this :

country = "Italy"
{^country, _} = {"France", "IT"}

Raise an exception :

Image for post
Pattern Matching — Tuple — Matching error

List

Let’s assume that we have a function get_all_countries that returns a list of Country :

defmodule ElixirPatternMatching.Country do
defstruct name: "", code: ""
end
defmodule ElixirPatternMatching.Examples do
alias ElixirPatternMatching.Country

def get_all_countries do
[
%Country{name: "France", code: "FR"},
%Country{name: "Spain", code: "ES"},
%Country{name: "Italy", code: "IT"}
]
end
end

I want to get the first, second, and third items

[first, second, third] = get_all_countries()

IO.inspect("first : #{inspect(first)}")
IO.inspect("second : #{inspect(second)}")
IO.inspect("third : #{inspect(third)}")
iex(16)> recompile
Compiling 1 file (.ex)
"first : %ElixirPatternMatching.Country{code: \"FR\", name: \"France\"}"
"second : %ElixirPatternMatching.Country{code: \"ES\", name: \"Spain\"}"
"third : %ElixirPatternMatching.Country{code: \"IT\", name: \"Italy\"}"

We destructure List to get items.

I want to get the first and the third items only

[first_item, _, third_item] = get_all_countries()

IO.inspect("first_item : #{inspect(first_item)}")
IO.inspect("third_item : #{inspect(third_item)}")
iex(20)> recompile
Compiling 1 file (.ex)
"first_item : %ElixirPatternMatching.Country{code: \"FR\", name: \"France\"}"
"third_item : %ElixirPatternMatching.Country{code: \"IT\", name: \"Italy\"}"

This is like : give me the first item, skip the second and then give me the third.

We can also do :

[first_item, _second_item, third_item] = get_all_countries()

IO.inspect("first_item : #{inspect(first_item)}")
IO.inspect("third_item : #{inspect(third_item)}")
iex(21)> recompile
Compiling 1 file (.ex)
"first_item : %ElixirPatternMatching.Country{code: \"FR\", name: \"France\"}"
"third_item : %ElixirPatternMatching.Country{code: \"IT\", name: \"Italy\"}"

We don’t need the third item but it must be present

[first_item, second_item, _] = get_all_countries()

IO.inspect("first_item : #{inspect(first_item)}")
IO.inspect("second_item : #{inspect(second_item)}")
iex(22)> recompile
Compiling 1 file (.ex)
"first_item : %ElixirPatternMatching.Country{code: \"FR\", name: \"France\"}"
"second_item : %ElixirPatternMatching.Country{code: \"ES\", name: \"Spain\"}"

Else an exception will been raised :

In the below example, our List contains only two items but we access to the third item :

[first_item, second_item, _] = [
%Country{name: "France", code: "FR"},
%Country{name: "Spain", code: "ES"}
]

IO.inspect("first_item : #{inspect(first_item)}")
IO.inspect("second_item : #{inspect(second_item)}")
Image for post
Pattern Matching — List — Matching error

I want to get the first item and collect the rest into an array

[first_element | rest] = get_all_countries()

IO.inspect("first_element : #{inspect(first_element)}")
IO.inspect("rest : #{inspect(rest)}")
Image for post
Pattern Matching — List — Rest

We can also skip the rest :

[first_element | _] = get_all_countries()

IO.inspect("first_element : #{inspect(first_element)}")

Head and Tail

[head | tail] = get_all_countries() #1

IO.inspect("head : #{inspect(head)}")
IO.inspect("tail : #{inspect(tail)}")

[head | tail] = tail #2

IO.inspect("head : #{inspect(head)}")
IO.inspect("tail : #{inspect(tail)}")

[head | tail] = tail #3

IO.inspect("head : #{inspect(head)}")
IO.inspect("tail : #{inspect(tail)}")
Image for post
Pattern Matching — List — Head and Tail

We had recursively iterate over the List.

As you can see, in the last iteration, tail = [].

If we iterate once more, we get an exception :

Image for post
Pattern Matching — List — Head and Tail Error

Matching to the content of the variable

def get_all_countries do
[
%Country{name: "France", code: "FR"},
%Country{name: "Spain", code: "ES"},
%Country{name: "Italy", code: "IT"}
]
end
first_country = %Country{name: "France", code: "FR"}
[^first_country, second_country, _] = get_all_countries()

The first element must have the same value as the variable first_country.

Else, an exception will been raised :

first_country = %Country{name: "Italy", code: "it"}
[^first_country, second_country, _] = get_all_countries()
Image for post
Pattern Matching — List — Matching error

Map

Assume that we have a function get_book_by_id that return a book’s informations by id :

def get_book_by_id(id) do
%{
"1" => %{
name: "Elixir In Action",
writer: "Sasa Juric",
date: "2019"
},
"2" => %{
name: "Phoenix In Action",
writer: "Geoffrey Lessel",
date: "2019"
}
}[id]
end

The returned value of our function is a Map.

Get the book of id = “1” :

book = get_book_by_id("1")
IO.inspect("book : #{inspect(book)}")
iex(41)> recompile
Compiling 1 file (.ex)
"book : %{date: \"2019\", name: \"Elixir In Action\", writer: \"Sasa Juric\"}"

I want to get book’s information now :

%{name: name, writer: writer, date: date} = get_book_by_id("1")
IO.inspect("book name: #{inspect(name)}")
IO.inspect("book writer: #{inspect(writer)}")
IO.inspect("book date: #{inspect(date)}")
iex(42)> recompile
Compiling 1 file (.ex)
"book name: \"Elixir In Action\""
"book writer: \"Sasa Juric\""
"book date: \"2019\""

I want to get only the book’s name :

%{name: name} = get_book_by_id("1")
IO.inspect("book name: #{inspect(name)}")
iex(43)> recompile
"book name: \"Elixir In Action\""

When matching a map, the left-side pattern doesn’t need to contain all the keys from the right-side term (partial-matching).

A match will fail if the pattern contains a key that’s not in the matched term :

%{name: name, cover: cover, date: date} = get_book_by_id("1")
IO.inspect("book name: #{inspect(name)}")
IO.inspect("book cover: #{inspect(cover)}")
IO.inspect("book date: #{inspect(date)}")
Image for post
Pattern Matching — Map — Matching error

Matching binary strings

command = "ping www.example.com"
"ping " <> url = command

IO.inspect("command: #{inspect(command)}")
IO.inspect("url: #{inspect(url)}")
iex(44)> recompile
Compiling 1 file (.ex)
"command: \"ping www.example.com\""
"url: \"www.example.com\""

MultiClause functions

Now, let’s assume that we have a website that sells cars. 🚘

Users can :

  • display cars by type : BMW, Volkswagen, Ford, Kia.
  • or display all cars.

How we can do our requests ?

We can use if/else or other classical branching ways, but Elixir offers another great way to do, let’s see :

def get_cars(%{type: "bmw"} = params) do
IO.puts("I want only #{params.type} !")
end

def get_cars(%{type: "volkswagen"} = params) do
IO.puts("I want only #{params.type} !")
end

def get_cars(%{type: "ford"} = params) do
IO.puts("I want only #{params.type} !")
end

def get_cars(%{type: "kia"} = params) do
IO.puts("I want only #{params.type} !")
end
# the default clause
def get_cars(_) do
IO.puts("I want all cars !")
end

Calling our get_cars :

params = %{
type: "volkswagen"
}

get_cars(params)
iex(47)> recompile
Compiling 1 file (.ex)
I want only volkswagen !

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. — Elixir in Action —

Then instead of having classical branching (if/else), we can do multiple clauses for our function get_cars.

Elixir will evaluate clauses from top to bottom. This is why I keep the default get_cars at the end. Else no specific clause will be invoked.

By putting the default clause the first :

def get_cars(_) do
IO.puts("I want all cars !")
end


def get_cars(%{type: "bmw"} = params) do
IO.puts("I want only #{params.type} !")
end

def get_cars(%{type: "volkswagen"} = params) do
IO.puts("I want only #{params.type} !")
end

def get_cars(%{type: "ford"} = params) do
IO.puts("I want only #{params.type} !")
end

def get_cars(%{type: "kia"} = params) do
IO.puts("I want only #{params.type} !")
end

I get :

I want all cars !

What about using a Struct instead of Map ?

Let’s define a Car Struct :

defmodule ElixirPatternMatching.Car do
defstruct type: "", category: ""
end

Now we change our get_cars clauses like this :

alias ElixirPatternMatching.Cardef get_cars(%Car{type: "bmw"} = car) do
IO.puts("I want only #{car.type} !")
end

def get_cars(%Car{type: "volkswagen"} = car) do
IO.puts("I want only #{car.type} !")
end

def get_cars(%Car{type: "ford"} = car) do
IO.puts("I want only #{car.type} !")
end

def get_cars(%Car{type: "kia"} = car) do
IO.puts("I want only #{car.type} !")
end

def get_cars(%Car{} = _car) do
IO.puts("I want all cars !")
end

Calling get_cars :

car = %Car{
type: "volkswagen",
category: "Golf"
}

get_cars(car)

Clause with Guard

Let’s change our clauses like this :

def get_cars(%Car{type: "bmw"} = car) do
IO.puts("I want only #{String.upcase(car.type)} #{String.upcase(car.category)} !")
end

def get_cars(%Car{type: "volkswagen"} = car) do
IO.puts("I want only #{String.upcase(car.type)} #{String.upcase(car.category)} !")
end

def get_cars(%Car{type: "ford"} = car) do
IO.puts("I want only #{String.upcase(car.type)} #{String.upcase(car.category)} !")
end

def get_cars(%Car{type: "kia"} = car) do
IO.puts("I want only #{String.upcase(car.type)} #{String.upcase(car.category)} !")
end

def get_cars(%Car{} = car) do
IO.puts("I want all cars !")
end

And let’s call our get_cars like this :

car = %Car{
type: "volkswagen",
category: 1
}

get_cars(car)

With the above use case I simulate a corrupted data case.

An exception is raised when running the code :

Image for post
Pattern Matching — Function — Matching error

Hmmm, this exception is raised because upcase should have a string as parameter’s type but I have called it with an integer type (category: 1). How to avoid execution or fail gracefully to the default clause if category isn’t a string ?

We can do this easily by using Guard with clause :

def get_cars(category, %Car{type: "bmw"} = car) when is_binary(category) do
IO.puts("I want only #{String.upcase(car.type)} #{String.upcase(car.category)} !")
end

def get_cars(category, %Car{type: "volkswagen"} = car) when is_binary(category) do
IO.puts("I want only #{String.upcase(car.type)} #{String.upcase(car.category)} !")
end

def get_cars(category, %Car{type: "ford"} = car) when is_binary(category) do
IO.puts("I want only #{String.upcase(car.type)} #{String.upcase(car.category)} !")
end

def get_cars(category, %Car{type: "kia"} = car) when is_binary(category) do
IO.puts("I want only #{String.upcase(car.type)} #{String.upcase(car.category)} !")
end

def get_cars(category, %Car{} = car) do
IO.puts("I want all cars !")
end

Calling get_cars :

import ElixirPatternMatching.Examples, only: [ get_cars: 2 ]car = %Car{
type: "volkswagen",
category: 1
}

get_cars(car.category, car)

Now we get :

iex(56)> recompile
Compiling 1 file (.ex)

I want all cars !
:ok
iex(57)>

I prefer the multiclause function to the classic branching especially when have multi-conditions, the code is more clear.


Conclusion

In this story we had seen together the different aspects of Elixir Pattern Matching: tuples, list, map and multiclause function.

I think that Pattern Matching technics enhance very much our development’s workflow: we can easily and simply access the values of tuples or list or maps.

Also mutliclause function are a good way to have branching without using the classical way using if/else. So the code still clear and easy to read and understand because it’s sequential (no branching).



Thank you for reading my story.

You can find me at :

Twitter : https://twitter.com/b_k_hela

Github : https://github.com/helabenkhalfallah

The Startup

Medium's largest active publication, followed by +705K people. Follow to join our community.

Héla Ben Khalfallah

Written by

I love coding whatever the language and I love trying new programming tendencies. I have a special love to JS (ES6+), functional programming, React and Redux.

The Startup

Medium's largest active publication, followed by +705K people. Follow to join our community.

Héla Ben Khalfallah

Written by

I love coding whatever the language and I love trying new programming tendencies. I have a special love to JS (ES6+), functional programming, React and Redux.

The Startup

Medium's largest active publication, followed by +705K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store