We will now describe Elixir's data types, which extend upon Erlang's data types. Elixir is a dynamic programming language. Consequently, you don't declare the type of each variable—it depends on the value it holds at each moment.
To improve the learning experience, we'll be providing some examples along the way. For now, we'll just use Elixir's REPL, IEx (short for Interactive Elixir). To start an IEx session, you must have Elixir installed on your machine. Elixir has an official page with instructions on how to do this if you don't have it installed, whether using package managers, the precompiled version, or compiling from the source yourself:
http://elixir-lang.github.io/install.html
Note
We will not dive into the memory usage of each type in Elixir. If you're curious about this, the official Erlang documentation contains detailed information: http://erlang.org/doc/efficiency_guide/advanced.html.
Provided that you have Elixir already installed on your machine, type iex
on your terminal to start a new IEx session. With this, you can run the examples present in this chapter in your machine. Note that your default iex
prompt contains a number in between parenthesis, which represents the number of expressions you've entered in the current session, such as iex>(1)
. To declutter the output, in our examples, we've removed this number.
We'll be exploring IEx in greater detail toward the end of this chapter, in the Tooling and ecosystems section. Throughout the following subsections, we'll be mentioning some built-in modules in Elixir. We'll explore what modules are in the Functions and modules section—for now, it's enough to know that a module is a collection of functions.
Note
Whenever you're done with your IEx session, you can either press Ctrl + C twice or Ctrl + \. This will kill the operating system process that's running the Erlang runtime (along with all background jobs). Alternatively, you can stop the system in a more polite way by entering System.halt
in the shell.
This type contains, as you would expect, numbers that can be written without a fractional component. The size of integers adjusts dynamically according to its magnitude—you don't have to worry about this: an integer will simply occupy more words in memory as it grows. Here's some basic arithmetic with integers:
iex> 25 + 8 33
To improve the readability of the code, you can also use underscores in between the digits of an integer, as shown here:
iex> 1_000_000 - 500_000 500000
Besides decimal, Elixir also supports integers written in binary, octal, and hexadecimal (using 0b
, 0o
, and 0x
, respectively):
iex> 0b10001 17 iex> 0o21 17 iex> 0x11 17
In Elixir, floats are written with a decimal point, with digits before and after it, meaning that .1
is not a valid float in Elixir (as it is, for instance, in JavaScript). In Elixir, you have to be explicit and write the leading 0
—so in this case, you'd write 0.1
. Here's an example of the multiplication of two floats:
iex> 0.1 * 0.5 0.05
You can also write floats using the exponent notation, as shown:
iex> 0.1e3 100.0
Floats are represented in IEEE 754 double precision, which yields between 15 to 17 significant decimal digits. As usual, you should take care when comparing floats for equality.
Note
Beware that the division operator (/
) always returns a float, even if the result of the division could be an integer:iex> 4/2
2.0
If you want to circumvent this behavior, use the auto-imported div
function from the Kernel
module. Also, if you want to get the remainder of a division, use the rem
function.
Atoms are a constant, whose value is its own name. They are always prefixed with a leading colon (:
), followed by alphanumeric characters (and possibly _
or @
). They may terminate with an exclamation or a question mark. Atoms are similar to enumerations in C and symbols in Ruby. Here are some examples of atoms:
iex> :ok :ok iex> :error :error iex> :some_descriptive_name! :some_descriptive_name! iex> :value@start :value@start
You can create atoms with arbitrary characters with the following syntax:
iex> :"Atom name with arbitrary characters#$%^" :"Atom name with arbitrary characters#$%^"
As with all data structures in Elixir, atoms can't be modified after their creation. Furthermore, they are not garbage-collected. Atoms are kept in the atom table, and upon compilation, their value is replaced by a reference to their entry on this table. This makes comparing atoms very efficient. As you'll learn throughout this book, this is one of the major use cases for atoms in Elixir, as we are constantly matching the return of a function against a certain expected atom.
Note
Since atoms are not garbage collected, don't create atoms dynamically from sources you can't control, as you can very easily use up all of the space allocated for the atom table. For instance, if you're parsing a JSON response and creating a map out of it, don't use atoms for its keys—use strings instead (both of these types, maps and strings, will be described later in this chapter).
Elixir has three values related to Boolean operations: true
, false
, and nil
(where nil
represents the absence of value—similar to null
in most other languages). However, those are just some syntatic sugar, as internally they are represented as atoms of the same name, as you can see in the following example:
iex> true == :true true iex> false == :false true iex> nil == :nil true
You have the common Boolean operators, or
, and
, and not
:
iex> true or false true iex> true and false false iex> not false true
However, these operators are type-strict in their first argument: they only accept true
or false
. If you pass anything else as an argument, you'll get BadBooleanError
.
This is where the concept of truthiness and falseness enters. Similar to what happens in Ruby or C, false
and nil
are treated as falsey values, and everything else is considered to be truthy. The operators that work with falsey and truthy values are &&
(and), ||
(or), and !
(not):
iex> "a value" || false "a value" iex> "a value" && false false iex> nil && "a value" nil iex> !"a value" false
Notice how these operators short circuit depending on the arguments. With ||
, it returns the first value that's truthy, whereas with &&
, it returns the first falsey value (in both cases, in the event those conditions never happen, they return the last value).
You also have the other normal comparison operators, such as greater than (>
) and inequality (!=
)—you can find the full list at https://hexdocs.pm/elixir/operators.html. The one that's worth pointing out is the strict equality operator, which, besides comparing values, compares types:
iex> 3 == 3.0 true iex> 3 === 3.0 false
Tuples are used to group a fixed number of elements together. They can hold any value—even other tuples. They are stored contiguously in memory, which provides constant access time to elements inside a tuple. You create a tuple surrounding the elements with curly brackes ({
and }
), and separate the elements with commas:
iex> {:ok, 3.14} {:ok, 3.14}
A common usage of tuples in Elixir is to pattern-match on the result of a function to ensure its success (usually with an :ok
atom) or deal with an error. We will be looking to pattern matching and functions later in this chapter.
To access an element inside a tuple, we use the elem
function (from the Kernel
module), providing the tuple and a zero-based index:
iex> result = {:ok, 3.14} {:ok, 3.14} iex> elem(result, 1) 3.14
Note
Functions from the Kernel
module are auto-imported. Thus, we don't need to prefix them with the module name.
To change the elements on a tuple, you can use the put_elem
function. The arguments are similar to the elem
function, but you also provide the new value for that position of the tuple:
iex> put_elem(result, 1, 1.61) {:ok, 1.61} iex> result {:ok, 3.14}
Notice how the result
variable hasn't changed. As we discussed in the beginning of this chapter, data in Elixir is immutable. As such, although we've updated the tuple with a new value, the original tuple hasn't changed—Elixir updated the value on a copy of the original tuple. This way our code is side-effect free, and any other function holding a reference to the result
variable won't have any surprises.
The general recommendation in Elixir is that tuples should hold up to four elements—anything more than that and you probably should be using another type.
Lists are created by wrapping the elements we want inside it with square brackets ([
and ]
), separating the values with commas. Internally, lists are implemented as singly linked lists, meaning that accessing the elements of a list is a O(n)
operation. Lists aren't stored contiguously in memory as arrays in other languages. As with tuples, list elements can be of any type:
iex> [1, :an_atom, 0.5] [1, :an_atom, 0.5]
We have the ++
and --
operators that are exclusive to lists, and serve to concatenate and subtract lists, respectively:
iex> [0, 1, 1] ++ [2, 3, 5] [0, 1, 1, 2, 3, 5] iex> [0, 1, 1] -- [1, 2, 3] [0, 1]
To check whether a certain element is present in a list, you can use the in
operator:
iex> 1 in [0, 1, 1, 2, 3, 5] true iex> 99 in [0, 1, 1, 2, 3, 5] false
To get the head of a list, we use the hd
function, whereas to get the tail of a list, we use the tl
function:
iex> hd([0, 1, 1, 2, 3, 5]) 0 iex> tl([0, 1, 1, 2, 3, 5]) [1, 1, 2, 3, 5]
Notice that the semantic of tail here is the list without its head (which is also a list), and not the last element of a list. We'll be exploring this concept in more depth, along with some more examples on how to work with lists, in the Working with collections section. For reference, you can find a detailed list of operations you can make on lists at https://hexdocs.pm/elixir/List.html.
Maps are key-value data structures, where both the key and the value can be of any type. They're similar to hashes in Ruby and dictionaries in Python. To create a map, you enclose your key-value pairs in %{}
, and put a =>
between the key and the value, as we can see in the following snippet:
iex> %{:name => "Gabriel", :age => 1} %{age: 1, name: "Gabriel"}
In this case, the keys are both of the same type, but this isn't required. If your keys are atoms, you can use the following syntax to make the map declaration simpler:
iex> %{name: "Gabriel", age: 1} %{age: 1, name: "Gabriel"}
To access the value associated with a certain key, put the key inside square brackets in front of the map:
iex> map = %{name: "Gabriel", age: 1} %{age: 1, name: "Gabriel"} iex> map[:name] "Gabriel"
As with the map declaration, when the key is an atom, we have some syntatic sugar on top of it:
iex> map.name "Gabriel"
Note
When you try to fetch a key that doesn't exist in the map, a KeyError
error will be raised when using the map.key
syntax–unlike the map[key]
syntax, which will return nil
.
To update a key in a map, you can use %{map | key => new_value}
. If the key is an atom, we can use the same notation described previously:
iex> %{map | age: 2} %{age: 2, name: "Gabriel"}
Note
If you're coming from an object-oriented programming background, you may instinctively use the following syntax to change the value of a key: map[key] = new_value
. Remember that in Elixir all types are immutable and you never operate on the data structure itself but always on a copy of it.
This will only work for keys that already exist in the map—this constraint allows Elixir to optimize and reuse the fields list when updating a map. If you want to insert a new key, use the put
function from the Map
module:
iex> Map.put(map, :gender, "Male") %{age: 1, gender: "Male", name: "Gabriel"}
As with all other types, in the official documentation, at https://hexdocs.pm/elixir/Map.html, you can find a pretty detailed reference on what you can do with maps.
A binary is group of consecutive bytes. You create them by surrounding the byte sequence with <<
and >>
. Here we are creating a two-byte binary:
iex> <<5, 10>> <<5, 10>>
In the decimal base, a byte can only contain values up to 255 (otherwise it overflows). If we want to store values greater that 255, we need to tell the runtime to use more space to store this binary:
iex> <<5, 256>> <<5, 0>> iex> <<5, 256::16>> <<5, 1, 0>>
As you can see, when we specify the size (16 bits in this case) we can see that the output as an extra byte and the overflow didn't occur. The size doesn't have to be a multiple of 8
. In that case, a binary is usually called a bitstring.
Most programmers will not handle data at such a low level, so your use of binaries may not be that frequent. However, they're extremely useful in certain scenarios, such as processing the header of a file to find a magic number and identify the file type, or even when dealing with network packets by hand.
Strings are binaries with UTF-8 codepoints in them. You create a string with the usual double-quote syntax:
iex> "hey, a string" "hey, a string"
Charlists are, as the name implies, lists of character codes. You create them using the single-quote syntax:
iex> 'hey, a charlist' 'hey, a charlist'
Since this is just a list, you can use the hd
function to get the code for the first character:
iex> hd('hey, a charlist') 104
Note
You can find out the code of a certain character with the ?
operator. For instance, to find out the character code of a lowercase d
, you'd use ?d
.
Both representations support string interpolation:
iex> "two plus two is: #{2+2}" "two plus two is: 4" iex> 'four minus one is: #{4-1}' 'four minus one is: 3'
Both representations also support the heredoc
notation, which is most commonly used to write documentation. To create it, use three single or double quotes:
iex> """ ...> a string with heredoc notation ...> """ "a string with heredoc notation\n" iex> ''' ...> a charlist with heredoc notation ...> ''' 'a charlist with heredoc notation\n'
Elixir provides sigils as another syntax to declare strings or charlists, which can be handy if you want to include quotes inside your string. You can use ~s
to create a string and ~c
to create a charlist (their uppercase versions, ~S
and ~C
, are similar but don't interpolate or escape characters):
iex> ~s(a string created by a sigil) "a string created by a sigil" iex> ~c(a charlist created by a sigil) 'a charlist created by a sigil'
There's another sigil that's worth mentioning: ~r
, which is used for regular expressions. In the next snippet, we're using the run
function from the Regex
module to exemplify the usage of the ~r
sigil:
iex> Regex.run(~r{str}, "a string") ["str"] iex> Regex.run(~r{123}, "a string") nil
You can find the list of supported sigils (and also how to create your own!) at http://elixir-lang.github.io/getting-started/sigils.html.
The convention in the Elixir community is to only use the term string when referring to the double-quote format. This distinction is important, since their implementation is very different. Functions from the String
module will only work on the double-quote format. You should always use the double-quote format, unless you're required to use a charlist—which is the case, for instance, when you're using Erlang libraries. You can use the following functions to convert between the two formats:
iex> String.to_charlist("converting to charlist") 'converting to charlist' iex> List.to_string('converting to string') "converting to string"
We'll now succinctly describe some other types. We'll begin with the types that build upon some types we've already described: keyword lists, ranges, mapsets, and IO lists.
A keyword list is a list in which its elements have a specific format: they are tuples where the first element is an atom (the second element can be of any type), as demonstrated in the following example:
iex> [name: "Gabriel", age: 1] = [{:name, "Gabriel"}, {:age, 1}] [name: "Gabriel", age: 1]
We can create keyword lists using the following syntax:
iex> keyword_list = [name: "Gabriel", age: 1] [name: "Gabriel", age: 1] iex> keyword_list[:name] "Gabriel"
As you can see from the previous snippet, a keyword list is indeed a list of tuples, with an atom; you can access values in a keyword list using the same syntax as you would in maps. As an alternative, you can use the get
function from the Keyword
module. Note that this way of declaring a keyword list is just syntatic sugar, as internally this still is a list of tuples–which means that searching for an item in a keyword list is O(n)
, and not O(1)
as in maps.
In a keyword list, contrary to what happens in maps, you can have more than one value for a given key. Also, you can control the order of its elements. Usually, keyword lists are used to allow functions to receive an arbitrary number of optional arguments. We'll be showing an example of this when we look at named functions later in this chapter. You can find all the operations you can do on a keyword list at https://hexdocs.pm/elixir/Keyword.html.
Ranges, again, similar to what happens in Ruby, represent an interval between two integers. To create a range, we use this:
iex> 17..21 17..21 iex> 19 in 17..21 true
Similar to what we do with a list, we can use the in
operator to check whether a number is between the start and the end of a range.
If you're looking for an implementation of a set in Elixir, you're looking for MapSet
. You create and manipulate them with the functions from the MapSet
module. Here are some examples:
iex> set = MapSet.new #MapSet<[]> iex> set = MapSet.put(set, 1) #MapSet<[1]> iex> set = MapSet.put(set, 2) #MapSet<[1, 2]> iex> set = MapSet.put(set, 1) #MapSet<[1, 2]>
Sets, by definition, can't contain duplicates. So, inserting a value that's already there has no effect. You can find the documentation for the MapSet
module at https://hexdocs.pm/elixir/MapSet.html.
There are three types, related to the underlying Erlang VM, that we have to mention before closing this section. They are as following:
- Reference: A reference is a type created by the
Kernel.make_ref
function. This functions creates an almost-unique reference, which gets repeated around every 282 calls. We will not use references in this book. - Port: A port is a reference to a resource. The Erlang VM uses it to interact with external resources, such as an operating system process. We will talk a bit more about ports later in this chapter, when we discuss the interoperability between Elixir and Erlang.
- PID: A PID is the type used to identify processes in the Erlang VM. You'll see PIDs in action later in this book, when we start working with Erlang VM processes.
To complete this section, there's a function that we want to highlight: the i
function, which is auto-imported from the Kernel
module. You can use it to find out more information about a data type. It will print information about the data type of the term you pass as an argument. Here is an example with a string:
iex> i("a string") Term "a string" Data type BitString Byte size 8 Description This is a string: a UTF-8 encoded binary. It's printed surrounded by "double quotes" because all UTF-8 encoded codepoints in it are printable. Raw representation <<97, 32, 115, 116, 114, 105, 110, 103>> Reference modules String, :binary Implemented protocols IEx.Info, Collectable, List.Chars, String.Chars, Inspect
And, with this, we've finished our tour of the data types in Elixir! We didn't go into much detail, but with the links we left throughout this section, you'll see how incredible the documentation in Elixir is, and how easy it is to figure out what a certain function does or the purpose of a certain argument.
We will now jump into one of the most prominent features of Elixir (and also one that will definitely change how you write programs): pattern matching.