Elixir Pattern Matching Function Arguments

Kentaro Kaneki
8 min readMay 28, 2021
The left and the right sides match!

Introduction

In declarative programming languages like Elixir, use of if statements for control flow is uncommon. Instead, Elixir uses multiple function clauses to manage conditional logic. This means that you can define functions with the same name as long as the guard clauses and/or the parameters differ for each. Parameters can differ in arity (number of parameters) or they can have differing pattern matching.

In this article we will focus on how pattern matching works and how that can be used to create multiple clauses for each function. Let’s start by talking about the match operator and how pattern matching works!

Match Operator

In many languages the equal sign, =, is used to assign a value to a variable and is referred to as the assignment operator. Similarly, in Elixir = can be used to assign a value to a variable.

iex> foo = "bar"

However, in Elixir = is called the match operator and can do much more than just assign. The match operator, as the name suggests, checks if the values on both sides of operator are equal. It returns the value if both sides are indeed equal.

iex> foo = "bar"
"bar"
iex> "bar" = foo
"bar"

In most languages "bar" = foo would not be a valid expression. However, as demonstrated above, this expression in Elixir returns "bar".

If the values are not equal, the match operator will try to coerce the left hand side to equal the right. If the operator fails to coerce the left side, it will throw a MatchError.

iex> foo = "bar"
"bar"
iex> foo = "something else"
"something else"
iex> "random text" = foo
** (MatchError) no match of right hand side value: "something else"

In the example above, foo = “something else” assigns "something else" to foo even though foo was previously assigned "bar".

Then, the next expression throws a MatchError. This is because the match operator attempts to coerce the left side to equal the right. You cannot assign a value to a string literal. Unless the values are equal on both sides, the left side must include a variable.

If the values are equal, the left side does not need to be variable as demonstrated below.

iex> 2 = 1 + 1
2
iex> 11 = 1 + 1
** (MatchError) no match of right hand side value: 2
iex> foo = %{a: 1}
%{a: 1}
iex> %{a: 1} = foo
%{a: 1}

Side Note: Equality Operators

Elixir does have equality operators like == and ===. They function mostly as you would expect and return a boolean. You can read the docs on basic operators here.

Partial Matching and Destructuring

So far it is not very clear why pattern matching is beneficial. This section will show you how it can be very useful.

Strings

Pattern matching can be used to match parts of string. Let’s use the binary concatenation operator (<>) to parse the name from a string.

iex> "Hello " <> name = "Hello Mocha!"
iex> name
"Mocha!"

What if you want to match the greeting? Or, what if we don’t want the exclamation mark in the name?

iex> greeting <> " Mocha!" = "Hello Mocha!"
** (ArgumentError) the left argument of <> operator inside a match should always be a literal binary because its size can't be verified. Got: greeting
iex> "Hello " <> name <> "!" = "Hello Mocha!"
** (ArgumentError) the left argument of <> operator inside a match should always be a literal binary because its size can't be verified. Got: name

The left side of a binary concatenation operator must be a binary (string is a binary in Elixir). In both examples, a variable precedes a binary concatenation operator. You will need to use a regex to accomplish these tasks.

Side note: Regex

Even though this does not use the match operator, I didn’t want to leave you wondering how to solve the challenge above.

iex> regex = ~r{(?<greeting>\w+) (?<name>\w+)!}iex> captures = Regex.named_captures(regex, "Hello Mocha!")
%{"greeting" => "Hello", "name" => "Mocha"}
iex> captures["greeting"]
"Hello"
iex> captures["name"]
"Mocha"

Let’s break down the regex: ~r{(?<greeting>\w+) (?<name>\w+)!}. For a more detailed explanation, check this out.

  • ~r{} defines a regex. This is Elixir syntax, but everything inside will be regex syntax.
  • () creates a regex capturing group.
  • ?<greeting> names the regex capturing group.
  • \w+ defines what the capturing group will match on. \w+ matches any word character (alphanumeric and underscore).

The Regex.named_captures will return a map with the matches. The name of each capturing group will be the key for string it matched.

Lists and Tuples

Similar to destructuring in JavaScript, you can use the match operator to unpack values from lists, tuples, and maps.

# List
iex> [first, second, third] = [1, 2, 3]
iex> first
1
# Tuple
iex> {first, second, third} = {:hello, "world", 42}
iex> first
:hello

Tuples and lists must have the same number of elements on both sides. However, you can choose to ignore certain values in a list or tuple using an underscore (`_`) as shown below.

# List
iex> [first, _, third] = [1, 2, 3]
iex> first
1
# Tuple
iex> {first, _, third} = {:hello, "world", 42}
iex> first
:hello

Lists and Tuples: Match on Known Values

Instead of a variable or an underscore, you can pass in values. This will come in handy when writing functions parameters with a match operator. Just note that because it is a match operator and not an assignment operator, it will throw an error if the values don’t match up.

# List
iex> [first, 2, third] = [1, 2, 3]
iex> first
1
iex> [first, 2, third] = [1, 3, 5]
** (MatchError) no match of right hand side value: [1, 3, 5]

Maps

