We will keep learning two of the most used collection data types in Elixir: lists and tuples. They are immutable!
Like lists, tuples can hold any value. In Elixir data structure they are immutable, so every operation will return a new List or a new Touple.
(Link)List
Elixir uses square brackets to specify a list of values. List operators never modify the existing list. You can freely pass the data around with the guarantee that no one will mutate it in memory—only transform it.
Linked lists hold zero, one, or more elements in the chosen order.
iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> [1, "two", 3, :four]
[1, "two", 3, :four]
iex> [1, 3, "two", :four]
[1, 3, "two", :four]
iex> length([1, 2, 3])
3
List concatenated
Two lists can be concatenated or subtracted using the ++/2
and --/2
operators.
-
left ++ right
Concatenates a proper list and a term, returning a list. If the
right
operand is not a proper list, it returns an improper list. If theleft
operand is not a proper list, it raises anArgumentError
. -
left -- right
List subtraction operator. Removes the first occurrence of each element in the left list for each element in the right list.
iex>[1] ++ [2,3]
[1, 2, 3]
iex> [1, 2, 3] ++ [4, 6, 6]
[1, 2, 3, 4, 5, 6]
iex>[1, 2] ++ 3
[1, 2 | 3]
iex> [1, 2] ++ {3, 4}
[1, 2 | {3, 4}]
iex> [1, 2, 3] -- [1, 2]
[3]
iex> [1, 2, 3, 2, 1] -- [1, 2, 2]
[3, 1]
iex> [1, 2, 3] -- [2] -- [3]
[1, 3]
iex> [1, 2, 3] -- ([2] -- [3])
[1, 3]
iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]
List prepended
An element can be prepended to a list using |
. Due to their cons cell-based representation, prepending an element to a list is always fast (constant time), while appending becomes slower as the list grows in size (linear time):
iex(1)> new = 0
iex(2)> list = [1, 2, 3]
iex(3)> [new | list]
[0, 1, 2, 3]
iex(1)>list = [1, 2, 3]
iex(2)[0 | list] # fast
[0, 1, 2, 3]
iex(3)list ++ [4] # slow
[1, 2, 3, 4]
Head and tail of list
Lists in Elixir are effectively linked lists, which means they are internally represented in pairs containing the head and the tail of a list.
iex> [head | tail] = [1, 2, 3]
iex> head
1
iex> tail
[2, 3]
The head is the first element of a list and the tail is the remainder of the list. They can be retrieved with the functions hd/1
and tl/1
-
hd(list)
Returns the head of a list. Raises
ArgumentError
if the list is empty. -
tl(list)
Returns the tail of a list, which is the list without its first element. Raises
ArgumentError
if the list is empty.
iex> hd([1, 2, 3, 4])
1
iex> hd([1 | 2])
1
iex> hd([])
** (ArgumentError) argument error
iex> tl([1, 2, 3, :go])
[2, 3, :go]
iex>tl([:a, :b | :improper_end])
[:b | :improper_end]
iex>tl([:a | %{b: 1}])
%{b: 1}
iex>tl([:one])
[]
iex>tl([])
** (ArgumentError) argument error
Tuples
Elixir uses curly brackets to define tuples. Tuples store elements contiguously in memory, which means accessing a tuple element by index or getting the tuple size is a fast operation. Indexes start from zero.
iex> {}
{}
iex> {:ok, "hello"}
{:ok, "hello"}
iex> {1, :two, "three"}
{1, :two, "three"}
iex> tuple_size({:ok, "hello"})
2
iex(1)> tuple = {:ok, 1, "hello"}
{:ok, 1, "hello"}
iex(2)> tuple_size(tuple)
3
Functions for working
Accessing a tuple element with elem/2
, put_elem/3
, and tuple_size/1
.
-
elem/2(tuple, index)
Gets the element at the zero-based
index
intuple
. RaisesArgumentError
when the index is negative or out of range of the tuple elements. -
put_elem/3(tuple, index, value)
Puts
value
at the given zero-basedindex
intuple
. -
tuple_size/1(tuple)
Returns the size of a tuple, which is the number of elements in the tuple.
iex(1)> tuple = {:ok, 1, "hello"}
{:ok, 1, "hello"}
iex(2)> elem(tuple, 1)
1
iex> elem({:ok, "hello", 1}, 1)
"helo"
iex> elem({}, 0)
** (ArgumentError) argument error
iex> elem({:foo, :bar}, 2)
** (ArgumentError) argument error
iex> put_elem({:foo, :ok, 3}, 2, 4)
{:foo, :ok, 4}
iex(1)> tuple = {:foo, :bar, 3}
iex(2)> put_elem(tuple, 0, :baz)
{:baz, :bar, 3}
iex(1)> tuple = {:ok, 1, "hello"}
{:ok, 1, "hello"}
iex(2)> tuple_size(tuple)
3
File.read/1
The File module is an interface module that provides an interface to the file system. The read(path)
function returns a tuple with the atom :ok
as the first element and the file contents as the second. Elixir allows us to pattern match on tagged tuples and effortlessly handle both success and failure cases.
iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}
What is the difference between lists and tuples?
iex > list = [1, 2, 3]
[1, 2, 3]
# This is fast as we only need to traverse `[0]` to prepend to `list`
iex > [0] ++ list [0, 1, 2, 3]
# This is slow as we need to traverse `list` to append 4
iex > list ++ [4] [1, 2, 3, 4]
#When you update a tuple, all entries are shared between the old and the new tuple, except for the entry that has been replaced
iex >tuple = {:a, :b, :c, :d}
{:a, :b, :c, :d}
iex >put_elem(tuple, 2, :e)
{:a, :b, :e, :d}
Final Thoughts
This wraps up today's introduction to collection data types in Elixir: Lists and Tuples. If you haven't read the previous article, Basic types in Elixir, be sure to check it out. Our next article will delve into Pattern matching. Make sure not to miss it. I hope this article helps you. See you next time, bye!
Top comments (6)
Frankly I think it's very lame that Elixir uses Linked Lists to implement Lists. There are so many better implementations these days. Falling back to linked lists is very memory wasteful, and very cache unconscious, as well as slow because it requires a memory dereference each time you go to the next item.
Array implementations are far superior for 99% of all applications.
Thank you for your feedback.
Article
This article is great for explaining why Elixir uses Linked Lists instead of Array. I hope it can help you to understand their choice!
I'm not convinced that immutability is a sufficiently good reason.
There's a data structure used in parallel processing that basically uses 2 arrays. One for the values, and the other for the indices. So it is possible to implement linked lists with contiguous memory.
But I guess fragmented linked lists was the easy way.
Keep doing it!
Thx!
This is the third article in my Elixir journal series.
Everyone is welcome to learn with me. 😊