Elixir’s “|” operator

Paweł Dawczak
3 min readJun 16, 2016

--

Recently I’ve seen the question on one of LinkedIn groups asking about explanation for vertical bar operator, so I decided to write a post about it.

If you have any experience with Elixir, you’re probably aware that you can create lists:

list = [1, 2, 3]

Pretty straightforward! However, this is just simpler notation of something like:

iex> [1 | [2 | [3 | []]]]
[1, 2, 3]

Oh wow! This looks strange! But this is just an evidence of what list’s elements really are — basically, they are recursive structures of (head, tail) pairs.

To see if this is true, lets use hd/1, to read the head of the list:

hd(list)
# => 1

So far — looks correct. How about reading the tail with tl/1?

tl(list)
# => [2, 3]

Something interesting happened here — even if I hd/1 earlier from the list, the list itself didn’t change (because the data in elixir is immutable), this is why I was able to tl/1 from the same list and receive two-element list.

Usually this is not the way you interact with lists. More often what you want to do, is to destructuring them with pattern matching. So:

[a_head | and_tail] = list
IO.inspect(a_head) # => 1
IO.inspect(and_tail) # => [2, 3]

We have achieved similar effect with slightly more descriptive way. But with pattern matching you can go even further! Please see next example:

[a, b | tail] = list
IO.inspect(a) # => 1
IO.inspect(b) # => 2
IO.inspect(tail) # => [3]

This example translates to something like ok, please give me two elements (a and b) from the list, and the rest (tail) of it. This is interesting! It becomes even more powerful when it comes to defining your functions. Imagine something like:

So:

First process/1 definition will match, if the list passed contains at least two elements, and the two first elements are the same.

Second definition will match if the passed list contains at least two elements and the second element is equal 2.

The third definition will match anything else.

Ok, this is about reading lists, but could the |operator be used for list manipulation? Certainly!

To prepend to the list, we could do:

[123 | list]
# => [123, 1, 2, 3]

How about appending? First guess to use the operator would be like:

[list | 4]

However, what happens is probably not quite what you would expect, because it turns to be:

# => [[1, 2, 3], 4]

This proofs that the | indicates head-tail relation of specified elements, thus, the list became a head, and 4 is a tail (there is limitation of what list elements can be, thus another list is perfectly valid choice), but this doesn’t mean, the head should be a single element (remember destrucuring list extracting two first elements?):

[3, 2, 1 | list]
# => [3, 2, 1, 1, 2, 3]

The reason for such a behaviour is because prepending to the list is very fast operation, comparing to appending to the list (which would require traversing whole list). If, however, you would like to append to a list, you should turn your attention to ++/2 for concatenation:

list ++ [4, 5]
# => [1, 2, 3, 4, 5]

To finish the discussion regarding the | operator and Lists, it’s worth mentioning possible problems which will be illustrated by code examples:

hd([])
** (ArgumentError) argument error
tl([])
** (ArgumentError) argument error
[head | tail] = []
** (MatchError) no match of right hand side value: []
[a, b | tail] = [1]
** (MatchError) no match of right hand side value: [1]
[a, a | tail] = [1, 2]
** (MatchError) no match of right hand side value: [1, 2]

The | outside the world of Lists

It turns, that the | operator is not limited to be used with Lists only, it might be used for manipulating Maps and Structs:

Previously, we were able to build Lists with | operator (using it to prepend elements). This is not the case for Maps — if you try to add new key to the already existing map, it won’t work:

user_map = %{user_map | occupation: “Developer”}
** (KeyError) key :occupation not found in: %{username: “PDAWCZAK”}
(stdlib) :maps.update(:occupation, “Developer”, %{username: “PDAWCZAK”})
(stdlib) erl_eval.erl:255: anonymous fn/2 in :erl_eval.expr/5
(stdlib) lists.erl:1262: :lists.foldl/3

or Struct:

user_str = %{user_str | occupation: “Developer”}
** (KeyError) key :occupation not found in: %User{username: “pdawczak”}
(stdlib) :maps.update(:occupation, “Developer”, %User{username: “PDAWCZAK”})
(stdlib) erl_eval.erl:255: anonymous fn/2 in :erl_eval.expr/5
(stdlib) lists.erl:1262: :lists.foldl/3

In this case, you should turn your attention to Map#put/3:

--

--