Pattern matching elixir

One of the great features of both Elixir and Erlang is pattern matching. Pattern matching allows you to work with the “shape” of data. Using pattern matching elixir, you can split an implementation up based on the shape of the data. There are various ways to define the shape of the data you expect. This post shows the various forms pattern matching can take in Elixir.

Throughout this post, we will use a simple greet function as an example. This greet function takes a name and then prints out a “hello” message to the screen. It looks like this:

def greet(name) do 
IO.puts("Hello, #{name}") 
end

Let’s start by discussing the basic pattern matching elixir elements.

Ignore Value

Use an underscore to ignore a value altogether.

def greet(_) do
IO.puts("Hello, world")
end

The greet function above ignores the given input. If for some reason you want to ignore a value, but still give it a meaningful name, prepend that value with an _.

def greet(_name) do
 IO.puts("Hello, world")
end

What if you want to use the value given? Give it a name without an underscore.

Variable

def greet(name) do
 IO.puts("Hello, #{name}")
end

Now you can use the name given. While you could have used _name, it would have given us a compiler warning. The compiler will warn you if you are using a variable starting with _.

Exact Value

With pattern matching, you can give an exact value as well.

def greet("Benjamin") do
 IO.puts("Hello, Ben")
end

We now have a function for someone named “Benjamin”. This works with all primitive values: integers (1), floats (1.0), binaries ("Sally"), bitstrings (<<1, 2, 3>>), tuples ({1,2}), lists ([1, 2, 3]), and maps (%{a: 1, b: 2}).

We will go into more depth on lists, tuples, and maps later.

Binary starts with

def greet("Ben" <> _) do
 IO.puts("Hello, Ben")
end

With this form, you can check if a given binary starts with the given characters. We have enhanced our greet Ben function to check for any name that starts with "Ben". Note, you can only match on the beginning of a binary.

Lists

Lists can be pattern-matched in several ways. The first way is by matching on the number of values in the list.

def greet(["Ben" <> _, _last_name]) do
 IO.puts("Hello, Ben")
end

The above expects a 2-element list as input.

Head/Tail

def greet([name | _]) do
 IO.puts("Hello, #{name}")
end

This form allows you to get the first value of a list in a variable. It also gets the rest of the elements in a list as a variable. In this case, we only need the first element. You can also match on the first number of elements.

def greet([first_name, last_name | _]) do
 IO.puts("Hello, #{first_name} #{last_name}")
end

Keyword Lists

def greet([first_name: name, last_name: _]) do
 IO.puts("Hello, #{name}")
end

In this example, we are matching elements of a keyword list. This works exactly the same way as list pattern matching (because a keyword list is a list). Remember that order of keys matter here. So if last_name came first in the list given to this function, it would not match.

Tuples

def greet({"Ben" <> _, _last_name}) do
 IO.puts("Hello, Ben")
end

Tuples are like lists except that they have an arity. For example, {1, 2} is a tuple with arity 2, or sometimes called a 2-tuple. {1, 2, 3} is a 3-tuple. In Elixir, you can match on the values with the tuple. In the above example, our greet function takes a 2-tuple. The first element is a name that starts with "Ben" and we ignore the second element. Note that this will only match on 2-tuples and not any other arity tuple. If we want to update our function to take a 3-tuple, we could do as follows:

def greet({"Ben" <> _, _middle_name, _last_name}) do
 IO.puts("Hello, Ben")
end

Maps

def greet(%{first_name: name}) do
 IO.puts("Hello, #{name}")
end

This type of pattern matching checks for keys and values within a map. Order does not matter here. As long as the given map has the key(s) and value(s) matching, this will work.

Structs

Structs support pattern matching in a couple of ways. One is matching on the type.

def greet(%Person{} = person) do
 IO.puts("Hello, #{person.name}")
end

This will match only if given a Person struct.

You can even make the type something to match.

def greet(%x{} = person) when x in [Person] do
 IO.puts("Hello, #{person.name}")
end

The variable, x, holds the type of the struct passed into the function. We used a guard (explained later) to make sure the type is within a subset of types defined.

Bitstrings

Bitstrings are useful when working with certain types of data. They also have the most variations when it comes to pattern matching. We will discuss one example here, but be sure to read the Bitstring docs for more information.

In the example above where we matched on the beginning of a name, I mentioned that you can only match on the beginning. For that particular form of pattern matching, that is true. With bitstrings, you can match on other parts as long as you know the length of the parts before it.

def greet(<<prefix::binary-size(3), "ja", ending::binary>>) do
 IO.puts("Hello, #{prefix}js#{ending}")
end

Notice we are matching on the size of the first element in the bitstring. We are expecting it to be a binary that is 3 characters long. Skipping the middle element, notice the end does not need to have a size. This is because it is the last element.

Guards

Guards allow you to add an extra constraint onto your pattern. What if we wanted to make sure the name given is a binary (string)?

def greet(name) when is_binary(name) do
 IO.puts("Hello, #{name}")
end

Now only binary values will match the above. Only a small set of functions can be guards. To find out which ones, check out the Elixir Kernel doc. Most of the functions allowed have the comment “Allowed in guard tests” in their docs.

There are various ways to define the shape of the data you expect. This post shows the various forms pattern matching can take in Elixir.

We're building an AI-powered Product Operations Cloud, leveraging AI in almost every aspect of the software delivery lifecycle. Want to test drive it with us? Join the ProdOps party at ProdOps.ai.