Elixir Introduction
This chapter will teach you the absolute basics of Elixir, just enough for you to become productive with Phoenix.
Buckle up. It is going to be a bumpy and sometimes dull ride. It’s tough to teach all the needed Elixir knowledge in just one chapter.
Elixir version
All code examples are written and tested for Elixir version 1.10.2. Please make sure that you have that or a higher version installed.
|
Elixir’s Interactive Shell (iex)
Your Elixir installation comes with Elixir’s Interactive Shell (iex
), which
we will use for most of the examples in this chapter. Please go to your command
line and fire it up:
$ iex
Erlang/OTP 22 [erts-10.6.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Interactive Elixir (1.10.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> (1)
1 | This is your iex prompt. |
You have to press CTRL-C twice (or CTRL-\ once) to exit iex .
|
The iex
will be your trusted friend during the work with this book and later
while working with Phoenix. While programming in development mode, you can use
it for diving into the core of your Phoenix application. You can do so too while
being in production mode but that is the equivalent to open-heart surgery. It
can be a lifesaver, but you need to know what you are doing.
iex offers autocomplete when possible. So when in doubt press TAB .
|
iex offers a history too. To access the last command, just press on the arrow-up key. |
Help in the iex shell
iex has a built-in help function h/1
which gives you access to some
basic documentation:
iex(2)> h length/1
def length(list)
@spec length(list()) :: non_neg_integer()
guard: true
Returns the length of the list.
Allowed in guard tests. Inlined by the compiler.
# Examples
iex> length([1, 2, 3, 4, 5, 6, 7, 8, 9])
9
Hello world!
The classic! But never the less very important. You can use the function
IO.puts()
to print a string to standard output:
iex(1)> IO.puts("Hello world!")
Hello world!
:ok
You should always enclose strings within double quotes. If you use single quotes, that creates a charlist, which is a different type.
In case there are double quotes within a string you have to escape them with backslashes:
iex(2)> IO.puts("With double quotes: \"Hello world!\"")
With double quotes: "Hello world!"
:ok
iex(3)>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
^C(1)
1 | Don’t be afraid of the BREAK menu. With the first Ctrl+C the iex
displays this list of choices (the BREAK menu) and with the second Ctrl+C
you end the iex session.
|
Basic Calculations
We can use the types integer
(integer numbers) and float
(real numbers) to
do all sorts of calculations. We can use the usual operators (+, -, etc.). Here
are a few examples:
$ iex
Erlang/OTP 22 [erts-10.6.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Interactive Elixir (1.10.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 1 + 1
2
iex(2)> 1.1 + 1
2.1
iex(3)> 2 - 1
1
iex(4)> 10 * 1000000000000000
10000000000000000
iex(5)> 23 / 3
7.666666666666667
Logical Expressions
A type boolean
can store the values true
or false
. These can be used with
the operators and
, or
and not
:
iex(1)> true and true
true
iex(2)> false and false
false
iex(3)> true or false
true
iex(4)> not true
false
The operators and
, or
and not
can only work with boolean values. The
operators &&
(and), ||
(or) and !
(not) do the same but are a bit more
free-spirited and accept truthy and falsy values (false or nil).
Variables
You already know how variables work from experiences in other programming languages. Therefore we can dive right into it. Variable names follow the snake_case format and start with a lower case. Some examples:
iex(1)> length = 10 (1)
10
iex(2)> width = 23
23
iex(3)> area = length * width
230
1 | We use the operator = to bind the value 10 to the variable with the name length . |
If you start a variable name with a capital error you will get an error:
iex(4)> Radius = 2
** (MatchError) no match of right hand side value: 2 (1)
1 | Yes, MatchError is a rather strange error message here. It will make more
sense later. Binding values to variables is a bit more complicated than it seems
right now. |
Modules and Functions
So far, we have looked at basic calculations and types in isolation. However, if we want to create an application, we will need to combine these calculations and types in a structured way. To see how this is done, we need to look at modules and functions.
In Elixir, code is organized into modules, and each module is a collection of functions.
iex(1)> defmodule Store do (1)
...(1)> def total_price(price, amount) do (2)
...(1)> price * amount (3)
...(1)> end
...(1)> end
{:module, Store,
<<70, 79, 82, 49, 0, 0, 5, 4, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 133, 0,
0, 0, 14, 12, 69, 108, 105, 120, 105, 114, 46, 83, 116, 111, 114, 101, 8, 95,
95, 105, 110, 102, 111, 95, 95, 7, ...>>, {:total_price, 2}} (4)
iex(2)> Store.total_price(10,7) (5)
70
1 | defmodule is the keyword to define a module. The name of a module starts with a capital letter. |
2 | def is the keyword to define a function within a module. |
3 | The return value of a function is the return value of the last expression in the function. |
4 | The return value of creating the module. To save space, we will abbreviate this output in the next examples. |
5 | A function of a given module can be called from outside the module with this syntax. |
defmodule
and def
use a do … end
construct to begin and end.
Module names use CamelCase starting with a capital letter. Function names use snake_case. |
You can also define a module in a separate file (with the .exs
extension),
and then call the function with iex filename.exs
.
As an example, save the following module to math.exs
.
defmodule Math do
def sum(x, y) do
x + y
end
def difference(x, y) do
x - y
end
end
Then, if you run iex math.exs
, you can access the functions in the Math module
in iex.
iex(1)> Math.sum(1, 2)
3
iex(2)> Math.difference(3, 1)
2
Private Functions
Sometimes you want to define a function within a module without exposing it to
the outside world. You can do this with a private function which gets declared
with defp
:
iex(1)> defmodule Area do
...(1)> def circle(radius) do
...(1)> pi() * radius * radius
...(1)> end
...(1)>
...(1)> defp pi do (1)
...(1)> 3.14
...(1)> end
...(1)> end
{:module, Area, ...
iex(2)> Area.circle(10) (2)
314.0
iex(3)> Area.pi (3)
** (UndefinedFunctionError) function Area.pi/0 is undefined or private
Area.pi()
1 | The function pi/0 is a private function. |
2 | The function circle/1 can be called from outside the module. It can use
the private function pi/0 from within the module. |
3 | The function pi/0 can not be called from outside the module. |
Function Arity
In the last couple of sentences, you probably recognized that the names of
functions were followed by the number of parameters. We refer to the pi
function as pi/0
and the circle
function as circle/1
. We call this number
arity. Arity is kind of a big thing in Elixir. Why? Because not just the
function name but also the arity defines a function. For example, the
Rectangle
module below has two functions with the same name, but different
arity, and so they are treated as different functions:
iex(1)> defmodule Rectangle do
...(1)> def area(a) do (1)
...(1)> a * a
...(1)> end
...(1)>
...(1)> def area(a, b) do (2)
...(1)> a * b
...(1)> end
...(1)> end
{:module, Rectangle, ...
iex(2)> Rectangle.area(9) (3)
81
iex(3)> Rectangle.area(4, 5) (4)
20
1 | The function area/1 with an arity of 1 accepts one parameter. |
2 | The function area/2 with an arity of 2 accepts two parameters. This is
essentially a different function from area/1 . |
3 | So to calculate the area of a square you can call area/1 with just one parameter. |
4 | All non square rectangle areas have to be calculated with area/2 which accepts two parameters. |
Hierarchical Modules
In a big project, you will have multiple layers of Module namespaces to keep everything in some sort of structure.
This can be done by adding .
between the Module names:
iex(1)> defmodule Calculator.Area do
...(1)> def square(a) do
...(1)> a * a
...(1)> end
...(1)> end
{:module, Calculator.Area, ...
iex(2)> Calculator.Area.square(5)
25
It is just a shortcut. You could also nest the Modules:
iex(1)> defmodule Calculator do
...(1)> defmodule Area do
...(1)> def square(a) do
...(1)> a * a
...(1)> end
...(1)> end
...(1)> end
{:module, Calculator, ...
iex(2)> Calculator.Area.square(5)
25
Import
We can import access to public functions from other modules. So that we don’t have to use their fully qualified name.
iex(1)> defmodule Rectangle do
...(1)> def area(a) do
...(1)> a * a
...(1)> end
...(1)>
...(1)> def area(a, b) do
...(1)> a * b
...(1)> end
...(1)> end
{:module, Rectangle, ...
iex(2)> import Rectangle (1)
Rectangle
iex(3)> area(5) (2)
25
1 | Here we import Rectangle to have all the functions of that module at our fingertips. |
2 | No need to Rectangle.area/1 any more area/1 is just fine. |
And you can also just import special functions from that module:
iex(3)> import Rectangle, only: [area: 2] (1)
Rectangle
iex(4)> area(1) (2)
** (CompileError) iex:7: undefined function area/1
iex(7)> area(1,5) (3)
5
1 | Let’s just import area/2 but not all the other functions of that module. |
2 | I try to run area/1 , but that triggered an error because I didn’t import it. |
3 | Just works fine. |
Whenever you just use a given function without a module name before it,
that means that the module has already been imported by Elixir (e.g. the
Kernel module gets imported automatically).
|
Alias
alias
sets an alias for a module.
iex(1)> defmodule Calculator.Area do
...(1)> def square(a) do
...(1)> a * a
...(1)> end
...(1)> end
{:module, Calculator.Area, ...
iex(2)> alias Calculator.Area, as: Area (1)
Calculator.Area
iex(3)> Area.square(99)
9801
iex(4)> alias Calculator.Area (2)
Calculator.Area
iex(5)> Area.square(99)
9801
1 | Set an alias for Calculator.Area as Area . |
2 | A shortcurt for that specific case. Same result but less to type. |
Use
use
allows a module to inject code into the current module, such as importing
modules, defining new functions, setting a module’s state, etc.
In many of the tests in your Phoenix application, you will see use
ExUnit.Case
, which performs certain checks, sets some module attributes and
imports needed modules.
Atoms
An atom is a constant whose name is its value. In some other programming
languages, these are known as symbols. Atoms start with a :
Atoms are often used to tag values and messages. For example, functions that
might fail often have the return values {:ok, value}
or {:error, message}
.
Atoms are also used to reference modules from Erlang libraries.
iex(1)> :red
:red
iex(2)> :blue
:blue
iex(3)> is_atom(:blue) (1)
true
1 | The function is_atom() can be used to check if something is an atom. |
You should write atoms in snake_case or CamelCase. The usual Elixir convention is to use snake_case. |
Strings
We already used a string in the Hello World example. The following examples show how strings can be used with variables:
iex(1)> first_name = "Stefan" (1)
"Stefan"
iex(2)> last_name = "Wintermeyer"
"Wintermeyer"
iex(3)> name = first_name <> " " <> last_name (2)
"Stefan Wintermeyer"
iex(4)> greeting = "Hello #{first_name}!" (3)
"Hello Stefan!"
iex(5)> counter = 23
23
iex(6)> "Count: #{counter}" (4)
"Count: 23"
1 | We assign the string "Stefan" to the variable with the name first_name . |
2 | The <> operator can be used to concatinate strings.
|
3 | #{} is used to interpolate strings. It can be used to inject a variable
into a string. |
4 | Elixir’s string interpolation also works with integers. By default, it can handle integers, floats, some lists (later more on lists) and atoms. |
String Functions
The String module contains functions for working with strings. Here are some examples:
iex(1)> String.downcase("SToP SHoutING!")
"stop shouting!"
iex(2)> String.split("no fist is big enough to hide the sky") (1)
["no", "fist", "is", "big", "enough", "to", "hide", "the", "sky"]
iex(3)> String.split("mail@example.com", "@") (2)
["mail", "example.com"]
iex(4)> String.to_integer("555")
555
1 | String.split/1 divides a string into substrings at each whitespace. |
2 | String.split/2 is similar to String.split/1 , but it also allows you to
define what pattern to use when splitting the string. |
remember that you can also access the documentation for the String module
in iex by running h String .
|
The Pipe Operator (|>)
Quite often one wants to chain a couple of different functions in a row. Let’s
assume you want to reverse a string with String.reverse/1
and capitalize it
with String.capitalize/1
afterwards. Here’s the code to do that:
iex(1)> String.reverse("house") (1)
"esuoh"
iex(2)> String.capitalize("esuoh") (2)
"Esuoh"
iex(3)> String.capitalize(String.reverse("house")) (3)
"Esuoh"
1 | String.reverse/1 reverses the string. |
2 | String.capitalize/1 capitalizes all the letters in a string. |
3 | Connect the two functions. |
The problem with String.capitalize(String.reverse("house"))
is the lack of
readability. It kind of works with just two functions, but what about one or two
more functions in that line? Here comes the pipe operator |>
to the rescue.
It is a piece of syntactic sugar. Have a look:
iex(4)> String.reverse("house") |> String.capitalize() (1)
"Esuoh"
1 | The pipe operator |> passes the result of the first function to the first
parameter of the following function. |
Of course you can use multiple pipe operators:
iex(5)> String.reverse("house") |> String.capitalize() |> String.slice(0, 3)
"Esu"
By using the pipe operator, the code becomes more readable and more maintainable.
Lists and Tuples
We store multiple elements in lists and tuples. Lists and tuples look alike but are quite different performance-wise.
-
Tuples are fast when you have to access its data but slow when you want to change its data. They are stored contiguously in memory. Accessing one element of a tuple or getting the size of it is fast and always takes the same amount of time.
-
Lists are stored as linked lists in memory. One element holds it’s own value and a link to the next element. Accessing single elements and the length of lists is a linear operation which takes more time. The longer the list, the more time it takes. But it is fast to add a new element to the end of a list.
Right now, you don’t need to lose sleep over the decision of which one to use. Throughout the book, you’ll get a feeling which one is best suited for what problem. |
Lists
Lists store multiple values, and they can contain different types. A list is
enclosed in brackets ([]
):
iex(1)> [1, 2, 3, 4]
[1, 2, 3, 4]
iex(2)> ["a", "b", "c"]
["a", "b", "c"]
iex(3)> [1, "b", true, false, :blue, "house"]
[1, "b", true, false, :blue, "house"]
The operators ++
and --
can be used to concatenate and substract lists from each other:
iex(1)> [1, 2] ++ [2, 4] (1)
[1, 2, 2, 4]
iex(2)> [1, 2] ++ [1] (2)
[1, 2, 1]
iex(3)> [1, "a", 2, false, true] -- ["a", 2] (3)
[1, false, true]
1 | Makes total sense. |
2 | So does this. |
3 | A bit trickier. The second and third element of the first list get subtracted. |
Head and Tail of Lists
A lot of times Elixir developers want to work with the head (the first element)
and tail (the rest) of a list. The following examples show how the functions
hd/1
and tl/1
can be used to return these values:
iex(1)> shopping_list = ["apple", "orange", "banana", "pineapple"] (1)
["apple", "orange", "banana", "pineapple"]
iex(2)> hd(shopping_list) (2)
"apple"
iex(3)> tl(shopping_list) (3)
["orange", "banana", "pineapple"]
iex(4)> shopping_list (4)
["apple", "orange", "banana", "pineapple"]
1 | We define a list and bind it to the variable shopping_list . |
2 | hd/1 fetches the first element of the list. |
3 | tl/1 fetches the rest of the list. |
4 | The shopping_list itself hasn’t changed. |
Let’s see what happens with empty lists or lists which just have one element:
iex(6)> hd([]) (1)
** (ArgumentError) argument error
:erlang.hd([])
iex(6)> tl([]) (2)
** (ArgumentError) argument error
:erlang.tl([])
iex(6)> hd(["grapefruit"]) (3)
"grapefruit"
iex(7)> tl(["grapefruit"]) (4)
[]
1 | You can’t get the head of an empty list. |
2 | And there is no tail of an empty list. |
3 | There is a "head" of a list with one element. |
4 | The "tail" of a file with one element is an empty list. |
length/1
The function length/1
tells how many elements a list contains:
iex(1)> shopping_list = ["apple", "orange", "banana", "pineapple"]
["apple", "orange", "banana", "pineapple"]
iex(2)> length(shopping_list)
4
iex(3)> length([1, 2])
2
iex(4)> length([])
0
List Functions
When working with lists, you will often use functions from the Enum module. There is also a List module, which contains a few useful list functions.
Here are a few examples:
iex(1)> numbers = [1, 5, 3, 7, 2, 3, 9, 5, 3]
[1, 5, 3, 7, 2, 3, 9, 5, 3]
iex(2)> Enum.max(numbers) (1)
9
iex(3)> Enum.sort(numbers) (2)
[1, 2, 3, 3, 3, 5, 5, 7, 9]
iex(4)> words = ["nothing", "like", "the", "sun"]
["nothing", "like", "the", "sun"]
iex(5)> Enum.join(words, " ")
"nothing like the sun"
iex(6)> List.last(words)
"sun"
1 | Enum.max/1 returns the maximum value in a list. |
2 | Enum.sort/1 returns a new list with the values sorted in ascending order. |
We will see more examples from the Enum
module when we look at higher-order
functions later in this introduction.
Tuples
Like lists, tuples can hold multiple elements of different types. The
elements are enclosed in curly braces ({}
):
iex(1)> {1, 2, 3} (1)
{1, 2, 3}
iex(2)> {:ok, "test"} (2)
{:ok, "test"}
iex(3)> {true, :apple, 234, "house", 3.14} (3)
{true, :apple, 234, "house", 3.14}
1 | A tuple which contains three integers. |
2 | A tuple which contains one atom that represents the status and a string. It is something prevalent in Elixir. You will see this a lot. |
3 | A tuple with values of different types. |
We can access an element of a tuple with by passing the index to the elem/2
function:
iex(1)> result = {:ok, "Lorem ipsum"}
{:ok, "Lorem ipsum"}
iex(2)> elem(result, 1) (1)
"Lorem ipsum"
iex(3)> elem(result, 0) (2)
:ok
1 | The function elem/2 gives us a fast access to each element of a tuple. |
2 | The count starts with 0 for the first element. |
Tuple Functions
The Tuple module contains functions for working with tuples. Here are some examples:
-
Tuple.append/2
adds an element to a tuple. -
Tuple.delete_at/2
deletes an element of a tuple. -
Tuple.insert_at/3
adds an element at a specific position. -
Tuple.to_list/1
converts a tuple to a list. -
Tuple.size/1
returns the number of elements of the tuple.
Examples:
iex(1)> results = {:ok, "Lorem ipsum"}
{:ok, "Lorem ipsum"}
iex(2)> b = Tuple.append(results, "Test")
{:ok, "Lorem ipsum", "Test"}
iex(3)> c = Tuple.delete_at(b, 1)
{:ok, "Test"}
iex(4)> d = Tuple.insert_at(b, 1, "ipsum")
{:ok, "ipsum", "Lorem ipsum", "Test"}
iex(5)> new_list = Tuple.to_list(d)
[:ok, "ipsum", "Lorem ipsum", "Test"]
iex(6)> tuple_size(d)
4
Higher-Order Functions
In Elixir, functions can be used like any other variable. For example, they can be passed to other functions as parameters.
A function that takes another function as one of its parameters is called a higher-order function, and these are very commonly used in Elixir.
When passing a function to a higher-order function, we need to use an anonymous function, and that is what we will look at next.
Anonymous Functions
Anonymous functions are functions that are defined without any name.
You define anonymous functions using the fn
keyword:
iex(1)> greeting = fn(name) -> "Hello #{name}!" end (1)
#Function<7.126501267/1 in :erl_eval.expr/5>
iex(2)> greeting.("Bob") (2)
"Hello Bob!"
iex(3)> greeting.("Alice")
"Hello Alice!"
iex(4)> square_area = fn a -> a * a end (3)
#Function<7.126501267/1 in :erl_eval.expr/5>
iex(5)> square_area.(10)
100
iex(6)> area = fn width, length -> width * length end (4)
#Function<13.126501267/2 in :erl_eval.expr/5>
iex(7)> area.(2,8)
16
1 | We create an anonymous function and bind it to the variable greeting .
|
2 | We need to use the . (dot) operator to run anonymous functions. |
3 | You don’t have to surround the function arguments with parentheses. They are optional. |
4 | Like regular functions, anonymous functions can be called with multiple arguments. The arguments are separated by commas. |
Most of the time anonymous functions are simple one liners. But they don’t have to be:
iex(1)> circular_area = fn radius ->
...(1)> pi = 3.14159265359
...(1)> pi * radius * radius
...(1)> end
#Function<7.126501267/1 in :erl_eval.expr/5>
iex(2)> circular_area.(3)
28.274333882310003
Let’s now look at using anonymous functions with higher-order functions:
iex(1)> numbers = [1,2,3,4,5,6,7,8,9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
iex(2)> Enum.filter(numbers, fn num -> rem(num, 2) == 0 end) (1)
[2, 4, 6, 8]
iex(6)> Enum.map(numbers, fn x -> x * x end) (2)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
1 | Enum.filter/2 filters a list and returns those elements for which the
function returns true. The rem/2 function calculates the remainder after
integer division. |
2 | Enum.map/2 calls the given function for every item in the list and returns a
new list. |
The & operator
Another way of creating anonymous functions is to use the &
operator, which
is called the capture operator.
iex(1)> second = &Enum.at(&1, 1) (1)
#Function<44.97283095/1 in :erl_eval.expr/5>
iex(2)> second.([1,2,3,4]) (2)
2
iex(3)> is_negative? = &(&1 < 0)
#Function<44.97283095/1 in :erl_eval.expr/5>
iex(4)> is_negative?.(-1)
true
1 | &1 refers to the first parameter. |
2 | Again, we need to use the . (dot) operator to run anonymous functions. |
And here are examples of using the capture operator with higher-order functions.
iex(1)> maybe_numbers = [1, nil, 4, nil, 5]
[1, nil, 4, nil, 5]
iex(2)> Enum.filter(maybe_numbers, &is_integer(&1)) (1)
[1, 4, 5]
iex(3)> Enum.filter(maybe_numbers, &is_integer/1) (2)
[1, 4, 5]
iex(4)> Enum.sort([1, 2, 3], &(&1 >= &2)) (3)
[3, 2, 1]
1 | &1 refers to the first parameter. |
2 | The same as the previous function, but with a different syntax. The /1
after is_integer means that the function takes one parameter. |
3 | You can use multiple parameters too (e.g. &1 , &2 ). |
Sometimes it is more convenient to use the &
operator, but there are times
when it makes the expression more difficult to read.
Variable Scopes
In every programming language variables have some sort of scope. Let’s have a look into some code to figure out how variables in Elixir are scoped:
iex(1)> area = 5 (1)
5
iex(2)> IO.puts(area)
5
:ok
iex(3)> square_area = fn a -> (2)
...(3)> area = a * a (3)
...(3)> area
...(3)> end
#Function<7.126501267/1 in :erl_eval.expr/5>
iex(4)> square_area.(10) (4)
100
iex(5)> IO.puts(area) (5)
5
:ok
1 | We bind the value of 5 to the variable area . |
2 | We define an anonymous function. |
3 | Within this function we bind the result of our calculation to another variable area . |
4 | Run the function with an argument of 10. That would mean that the area in the function gets set to the value 100. |
5 | The original area hasn’t changed a bit. Because it is in a different scope. |
The area
within the function is in an inner scope. The original area
is in an outer scope.
But it gets a bit more complex:
iex(1)> pi = 3.14159265359 (1)
3.14159265359
iex(2)> circular_area = fn radius -> pi * radius * radius end (2)
#Function<7.126501267/1 in :erl_eval.expr/5>
iex(3)> circular_area.(10)
314.15926535899996
1 | We bind the value 3.14159265359 to the variable with the name pi . |
2 | We create an anonymous function which uses the variable pi to make the calculation. |
So we can read the outer scope variable from within the function. So lets check if we can change it too:
iex(1)> pi = 3.14159265359 (1)
3.14159265359
iex(2)> circular_area = fn radius ->
...(2)> pi = 3.14 (2)
...(2)> pi * radius * radius
...(2)> end
#Function<7.126501267/1 in :erl_eval.expr/5>
iex(3)> circular_area.(10) (3)
314.0
iex(4)> IO.puts(pi) (4)
3.14159265359
:ok
1 | We bind the value 3.14159265359 to the variable with the name pi . |
2 | We bind the inner scoped variable pi with the value 3.14. |
3 | The 3.14 and not the 3.14159265359 gets used. |
4 | The outer scoped pi is not changed. |
You can not change the value of an outer scoped variable, but you can read it. And you can create a new inner scope variable with the same name without interacting with the outer scoped one. |
Keyword Lists, Maps and Structs
List and Tuples don’t provide the functionality to access values with a key. We can achieve that functionality with keyword lists, maps and structs.
Keyword Lists
Keyword lists are key-value data structures, in which keys are atoms and keys can appear more than once.
iex(1)> user = [{:name, "joe"}, {:age, 23}] (1)
[name: "joe", age: 23]
iex(2)> user = [name: "joe", age: 23] (2)
[name: "joe", age: 23]
iex(3)> user[:name] (3)
"joe"
iex(4)> new_user = [name: "fred"] ++ user
[name: "fred", name: "joe", age: 23]
iex(5)> new_user[:name] (4)
"fred"
1 | Keyword lists are lists of 2-item tuples, with the first item of each tuple being an atom. |
2 | This [key: value] syntax is more commonly used (this expression is
the same as the list of tuples above). |
3 | The keyword list name followed by the key name in brackets returns a value for the given key. |
4 | If there are duplicate keys in a keyword list, the first one is fetched on lookup. |
In your Phoenix application, you will see a keyword list used as the last
argument in the render/3
function:
render(conn, "show.html", message: "Hello", username: "Mary") (1)
1 | [message: "Hello", username: "Mary"] is a keyword list. As you can see
from this example, the brackets are optional. |
Keyword List Functions
The Keyword module offers functions for working with keyword lists.
Here are a few examples:
iex(1)> Keyword.get([age: 34, height: 155], :height)
155
iex(2)> Keyword.delete([length: 78, width: 104], :length)
[width: 104] (1)
1 | After deleting the :length , the keyword list just contains the :width
key-value pair. |
Maps
Maps provide a way to store and retrieve key-value pairs. The %{}
syntax
creates a Map.
iex(1)> product_prices = %{"Apple" => 0.5, "Orange" => 0.7} (1)
%{"Apple" => 0.5, "Orange" => 0.7}
iex(2)> product_prices["Orange"] (2)
0.7
iex(3)> product_prices["Banana"] (3)
nil
iex(4)> product_prices = %{"Apple" => 0.5, "Orange" => 0.7, "Apple" => 1}
warning: key "Apple" will be overridden in map
iex:4
%{"Apple" => 1, "Orange" => 0.7} (4)
1 | We create a new map and bind it to the variable product_prices . |
2 | The map name followed by the key name in brackets returns a value for the given key. |
3 | This returns nil if a given key doesn’t exist. |
4 | Unlike keyword lists, maps cannot contain duplicate keys. |
But keys don’t have to be a specific type. Everything can be a key and a value:
iex(1)> %{"one" => 1, "two" => "abc", 3 => 7, true => "asdf"} (1)
%{3 => 7, true => "asdf", "one" => 1, "two" => "abc"}
iex(2)> %{"one" => 1, true => "asdf", true => "z"} (2)
warning: key true will be overridden in map
iex:2
%{true => "z", "one" => 1}
1 | A mixed bag of different types. Feel free to go wild. |
2 | A key has to be unique within a map. The last one overwrites the previous
values. In this case, the key true will have a value of "z". |
Atom keys
Using atoms as keys in maps gives you access to some nifty features:
iex(1)> product_prices = %{apple: 0.5, orange: 0.7} (1)
%{apple: 0.5, orange: 0.7}
iex(2)> product_prices.apple (2)
0.5
iex(3)> product_prices.banana (3)
** (KeyError) key :banana not found in: %{apple: 0.5, orange: 0.7}
1 | With atoms as keys you can use this syntax which is a bit easier to read and less work to type. |
2 | When using atom keys, you can use the dot operator (. ) to return the value of a given key. |
3 | If you use the dot operator and the key does not exist, an error is raised. |
Map Functions
The Map module offers many useful functions for working with maps.
Here are just a few examples:
iex(1)> product_prices = %{apple: 0.5, orange: 0.7, coconut: 1}
%{apple: 0.5, coconut: 1, orange: 0.7}
iex(2)> Map.to_list(product_prices) (1)
[apple: 0.5, coconut: 1, orange: 0.7]
iex(3)> Map.values(product_prices) (2)
[0.5, 1, 0.7]
iex(4)> Map.split(product_prices, [:orange, :apple]) (3)
{%{apple: 0.5, orange: 0.7}, %{coconut: 1}}
iex(5)> a = Map.delete(product_prices, :orange) (4)
%{apple: 0.5, coconut: 1}
iex(6)> b = Map.drop(product_prices, [:apple, :orange]) (5)
%{coconut: 1}
iex(7)> additional_prices = %{banana: 0.4, pineapple: 1.2}
%{banana: 0.4, pineapple: 1.2}
iex(8)> Map.merge(product_prices, additional_prices) (6)
%{apple: 0.5, banana: 0.4, coconut: 1, orange: 0.7, pineapple: 1.2}
iex(9)> c = Map.put(product_prices, :potato, 0.2) (7)
%{apple: 0.5, coconut: 1, orange: 0.7, potato: 0.2}
1 | Map.to_list/1 converts a map into a keyword list. |
2 | Map.values/1 returns the values of a map. |
3 | Map.split/2 splits a given map into two new maps. The first one contains
all the key-value pairs which are requested by a list (e.g. [:orange, :apple] ) |
4 | Map.delete/2 deletes a specific key-value pair from a map. |
5 | Map.drop/2 deletes a list of key-value pairs from a map. |
6 | Map.merge/2 merges two maps. |
7 | Map.put/2 adds a key-value pair to a map. |
Structs
A struct is a map that provides compile-time checks and default values. To
define a struct you have to use the defstruct
construct:
iex(1)> defmodule Product do (1)
...(1)> defstruct name: nil, price: 0 (2)
...(1)> end
{:module, Product, ...
iex(2)> %Product{}
%Product{name: nil, price: 0}
iex(3)> apple = %Product{name: "Apple", price: 0.5} (3)
%Product{name: "Apple", price: 0.5}
iex(4)> apple
%Product{name: "Apple", price: 0.5}
iex(5)> apple.price
0.5
iex(6)> orange = %Product{name: "Orange"} (4)
%Product{name: "Orange", price: 0}
1 | We define a new struct with the name Product and the keys name and price . |
2 | We define default values. |
3 | We define a new Product struct and set all values. |
4 | We define a new Product struct and set only the name. The price is set to the default value. |
A struct guarantees that only the defined fields are allowed:
iex(7)> apple.description (1)
** (KeyError) key :description not found in: %Product{name: "Apple", price: 0.5}
iex(7)> banana = %Product{name: "Banana", weight: 0.1} (2)
** (KeyError) key :weight not found
expanding struct: Product.__struct__/1
iex:7: (file)
iex(7)>
1 | Since we didn’t define a description field in the Struct, we cannot access it. |
2 | Same with a new struct. There is no weight field defined. Therefore we can not set it. |
Because structs are built on top of maps, they can be used with the same functions. |
Pattern Matching
Pattern matching is essential in Elixir, and we have already used it, without knowing it, for binding values to variables.
iex(1)> a = 10 (1)
10
iex(2)> a
10
iex(3)> {b, c} = {10, 15} (2)
{10, 15}
iex(4)> b
10
iex(5)> c
15
iex(6)> {d, e} = 100
** (MatchError) no match of right hand side value: 100 (3)
1 | This is actually a pattern match. The left side of = will be matched to the right site if possible. |
2 | Here we pattern match {b, c} on the left side with a tuple on the right side. |
3 | Boom! Because we can not match the {d, e} tuple with an integer we get a MatchError . |
Since we don’t have much time, I’ll fast forward to match a head and tail of a list. Because there is a special syntax for that:
iex(1)> shopping_list = ["apple", "orange", "banana", "pineapple"] (1)
["apple", "orange", "banana", "pineapple"]
iex(2)> [head | tail] = shopping_list (2)
["apple", "orange", "banana", "pineapple"]
iex(3)> head
"apple"
iex(4)> tail
["orange", "banana", "pineapple"]
iex(5)> [a | b] = tail (3)
["orange", "banana", "pineapple"]
iex(6)> a
"orange"
iex(7)> b
["banana", "pineapple"]
iex(8)> [first_product, second_product | tail] = shopping_list (4)
["apple", "orange", "banana", "pineapple"]
iex(9)> first_product
"apple"
iex(10)> second_product
"orange"
iex(11)> tail
["banana", "pineapple"]
iex(12)> [first_product | [second_product | tail]] = shopping_list (5)
["apple", "orange", "banana", "pineapple"]
1 | We match a list to the variable shopping_list . |
2 | [head | tail] is the special syntax to match a head and tail of a given list. |
3 | Again we match the head a and the tail b with tail . |
4 | A bit more complex. We match agains the first and second product followed by a tail. |
5 | Same result. Different syntax and logic. Pick the one you prefer. |
Of course, if we know that a list has a specific number of elements we can match it directly:
iex(1)> shopping_list = ["apple", "orange", "banana", "pineapple"]
["apple", "orange", "banana", "pineapple"]
iex(2)> [a, b, c, d] = shopping_list
["apple", "orange", "banana", "pineapple"]
iex(3)> a
"apple"
iex(4)> b
"orange"
iex(5)> [e, f, g] = shopping_list (1)
** (MatchError) no match of right hand side value: ["apple", "orange", "banana", "pineapple"]
1 | Just checking. You get an MatchError if Elixir can’t match both sides. |
Matching Maps
Matching a Map works a little bit different to matching a Tuple or List. You can match just against the values you are interested in:
iex(1)> product_prices = %{apple: 0.5, orange: 0.7, pineapple: 1}
%{apple: 0.5, orange: 0.7, pineapple: 1}
iex(2)> %{orange: price} = product_prices (1)
%{apple: 0.5, orange: 0.7, pineapple: 1}
iex(3)> price
0.7
iex(4)> %{orange: price1, apple: price2} = product_prices (2)
%{apple: 0.5, orange: 0.7, pineapple: 1}
iex(5)> price1
0.7
iex(6)> price2
0.5
1 | We can just match one value. |
2 | Or we can match multiple values. But we don’t have to match the whole Map. |
Matching String parts
Easiest explained with a code example:
iex(1)> user = "Stefan Wintermeyer"
"Stefan Wintermeyer"
iex(2)> "Stefan " <> last_name = user
"Stefan Wintermeyer"
iex(3)> last_name
"Wintermeyer"
The left side of a <> operator in a match should always be a string.
Otherwise, Elixir can’t verify it’s size.
|
Wildcard Matching
Sometimes you need pattern matching to get a value, but you don’t need all of
the values in the pattern. For those cases, you can use _
(alone or as a
prefix to a variable name). It indicates to Elixir that you don’t need that
variable to be bound to anything.
iex(1)> shopping_list = ["apple", "orange", "banana", "pineapple"]
["apple", "orange", "banana", "pineapple"]
iex(2)> [first_product | _tail] = shopping_list (1)
["apple", "orange", "banana", "pineapple"]
iex(3)> first_product
"apple"
iex(4)> [head | _] = shopping_list (2)
["apple", "orange", "banana", "pineapple"]
iex(5)> head
"apple"
1 | We pattern match the head of shopping_list to first_product . But we don’t need the tail, and we indicate that by prefixing it with a _ . |
2 | We can use just a _ too. Using _tail just improves the code readability
a bit. |
Pattern Matching with Functions
Pattern matching is used everywhere in Elixir. You can even use it with Functions:
iex(1)> defmodule Area do
...(1)> def circle(:exact, radius) do (1)
...(1)> 3.14159265359 * radius * radius
...(1)> end
...(1)>
...(1)> def circle(:normal, radius) do (2)
...(1)> 3.14 * radius * radius
...(1)> end
...(1)>
...(1)> def circle(radius) do (3)
...(1)> circle(:normal, radius)
...(1)> end
...(1)> end
{:module, Area, ...
iex(2)> Area.circle(:exact, 4)
50.26548245744
iex(3)> Area.circle(:normal, 4)
50.24
iex(4)> Area.circle(4)
50.24
1 | We define a circle/2 function which matches if the first argument is the atom :exact . |
2 | We define a circle/2 function which matches if the first argument is the atom :normal . |
3 | We define a circle/1 function which calls the cirle/2 function with the :normal argument. |
Functions with Guards
Guards add some additional spices to pattern matching with functions. You can find all the details at https://hexdocs.pm/elixir/guards.html
Here are just some examples to show you the concept. Guards start with when
:
iex(1)> defmodule Law do
...(1)> def can_vote?(age) when is_integer(age) and age > 17 do (1)
...(1)> true
...(1)> end
...(1)>
...(1)> def can_vote?(age) when is_integer(age) do (2)
...(1)> false
...(1)> end
...(1)>
...(1)> def can_vote?(_age) do (3)
...(1)> raise ArgumentError, "age should be an integer"
...(1)> end
...(1)> end
{:module, Law, ...
iex(2)> Law.can_vote?(15)
false
iex(3)> Law.can_vote?(20)
true
iex(4)> Law.can_vote?("test") (4)
** (ArgumentError) age should be an integer
iex:4: Law.can_vote?/1
1 | This guard checks if the age argument is an integer and the value of it is bigger than 17. |
2 | This guard just checks if the age argument is an integer. |
3 | This clause catches any value that is not called with an integer. |
4 | Since "test" is a string and not an integer, the ArgumentError that we
wrote is raised. |
Case
case
is a control structure which matches a given value to a couple of
matching cases until one matches.
Let’s assume we want to create a function that converts morse coded numbers to integers:
iex(1)> defmodule Morse do
...(1)> def morse_to_number(input) do
...(1)> case input do (1)
...(1)> "-----" -> 0 (2)
...(1)> ".----" -> 1
...(1)> "..---" -> 2
...(1)> "...--" -> 3
...(1)> "....-" -> 4
...(1)> "....." -> 5
...(1)> "-...." -> 6
...(1)> "--..." -> 7
...(1)> "---.." -> 8
...(1)> "----." -> 9
...(1)> _ -> :error (3)
...(1)> end
...(1)> end
...(1)> end
{:module, Morse, ...
iex(2)> Morse.morse_to_number("-....") (4)
6
1 | After case comes the value we want to check. |
2 | "-----" is the expression we want to match to return a 0. |
3 | _ is the catch-all in case nothing matched yet. In this case, return an :error atom. |
4 | It works. :-) |
Of course, we could solve this problem just with functions too. It’s up to you what makes the most sense in a given situation.
if and unless
if
is common to many programming languages. unless
is equivalent to if
not
. The following examples will show how to use them:
iex(1)> if 1 == 1 do
...(1)> "Bingo!"
...(1)> else
...(1)> "Negative"
...(1)> end
"Bingo!"
iex(2)> unless true do
...(2)> "Never"
...(2)> end
nil
Sometimes you see a one-line short form:
iex(3)> if 1 == 1, do: "Bingo!"
"Bingo!"
Most Elixir developers prefer case over if or unless .
|
Immutability
Probably you have already heard about immutability in Elixir. What’s that about?
A variable points to a specific part of the memory where the data is stored. In many programming languages that data can be changed to update a variable. In Elixir, you can’t change it. So that doesn’t mean that you can’t rebind a variable to a different value but that this new value gets a new piece of memory and doesn’t overwrite the old memory. Once a function returns a result and therefore, has finished its work, everything gets garbage collected (wiped blank).
Why is that important at all? With immutable variables, we can be sure that other processes can not change their values while running parallel tasks. That has a massive effect. In the end, it means that your Phoenix application can run on multiple CPUs on the same server in parallel. It even means that your Phoenix application can share multiple CPUs on several nodes of a server cluster in your data center; this makes Elixir extremely scalable and save.
But doesn’t that make your application slower? Funny thing: No. This way is faster. It is not efficient to change data in memory.
But don’t worry. It is not as complicated as it sounds. Everytime you use a variable it uses the value of that moment in time. It will not be effected/changed afterwords:
iex(1)> product = "Orange"
"Orange"
iex(2)> test1 = fn -> IO.puts(product) end (1)
#Function<21.126501267/0 in :erl_eval.expr/5>
iex(3)> product = "Apple"
"Apple"
iex(4)> test2 = fn -> IO.puts(product) end
#Function<21.126501267/0 in :erl_eval.expr/5>
iex(5)> product = "Pineapple"
"Pineapple"
iex(6)> test3 = fn -> IO.puts(product) end
#Function<21.126501267/0 in :erl_eval.expr/5>
iex(7)> product = "Banana"
"Banana"
iex(8)> test1.() (2)
Orange
:ok
iex(9)> test2.()
Apple
:ok
iex(10)> test3.()
Pineapple
:ok
iex(11)> IO.puts(product)
Banana
:ok
1 | Those anonymous functions may run on totally different CPUs. The life in their own little universe. |
2 | The value of product has changed multiple times. But for test1.() it is the value from that point in time when we created the function. |
Sigils
Until now encapsulated Strings in double quotes and we haven’t talked about char
lists at all (IMO not needed for a beginners introduction). But there is one more mechanism to represent texts. They are called Sigils
and start with a ~
(tilde) character which is followed by one letter which indicates what kind of sigil it is. After that, you can use a couple of different delimiters:
~r/example text/
~r|example text|
~r"example text"
~r'example text'
~r(example text)
~r[example text]
~r{example text}
~r<example text>
Elixir provides different delimiters for sigils so that you can write literals without escaped delimiters. |
Regular expressions
~r
marks a regular expression:
iex(1)> regex = ~r/bcd/
~r/bcd/
iex(2)> "abcde" =~ regex
true
iex(3)> "efghi" =~ regex
false
iex(4)> regex = ~r/stef/i (1)
~r/stef/i
iex(5)> "Stefan" =~ regex
true
1 | Modifiers are supported too. For a complete list have a look at https://hexdocs.pm/elixir/Regex.html |
String
You can use the ~s
sigil to generate a string:
iex(1)> example = ~s(WOW! "double" and 'single' quotes without escaping)
"WOW! \"double\" and 'single' quotes without escaping"
iex(2)> IO.puts(example)
WOW! "double" and 'single' quotes without escaping
:ok
Sigils support heredocs too. You can use triple, double, or single quotes as separators:
iex(1)> example_text = ~s"""
...(1)> This is an example text.
...(1)> Multiple lines are not a problem.
...(1)> """
"This is an example text.\nMultiple lines are not a problem.\n"
iex(2)> IO.puts(example_text)
This is an example text.
Multiple lines are not a problem.
:ok
Word lists
The ~w
sigil is a useful way to generate lists of words:
iex(1)> shopping_cart = ~w(apple orange banana)
["apple", "orange", "banana"]
iex(2)> shopping_cart_atoms = ~w(apple orange banana)a (1)
[:apple, :orange, :banana]
1 | The a modifier tells Elixir to generate a list of atoms and not strings. |
Date and Time
Elixir provides a couple of good to go time-related Struct[structs] which all have their sigil.
Date
Elixir provides a %Date{}
struct that contains the following fields:
-
year
-
month
-
day
-
calendar
With the ~D
sigil you can create new %Date{}
struct:
iex(1)> birthday = ~D[1973-03-23]
~D[1973-03-23]
iex(2)> birthday.day
23
iex(3)> birthday.month
3
iex(4)> birthday.year
1973
Time
Elixir provides a %Time{}
struct that contains the following fields:
-
hour
-
minute
-
second
-
microsecond
-
calendar
With the ~T
sigil you can create new %Time{}
struct:
iex(1)> now = ~T[09:29:00.0]
~T[09:29:00.0]
iex(2)> now.hour
9
NaiveDateTime
The %NaiveDateTime{}
struct mixes %Date{}
with %Time{}
.
With the ~N
sigil you can create new %NaiveDateTime{}
struct:
iex(1)> timestamp = ~N[2020-05-08 09:48:00]
~N[2020-05-08 09:48:00]
DateTime
The %DateTime{}
struct adds a timezone to a %NaiveDateTime{}
.
With the ~U
sigil you can create new %NaiveDateTime{}
struct:
iex(4)> timestamp = ~U[2029-05-08 09:59:03Z]
~U[2029-05-08 09:59:03Z]
Find more information about timezones and DateTime at https://hexdocs.pm/elixir/DateTime.html |
Recursion
Recursions are often used when you would use a loop in an object-oriented language.
Let’s write a recursive function which provides a countdown:
iex(1)> defmodule Example do
...(1)> def countdown(1) do (1)
...(1)> IO.puts "1" (2)
...(1)> end
...(1)>
...(1)> def countdown(n) when is_integer(n) and n > 1 do (3)
...(1)> IO.puts Integer.to_string(n) (4)
...(1)> countdown(n - 1) (5)
...(1)> end
...(1)> end
{:module, Example, ...
iex(2)> Example.countdown(4) (6)
4
3
2
1
:ok
1 | If countdown/1 is called with the argument 1 this is the best match. |
2 | We call IO.puts("1") to print 1 to STDOUT. |
3 | If countdown/1 is called with an integer bigger than 1 as an argument this function matches. |
4 | We have to use Integer.to_string(n) to print the integer to STDOUT. |
5 | We recursively decrese n by 1 and call countdown/1 with that new number. |
6 | It works! |
Here’s a different example where we calculate the sum of a list of integers:
iex(1)> defmodule Example do
...(1)> def sum([]) do (1)
...(1)> 0
...(1)> end
...(1)>
...(1)> def sum([head | tail]) do (2)
...(1)> head + sum(tail) (3)
...(1)> end
...(1)> end
{:module, Example, ...
iex(2)> Example.sum([10, 8, 12, 150]) (4)
180
iex(3)> [head | tail] = [150] (5)
[150]
iex(4)> tail
[]
1 | The sum of an empty list is 0. |
2 | We pattern match a list and split it into a head and a tail . |
3 | We add the current head to the sum of the tail . |
4 | It works! |
5 | This is just to show how Elixir handles the case of a list with one element. |
You can use the same concept to transform every element of a list. Let’s assume we want to double the value of every element of a list:
iex(1)> defmodule Example do
...(1)> def double([]) do (1)
...(1)> []
...(1)> end
...(1)>
...(1)> def double([head | tail]) do
...(1)> [head * 2 | double(tail)] (2)
...(1)> end
...(1)> end
{:module, Example, ...
iex(2)> Ex
Example Exception
iex(2)> Example.double([10, 5, 999])
[20, 10, 1998]
1 | We again start with the most simple match. An empty list. That will result in an empty list. |
2 | The [head | tail] syntax works both ways. We can use it to build a list too. |
How to tackle a recursion
Unless you are doing this every day, you will get to problems where you know that recursion is a good solution, but you just can’t think of a good recursion for it.
Let me share a pro tip for these situations: https://www.google.com and https://stackoverflow.com are my lifesavers in such cases. No embarrassment!
During this book, we will work with recursions. So you’ll get a better feeling for it.
mix
By now, you understand the basics of Elixir. The next step is to create an
application. In the Elixir ecosystem, this is done with the (already installed)
command-line interface (CLI) mix
. Let’s do that for a "Hello world!"
application:
$ mix new hello_world
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/hello_world.ex
* creating test
* creating test/test_helper.exs
* creating test/hello_world_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd hello_world
mix test
Run "mix help" for more commands.
The command ´mix new projectname` creates a new directory with the name projectname
and fills it with a default structure:
$ cd hello_world
$ tree
.
├── README.md
├── lib
│ └── hello_world.ex
├── mix.exs
└── test
├── hello_world_test.exs
└── test_helper.exs
2 directories, 5 files
The Phoenix directory structure will be more involved but has the same core.
mix tasks
A task is a mechanism to start code with mix
. For our "Hello world!"
programme we have to create the directory lib/mix/tasks
and create the file
lib/mix/tasks/start.ex
with this code:
lib/mix/tasks/start.ex
defmodule Mix.Tasks.Start do
use Mix.Task
def run(_) do (1)
IO.puts "Hello world!"
end
end
1 | The run(_) function is the default function which gets called automatically. |
Now we can start the mix start
task:
$ mix start
Compiling 1 file (.ex)
Generated hello_world app
Hello world!
The .ex
file gets compiled, and the start
task gets run. The compile is only
done when needed. If we call mix start
a second time no compile is needed:
$ mix start
Hello world!
Obviously mix
as a topic is much more complicated. In this section, I just
wanted to show you the very basic idea of mix
so that you know where to search
if you want to know what happens if you do a mix server
with a Phoenix
application.
mix format
You are going to love mix format
. You can call it in the root directory of
your Phoenix application and it will autoformat all your Elixir source code
files.
You should use mix format
every time you are going to commit code to a
repository.
What else?
This chapter just deals with the tip of the iceberg. It provides the basic knowledge that you need to start with the Phoenix Framework. There is a lot more to learn. But I wouldn’t worry too much about that right now. You are good to go for the next chapter of this book. Have fun!
Elixir Books
If you want to dive more into Elixir than I recommend the following books:
-
Learn Functional Programming with Elixir by Ulisses Almeida (@ulissesalmeida)
In my opinion, the best beginners book for Elixir.
-
[Programming Elixir 1.6](https://pragprog.com/book/elixir16/programming-elixir-1-6) by Dave Thomas (@pragdave)
Dave - as always - wrote a very book which shines a light into many details.