DEV Community

Cover image for Lists and tuples in Elixir
Rhaenyra
Rhaenyra

Posted on • Edited on

Lists and tuples in Elixir

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
Enter fullscreen mode Exit fullscreen mode

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 the left operand is not a proper list, it raises an ArgumentError.

  • 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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 in tuple. Raises ArgumentError 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-based index in tuple.

  • 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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

What is the difference between lists and tuples?

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]
Enter fullscreen mode Exit fullscreen mode
#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}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
aminmansuri profile image
hidden_dude

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.

Collapse
 
rhaenyraliang profile image
Rhaenyra • Edited

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!

Collapse
 
aminmansuri profile image
hidden_dude

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.

Collapse
 
snowyang profile image
Snow Yáng

Keep doing it!

Collapse
 
rhaenyraliang profile image
Rhaenyra

Thx!

Collapse
 
rhaenyraliang profile image
Rhaenyra • Edited

This is the third article in my Elixir journal series.
Everyone is welcome to learn with me. 😊