2 Data Types
2.2 Tuples
Elixir tuples are like arrays in other languages, storing elements in contiguous memoery. This means accessing a tuple element by index or getting the tuple size is a fast operation. Indexes start from zero:
> {:ok, "hello"}
iex#> {:ok, "hello"}
> tuple_size {:ok, "hello"}
iex#> 2
It is also possible to put an element at a particular index in a tuple with put_elem/3
:
> tuple = {:ok, "hello"}
iex#> {:ok, "hello"}
> put_elem(tuple, 1, "world")
iex#> {:ok, "world"}
> tuple
iex#> {:ok, "hello"}
Notice that put_elem/3
returned a new tuple. The original tuple stored in the tuple variable was not modified. Like lists, tuples are also immutable. Every operation on a tuple returns a new tuple, it never changes the given one.
2.3 (Linked) List
Elixir uses square brackets to specify a list of values. Values can be of any type:
> [1, 2, true, 3]
iex#> [1, 2, true, 3]
> length [1, 2, 3]
iex#> 3
Lists are stored in memory as linked lists, meaning that each element in a list holds its value and points to the following element until the end of the list is reached. This means accessing the length of a list is a linear operation: we need to traverse the whole list in order to figure out its size.
Two lists can be concatenated or subtracted using the ++/2
and --/2
operators respectively:
> [1, 2, 3] ++ [4, 5, 6]
iex#> [1, 2, 3, 4, 5, 6]
> [1, true, 2, false, 3, true] -- [true, false]
iex#> [1, 2, 3, true]
List operators never modify the existing list. Concatenating to or removing elements from a list returns a new list. We say that Elixir data structures are immutable. One advantage of immutability is that it leads to clearer code. You can freely pass the data around with the guarantee no one will mutate it in memory - only transform it.
The head of a list 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
. Let’s assign a list to a variable and retrieve its head and tail:
> list = [1, 2, 3]
iex> hd(list)
iex#> 1
> tl(list)
iex#> [2, 3]
2.3.1 Comparing Lists and Tuples
Note that lists in Elixir are actually linked lists, while tuples are “arrays” or “list” in other languages with contiguous elements.
One very common use case for tuples is to use them to return extra information from a function. For example, File.read/1
is a function that can be used to read file contents. It returns a tuple:
> File.read("path/to/existing/file")
iex#> {:ok, "... contents ..."}
> File.read("path/to/unknown/file")
iex#> {:error, :enoent}
Most of the time, Elixir is going to guide you to do the right thing. For example, there is an elem/2
function to access a tuple item but there is no built-in equivalent for lists:
> tuple = {:ok, "hello"}
iex#> {:ok, "hello"}
> elem(tuple, 1)
iex#> "hello"
When counting the elements in a data structure, Elixir also abides by a simple rule: the function is named size if the operation is in constant time (i.e. the value is pre-calculated) or length if the operation is linear (i.e. calculating the length gets slower as the input grows). As a mnemonic, both “length” and “linear” start with “l”.
For example, we have used 4 counting functions so far: byte_size/1
(for the number of bytes in a string), tuple_size/1
(for tuple size), length/1
(for list length) and String.length/1
(for the number of graphemes in a string). We use byte_size to get the number of bytes in a string – a cheap operation. Retrieving the number of Unicode graphemes, on the other hand, uses String.length
, and may be expensive as it relies on a traversal of the entire string.
2.4 Map
Whenever you need a key-value store, maps are the “go to” data structure in Elixir. A map is created using the %{} syntax:
> map = %{:a => 1, 2 => :b}
iex#> %{2 => :b, :a => 1}
> map[:a]
iex#> 1
> map[2]
iex#> :b
> map[:c]
iex#> nil
Compared to keyword lists, we can already see two differences:
Maps allow any value as a key.
Maps’ keys do not follow any ordering.
Maps (as well as structs) have a shortcut syntax for updating a key’s value:
= %{a: 1, b: 2}
map | a: 3}
%{map #> %{a: 3, b: 2}
2.4.1 Map Utilities
get a key’s value Map.get
Map.get(map, key, default_value)
puts the given value under key in map (an alternative to the |
syntax) Map.put
Map.put(map, key, value)
Updates the key in map with the given function. Map.update
Map.update(map, key, default, fun)
If key is present in map then the existing value is passed to fun
and its
result is used as the updated value of key. If key is not present in map,
default
is inserted as the value of key. The default value will not be passed
through the update function.
List all keys Map.keys
Map.keys(%{name: "qiushi", age: 18})
#> [:name, :age]
List all values Map.values
Map.values(%{name: "qiushi", age: 18})
Check if a key is in the map Map.has_key?
Map.has_key?(%{name: "qiushi", age: 18}, :hobby)
#> false
2.5 Keyword List
Keyword lists are lists of 2-item tuples as the representation of a key-value data structure.
:name, "qiushi"}, {:age, 1}] [{
Elixir supports a special syntax for defining such lists: [key: value]
. Underneath it maps to the same list of tuples as above.
name: "qiushi", age: 1] # shortcut for [{:name, "qiushi"}, {:age, 1}] [
e can use ++ to add new values to a keyword list:
= [{:a, 1}, {:b, 2}]
list ++ [c: 3]
list #> [a: 1, b: 2, c: 3]
a: 0] ++ list
[#> [a: 0, a: 1, b: 2]
Many useful functions are available in the Keyword
module (https://hexdocs.pm/ elixir/Keyword.html). For example, you can use the bracket operator []
or Keyword.get/2
to fetch the value for a key
= [monday: 1, tuesday: 2, wednesday: 3]
days
:monday]
days[#> 1
Keyword.get(days, :monday)
#> 1
Keyword.get(days, :noday)
#> nil
Don’t let that fool you, though. Because you’re dealing with a list, the complexity of a lookup operation is O(n).
Keyword lists are most often useful for allowing clients to pass an arbitrary number of
optional arguments. For example, the result of the function IO.inspect
, which prints
a string representation of a term to the console, can be controlled by providing addi-
tional options through a keyword list:
IO.inspect([100, 200, 300]) # Default behavior
#> [100, 200, 300]
IO.inspect([100, 200, 300], [width: 3]) # passing a keyword list as options
#> [100,
#> 200,
#> 300]
In fact, this pattern is so frequent that Elixir allows you to omit the square brackets if the last argument is a keyword list:
IO.inspect([100, 200, 300], width: 3, limit: 1)
# pass the second argument as [width: 3, limit: 1]
# not having a third argument
Keyword lists are important because they have three special characteristics:
Keys must be atoms.
Keys are ordered, as specified by the developer.
Keys can be given more than once
For example, keyword lists are useful in database queries because we can have the same key in different positions.
.find_user([where: age > 18, where: subscribed == true]) db
If we are using a map instead, we cannot have where
twice.