Here is how you can destructure maps.

# Map
iex> %{a: foo} = %{a: 1, b: 2}
iex> foo
1
iex> %{"a" => foo} = %{"a" => 1, b: 2}
iex> foo
1
iex> %{a: foo, b: bar} = %{a: 1}
** (MatchError) no match of right hand side value: %{a: 1}

Notice that with a map, the left side does not need to contain all the keys on the right side. However, the right side needs to have all of the keys found on the left. In other words, the map on right side must be a superset of the map on the left.

Maps: Match on Known Values

With maps there is no need for underscores, because the left side does not need to have all of the keys of the right side. However, it is useful to be able to match on known values. Again, it will throw an error if the values don’t match up.

# Map
iex> %{a: foo, b: 2} = %{a: 1, b: 2, c: 3}
iex> foo
1
iex> %{a: foo, b: 3} = %{a: 1, b: 2, c: 3}
** (MatchError) no match of right hand side value: %{a: 1, b: 2, c: 3}

The second example throws an error, because b: is 3 in the left map and 2 in the right map.

Pattern Matching in Function Parameters

As discussed earlier, pattern matching is a way to have multiple function clauses with the same function name. This is where pattern matching becomes super useful!

Before we start, I want to point out that the match operator will not throw an error when used on function arguments. Instead, Elixir will continue to look for a function that matches the arguments passed in. This is unlike earlier where the match operator would throw a MatchError.

Strings

Let’s say we are routing HTTP requests based on the route. In the example below, first route function will match any requests with the URL, "/pets".

The second one will do the same for "/people". Only difference is that the argument will be accessible via the route_url variable. This is not very practical with strings, since route_url can only be assigned one value, "/people". I included here to point out that the match operator does not assign "/people" to route_url. To set default arguments, use \\.

If no route function matches the argument passed in, Elixir will throw an error. If you’re building an HTTP server, make sure to include a catchall that will return a 404.

defmodule PetRoutes do
def route("/pets") do
PetView.render_index_page()
end

def route(route_url = "/people") do
IO.puts(route_url)
PersonView.render_index_page()
end
end
MyModule.is_it_good("Penguins")
# outputs "Penguins are great!"
MyModule.is_it_good("Lions")
# outputs "Lions are okay."

What if we want to match a part of a string? Let’s say the URL has an ID parameter that can vary.

defmodule PetRoutes do
...
def route("/pets/" <> id) do
PetView.render_show_page(id)
end
end

Any value after /pets/ in the URL will be assigned to id. In this example, the id is passed along to render the page.

How about a create page at /pets/new? You can define another function that matches the route.

defmodule PetRoutes do
...
def route("/pets/new") do
PetView.render_create_page()
end
def route("/pets/" <> id) do
PetView.render_show_page(id)
end
end

Keep in mind that the order of the function definitions is crucial. Elixir will go through the functions top to bottom and match the first function. If we put the create route definition below the show route definition, the new in "/pets/new” will match with id in “/pets/” <> id and take you to the show page.

Lists and Tuples

When using pattern matching for function parameters, you can extract the variables from the list and access the list itself. In the second function definition below, notice the = friends. This assigns the whole list to the variable friends.

Note: I will only show the examples for lists, because tuples work basically the same way. Enum.count(friends) will not work for tuples and one more difference is noted below.

defmodule Pet do
def list_friends([friend_1, friend_2, friend_3]) do
friends = "#{friend_1}, #{friend_2}, and #{friend_3}"
IO.puts("My pet penguin's best friends are #{friends}!")
end
def list_friends([friend_1, friend_2] = friends) do
friend_count = Enum.count(friends)
friends = "#{friend_1}, #{friend_2}"
IO.puts("My pet penguin has #{friend_count} friends!")
IO.puts("They are #{friends}!")
end
def list_friends([]) do
IO.puts("My pet penguin doesn't have any friends")
end
end

Make sure the length of the list (or tuple) you are matching on is the same as the length of the list (or tuple) passed into the function. If you have a variable length list, you will need to use recursion and head | tail, which you can read about here.

A difference between lists and tuples is that head | tail only works with lists and it is much more complicated dealing with variable length tuples.

Maps

defmodule Pet do
def print(%{name: pet_name, likes_cookies: true} = pet) do
IO.puts("My pet's name is #{pet_name}")
IO.puts("Favorite cookie is #{pet.fav_cookie}")
end
def print(%{name: pet_name, likes_cookies: false}) do
IO.puts("My pet's name is #{pet_name}")
IO.puts("#{pet_name} doesn't like cookies")
end
end

Remember that maps can pattern match on literal values. In the example above, both functions match on likes_cookies. If likes_cookies does not equal true or false, neither function will be matched.

Similar to lists and tuples, you can assign the whole map to a variable. In the first function definition, the map is assigned to pet. pet variable is used to print out the pet’s favorite kind of cookie

Conclusion

Pattern matching is a very powerful tool in Elixir that can be used to handle conditional logic and destructure data structures. This significantly reduces code clutter and nesting, which improve the readability of the code. It is a fundamental building block for a functional programming language like Elixir.

--

--