elixir, pattern-matching

Getting Started with Elixir - Pattern Matching

We discussed the = operator in the previous section where we used it to assign a value to a variable.  But in Elixir it's actually called the match operator because it has a powerful feature built into it that that enables us to perform what is known as "Pattern Matching".

Pattern Matching is used almost as often as functions in Elixir which makes it an important core feature to utilize in your programs.

Pattern Matching is the act of checking a given sequence of tokens for the presence of the constituents of some pattern.

A slightly cryptic definition, but we can break this down using some examples.

Ensure you have Elixir and IEx installed to follow along and test the examples.

Elixir Series

Versions

  • Elixir 1.10.0

Basic Example

Let's try a few examples to demonstrate how pattern matching works:

iex> 4 = 2 + 2
4

Nothing surprising here.

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

Let's break down this example:

  • 4 - the left-hand side, or "the constituents of some pattern"
  • 2 + 1 - the right-hand side, or "the sequence of tokens"

In this simple example, the pattern we matched against did not exist in the sequence of tokens hence the error.

Tuple Examples

We discussed tuples in the section on Elixir Data Types.

Tuples are commonly used a return value from functions in Elixir and we can use Pattern Matching for destructuring.

Let's say you have a tuple that contains result atom (:ok) and a dog's name, and you wanted to display just dog's name:

iex> result = {:ok, "titus"}
{:ok, "titus"}

iex> elem(result, 1)
"titus"

You could use elem/2 to access name which works, but this is where pattern matching in Elixir shines.

Let's pattern match on the result and assign a value to dog_name:

iex> {result, dog_name} = {:ok, "titus"}
{:ok, "titus"}

iex> result
:ok

iex> dog_name
"titus"

This simple feature is a powerful when working with Elixir.  We're able to pattern match and bind values to variables with simple notation.

If a left-hand value in your pattern is not going to be used, the convention is to replace it with and underscore _ .

iex> {_, dog_name} = {:ok, "titus"}
{:ok, "titus"}

The variable _ is special in that it can never be read from. Trying to read from it gives a compile error.

** (CompileError) iex:1: invalid use of _. "_" represents a value to be ignored in a pattern and cannot be used in expressions

Definition Breakdown

Let's break this Example down using the "definition":

  • We're checking the given sequence of tokens (right-hand side)
  • For the presence of the constituents of some pattern (left-hand side)

NOTE: We can only assign variables on the left-hand side of an expression.

For example, this raises an error:

iex> {:ok, "titus"} = {:ok, dog_name}
** (CompileError) iex:1: undefined function dog_name/0
(stdlib 3.11) lists.erl:1354: :lists.mapfoldl/3
(stdlib 3.11) lists.erl:1355: :lists.mapfoldl/3

Pin Operator

Before we go much further we should discuss the Pin Operator.

There are scenarios where you would like use a value already bound to a variable.

In these situations you can use the pin operator ^ to pattern match against an existing variable’s value rather than rebinding the variable:

iex> x = 1
1

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

iex> {y, ^x} = {2, 1}
{2, 1}

iex> y
2

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

The pin operator will tell Elixir that you would like to use the currently bound value of x rather than re-bind a new value to it.

You'll see this used throughout Elixir programs and it'll be an important part of your Elixir toolbox.

List Examples

Now that we have a good handle on how pattern matching works, we can look at some more complex examples.

Pattern Matching on Lists:

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

Nested Lists

Some lists include nested lists and other data types, and we can pattern match against the nested data:

iex> [a, b, [c, d]] = [1, 2, [3, 4]]
[1, 2, [3, 4]]
iex> a
1
iex> b
2
iex> c
3
iex> d
4

Head / Tail Format (Guillotine) 

Again, we're using a list of numbers to simplify the examples:

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

This format won't work on empty list 

iex> [head | tail] = []
** (MatchError) no match of right hand side value: []


NOTE: an important distinction to keep in mind when working with lists: 

The [head | tail] format is also used to prepend items to a list:

iex> list = [1, 2, 3]
[1, 2, 3]
iex> [0 | list]
[0, 1, 2, 3]

Combining Formats

If we need to grab the first and second items in a list, but aren't necessarily concerned with the "remainder":

iex> [a, b | other] = [1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> a
1
iex> b
2
iex> other
[3, 4, 5, 6]

In this case, at least three items will need to exist on the right-hand side for a match to be found. 

Map Examples

You'll see pattern matching on maps widely used in Elixir with some important differences than we've seen with lists and tuples.

Consider this example:

iex> %{a: one} = %{a: 1,  b: 2, c: 3}
%{a: 1, b: 2, c: 3}

iex> one
1

Unlike with lists or tuples, we can pattern-match on a subset of a value.

The map key(s) must match the pattern, but not the overall data-structure.

Matching Map Keys

String based keys will NOT match Atom based keys:

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

Nested Maps

Similar to Lists, we can pattern match on nested lists.  There are a few steps involved so let's look at the code then break it down:

iex> list = %{a: 1,  b: 2, c: 3}
%{a: 1, b: 2, c: 3}

iex> nested_list = %{list: list, other: "other"}
%{list: %{a: 1, b: 2, c: 3}, other: "other"}

iex> %{list: %{a: one}} = nested_list
%{list: %{a: 1, b: 2, c: 3}, other: "other"}

iex> one
1

We created a flat list, then assigned it to the :list key of a nested list.

Then in a single step we pattern matched on the nested list for the value with the key :a and assigned it to the variable one.

We can then access the nested value using: one.

Wrapping Up

We've only seen some contrived examples which have been used to demonstrate the features of pattern matching on different data types. 

Nevertheless, you can start to see how powerful pattern matching can become for destructuring complex pieces of data using very little code.  

Pattern matching is a main feature of Elixir that enables you to write more "declarative" code that's simpler to reason about and thereby easier to debug and manage in production.

Next, we're going to dive deeper into the heart of Elixir and explore functions.

Elixir Series

Connect

I hope you're finding this series helpful.  If you find any issues or have any feedback feel free to hit me up on twitter: @tmartin8080 || @phxroad  or subscribe to the mailing list to receive occasional updates.

Author image

About Troy Martin

Ruby, Elixir and Javascript Software Developer.