Variations on Pattern Matching in Elixir
One of the more common ways of pattern matching on lists is to separate the first item of the list from the rest of the items. In Elixir, this is done using a vertical bar symbol called the cons operator.
list = [1, 2, 3]
[head | tail] = list
In this little example, the variable head
will be bound to 1
and
the variable tail
will be bound to the list [2, 3]
.
The cons operator can also be used to build a larger list by adding items to the front of the given list.
head = 1
tail = [2, 3]
list = [head | tail]
In this example, head
is added to the beginning of tail
. In the
end, the variable list
is bound to the list [1, 2, 3]
.
Pattern matching as shown in the first example and list construction as shown in the second example are inverse operations of each other. Many languages have ways to take lists or arrays apart and put them back together. The Elixir and Erlang implementations are particularly beautiful because pattern matching and list construction look identical.
In Elixir, the methods described above can be easily extended to matching the first two (or more) list items.
[e1, e2 | tail] = [1, 2, 3]
Now, e1
will be bound to 1
, e2
will be bound to 2
, and tail
will be bound to the list [3]
with a single remaining item..
This way of pattern matching can be used recursively to walk through a
list as shown in the following little function that computes the sum
of consecutive list items. For the list [1, 2, 3, 4, 5]
, we expect
the result to be [3, 7]
. We ignore the last item in the given list
because there is no other number we could add.
defmodule ChunkConsecutive do
def add_consecutive([e1, e2 | rest]) do
[e1 + e2 | add_consecutive(rest)]
end
def add_consecutive([_]) do
[]
end
def add_consecutive([]) do
[]
end
def run do
# prints [3, 7]
IO.inspect(ChunkConsecutive.add_consecutive([1, 2, 3, 4, 5]))
end
end
In each recursive step, we consume the first two items of the given
list, naming them e1
and e2
. The rest
variable is bound to the
given list without the first two items. We add e1
and e2
and run
add_consecutive/1
passing in the remaining list items. To stop the
recursion, we return an empty list if add_consecutive/1
is called
with a list consisting of a single item or with an empty list.
In the next example, we consume only one list item in each recursive
step. We still compute the sum of consecutive items, but this time the
list items overlap. For the list [1, 2, 3, 4, 5]
, we expect the
result to be [3, 5, 7, 9]
.
defmodule ChunkOverlapping do
def add_overlapping([e1, e2 | rest]) do
[e1 + e2 | add_overlapping([e2 | rest])]
end
def add_overlapping([_]) do
[]
end
def add_overlapping([]) do
[]
end
def run do
# prints [3, 5, 7, 9]
IO.inspect(ChunkOverlapping.add_overlapping([1, 2, 3, 4, 5]))
end
end
Again, we take the first two items of the given list naming them e1
and e2
in each recusive step.
The difference between the implementations of add_consecutive/1
and
add_overlapping/1
is that in add_overlapping/1
, we put e2
back
into the list we are processing. Instead of
[e1 + e2 | add_consecutive(rest)]
we write
[e1 + e2 | add_overlapping([e2 | rest])]
In hindsight, the implementation of add_overlapping/1
looks more or
less obvious. However, (at least) two ideas must be present before
being able to write add_overlapping/1
in the way shown above:
-
we can use pattern matching to take more than one item from the head of a given list and
-
we can put items back into the list before running the next recursive step.
The two little problems solved above using add_consecutive/1
and
add_overlapping/1
can be split in two smaller problems. The first
problem is to create a list of consecutive or overlapping pairs. And
the second problem is to add the numbers in the pairs. For both
problems, Elixir provides solutions out of the box and the solutions
of our little problems become a matter of function composition.
defmodule ChunkUsingEnum do
def sum_consecutive(list) do
Enum.chunk_every(list, 2, 2, :discard)
|> Enum.map(fn [fst, lst] -> fst + lst end)
end
def sum_overlapping(list) do
Enum.chunk_every(list, 2, 1, :discard)
|> Enum.map(fn [fst, lst] -> fst + lst end)
end
def run_sum_consecutive do
# prints [3, 7]
IO.inspect(ChunkUsingEnum.sum_consecutive([1, 2, 3, 4, 5]))
end
def run_sum_overlapping do
# prints [3, 5, 7, 9]
IO.inspect(ChunkUsingEnum.sum_overlapping([1, 2, 3, 4, 5]))
end
end
The documentation of the Elixir Enum
module explains how exactly
Enum.chunk_every/4
and Enum.map/2
operate on lists.