Elixir Succinctly Páginas 2

Download as pdf or txt
Download as pdf or txt
You are on page 1of 25

Though simple, these couple of functions illustrate some powerful features of Elixir.

You
probably first thought to use a loop to implement the function, but thanks to recursion and
pattern matching, the loop is not needed, and the code is very easy to read and understand.

Actually, this operation could be implemented using a function of the Enum module. The Enum
module contains the reduce function that can be used to aggregate values from a list:

iex(5)> Enum.reduce([1, 2, 3, 4], fn x, acc -> x + acc end)


10

The Enum.reduce function receives the enumerable list to work with, and for every element of
the list, it calls the function passed as the second parameter. This function receives the current
value and an accumulator (the result of the previous iteration). So, to sum all the values, it just
needs to add to the accumulator the current value.

Another useful and well-known function of the Enum module is the map function. The map
function signature is similar to reduce, but instead of aggregating the list in a single value, it
returns a transformed array:

iex(6)> Enum.map(["a", "b", "c"], fn x -> String.to_atom(x) end)

[:a, :b, :c]

Here we are transforming strings into atoms.

Another function of the Enum module is filter:

iex(7)> Enum.filter([1, 2, 3, 4, 5], fn x -> x > 2 end)


[3, 4, 5]

All these functions can be called using a more compact syntax. For example, the map function:

iex(8)> Enum.map(["a", "b", "c"], &String.to_atom/1)


[:a, :b, :c]

The &String.to_atom/1 is a way to specify which function has to be applied to the element of
the list: the function String.to_atom with arity 1. The use of this syntax is quite typical.

The List module contains functions that are more specific to linked list, like flatten, fold,
first, last, and delete.

25
Map
Maps are probably the second-most used structure for managing application data, since it is
easily resembled to an object with fields and values.

Consider a map like this:

book = %{
title: "Programming Elixir",
author: %{
first_name: "Dave",
last_name: "Thomas"
},
year: 2018
}

It is easy to view this map as a POJO/POCO; in fact, we can access its field using the well-
known syntax:

iex(2)> book[:title]
"Programming Elixir"

Actually, we cannot change the attribute of the hash map—remember that in functional
programming, values are immutable:

iex(5)> book[:title] = "Programming Java"


** (CompileError) iex:5: cannot invoke remote function Access.get/2 inside
match

To change a key value in a map, we can use the put function:

iex(6)> Map.put(book, :title, "Programming Elixir >= 1.6")


%{
author: %{first_name: "Dave", last_name: "Thomas"},
title: "Programming Elixir >= 1.6",
year: 2018
}

The Map.put function doesn't update the map, but it creates a new map with the modified key.
Maps have a special syntax for this operation, and the previous put can be rewritten like this:

26
iex(7)> new_book = %{ book | title: "Programming Elixir >= 1.6"}
%{
author: %{first_name: "Dave", last_name: "Thomas"},
title: "Programming Elixir >= 1.6",
year: 2018
}

The short syntax takes the original map and a list of attributes to change:

new_map = %{old_map | attr1: value1, attr2: value2, ...}

To read a value from a map, we already see the [] operator. The Map module has a special
function to get the value, the fetch function:

iex(7)> Map.fetch(book, :year)


{:ok, 2018}

Here, for the first time, we see a usual convention used in Elixir: the use of a tuple to return a
value from a function. Instead of returning just 2018, fetch returns a tuple with the "state" of the
operation and the result. Can a fetch fail in some way?

iex(8)> Map.fetch(book, :foo)


:error

This way of returning results as tuples is quite useful when used in conjunction with pattern
matching.

iex(9)> {:ok, y} = Map.fetch(book, :year)


{:ok, 2018}
iex(10)> y
2018

We call fetch, pattern matching the result with the tuple {:ok, y}. If it matches, in y we will
have the value 2018.

In case of error, the match fails, and we can branch to better manage the error using a case
statement (which will see later).

27
iex(11)> {:ok, y} = Map.fetch(book, :foo)
** (MatchError) no match of right hand side value: :error
(stdlib) erl_eval.erl:453: :erl_eval.expr/5
(iex) lib/iex/evaluator.ex:249: IEx.Evaluator.handle_eval/5
(iex) lib/iex/evaluator.ex:229: IEx.Evaluator.do_eval/3
(iex) lib/iex/evaluator.ex:207: IEx.Evaluator.eval/3
(iex) lib/iex/evaluator.ex:94: IEx.Evaluator.loop/1
(iex) lib/iex/evaluator.ex:24: IEx.Evaluator.init/4

Control flow
We already saw that with pattern matching, we can avoid most conditional control flows, but
there are cases in which an if is more convenient.

Elixir has some control flow statements, like if, unless, and case.

if has the classic structure of if…else:

if test_conditional do
# true case
else
# false case
end

Since everything in Elixir is an expression, and if is no exception, the if…else construct


returns a value that we can assign to a variable for later use:

a = if test_conditional do
# ...

When there are more than two cases, we can use the case statement in conjunction with
pattern matching to choose the correct option:

welcome_message = case get_language(user) do


"IT" -> "Benvenuto #{user.name}"
"ES" -> "Bienvenido #{user.name}"
"DE" -> "Willkommen #{user.name}"
_ -> "Welcome"
end

28
The last case is used when none of the previous cases match with the result. case is a sort of
switch where the _ case is the default. As with if, case returns a value; here,
welcome_message can be used.

Guards
In addition to control flow statements, there are guards that can be applied to functions,
meaning that the function will be called if the guard returns true:

defmodule Foo do
def divide_by_10(value) when value > 0 do
value / 10
end
end

The when clause added on the function signature says that this function is available only if the
passed value is greater than 0. If we pass a value that is equal to 0, we obtain a match error:

iex(4)> Foo.divide_by_10(0)
** (FunctionClauseError) no function clause matching in Foo.divide_by_10/1

The following arguments were given to Foo.divide_by_10/1:

# 1
0

iex:2: Foo.divide_by_10/1

Guards works with Boolean expressions, and even with a series of build-in functions, like:
is_string, is_atom, is_binary, is_list, is_map.

defmodule Foo do
def divide_by_10(value) when value > 0 and (is_float(value) or
is_integer(value)) do
value / 10
end
end

In this case, we are saying that the divide_by_10 function can be used with numbers greater
than 0.

29
Pipe operator
Elixir supports a special flow syntax to concatenate different functions.

Suppose, for example, that we need to filter a list to obtain only the values greater than 5, and to
these values we have to add 10, sum all the values, and finally, print the result to the terminal.

The classic way to implement it could be:

iex(14)> IO.puts Enum.reduce(Enum.map(Enum.filter([1, 3, 5, 7, 8, 9], fn x


-> x > 5 end), fn x -> x + 10 end), fn acc, x -> acc + x end)
54
:ok

Not very readable, but in functional programming, it’s quite easy to write code that composes
different functions.

Elixir gives us the pipe operator |> that can compose functions in an easy way, so that the
previous code becomes:

iex(15)> [1, 3, 5, 7, 8, 9] |> Enum.filter(fn x -> x > 5 end) |>


Enum.map(fn x -> x + 10 end) |> Enum.reduce(fn acc, x -> acc + x end) |>
IO.puts

The pipe operator gets the result of the previous computation and passes it as the first
argument to the next one. So in the first step, the list is passed as the first argument to
Enum.filter, the result is passed to the next, and so on.

This way, the code is more readable, especially if we write it like this:

[1, 3, 5, 7, 8, 9]
|> Enum.filter(fn x -> x > 5 end)
|> Enum.map(fn x -> x + 10 end)
|> Enum.reduce(fn acc, x -> acc + x end)
|> IO.puts

Type specifications
Elixir is a dynamic language, and it cannot check at compile time that a function is called with
the right arguments in terms of number, and even in terms of types.

30
But Elixir has features called specs and types that are helpful in specifying modules’ signatures
and how a type is composed. The compiler ignores these specifications, but there are tools that
can parse this information and tell us if everything matches.

These features are the @spec and @type macros.

defmodule Math do
@spec sum(integer, integer) :: integer
def sum(a, b) do
a + b
end
end

The @spec macro comes just before the function to document. In this case, it helps us
understand that the sum function receives two integers, and returns an integer.

The integer is a built-in type; you can find additional types here.

Specs are also useful for functions that return different values:

defmodule Math do
@spec div(integer, integer) :: {:ok, integer} | {:error, String.t }
def div(a, b) do
# ...
end
end

In this example, the div returns a tuple: {ok, result} or {:string, "error message"}.

But since Elixir is a dynamic language, how can the specs help in finding errors? The compiler
itself doesn't care about the specifications—we must use Dialyzer, an Erlang tool that analyzes
the specs and identifies possible issues (mainly type mismatch and non-matched cases).

Dialyzer, which came from Erlang, is a command-line tool that analyzes the source code. To
simplify the use of Dialyzer, the Elixir community has created a tool called Dialyxir that wraps
the Erlang tool and integrates it with Elixir tools.

The spec macro is usually used in conjunction with the type and struct macros that are used
to define new types:

31
defmodule Customer do
@type entity_id() :: integer()

@type t :: %Customer{id: entity_id(), first_name: String.t, last_name:


String.t}
defstruct id: 0, first_name: nil, last_name: nil
end

defmodule CustomerDao do
@type reason :: String.t
@spec get_customer(Customer.entity_id()) :: {:ok, Customer} | {:error,
reason}
def get_customer(id) do
# ...
IO.puts "GETTING CUSTOMER"
end
end

Let’s take a closer look at this code sample, starting with @type entity_id() :: integer().
This is a simple type alias; we have defined the type entity_id, which is an integer. Why have
a special type for an integer? Because entity_id is speaking from a documentation point of
view, and is contextualized since it represents an identity for a customer (it could be a primary
key or an ID number). We won’t use entity_id in another context, like sum or div.

We have a new type t (name is just a convention) to specify the shape of a customer that has
an ID: a first_name and a last_name. The syntax %Customer{ ... } is used to specify a
type that is a structure (see the next line). We can think of it as a special HashMap or a record
in other languages.

The struct is defined just after the typespec; it contains an id, a first_name, and a
last_name. To this attribute, the defstruct macro also assigns default values.

This couple of lines define the shape of a new complex type: a Customer with its attributes.
Again, we could have used a simple hash, but structs with type defines a better context, and
the core result is more readable.

After the customer module in which there is any code, we open the CustomerDao module that
uses the types defined previously.

The function get_customer receives an entity_id (an integer) and returns a tuple that
contains an atom (:ok) and a Customer struct, or a tuple with the atom :error and a reason
(String).

Adding all this metadata to our programs comes with a cost, but if we are able to start from the
beginning, and the application size grows to a certain level, it’s an investment with a high return
in value, in terms of documentation and fewer bugs.

32
Behavior and protocols
Elixir is a functional programming language that supports a different paradigm than C# or Java,
which are object-oriented programming (OOP) languages. One of the pillars of OOP is
polymorphism. Polymorphism is probably the most important and powerful feature of OOP in
terms of composition and code reuse. Functional programming languages can have
polymorphism too, and Elixir uses behavior and protocols to build polymorphic programs.

Protocols
Protocols apply to data types, and give us a way to apply a function to a type.

For example, let’s say that we want to define a protocol to specify that the types that will
implement this protocol will be printable in CSV format:

defprotocol Printable do
def to_csv(data)
end

The defprotocol macro opens the definition of a protocol; inside, we define one or more
functions with its own arguments.

It is a sort of interface contract: we can say that every data type that is printable will have an
implementation for the to_csv function.

The second part of a protocol is the implementation.

defimpl Printable, for: Map do


def to_csv(map) do
Map.keys(map)
|> Enum.map(fn k -> map[k] end)
|> Enum.join(",")
end
end

We define the implementation using the defimpl macro, and we must specify the type for which
we are writing the implementation (Map in this case). In practice, it is as if we are extending the
map type with a new to_csv function.

In this implementation, we are extracting the keys from the map (:first_name, :last_name),
and from these, we are getting the values using a map on the keys list. And finally, we are
joining the list using a comma as a separator.

33
iex(1)> c("./samples/protocols.exs")
[Printable.Map, Printable]
iex(2)> author = %{first_name: "Dave", last_name: "Thomas"}
%{first_name: "Dave", last_name: "Thomas"}
iex(3)> Printable.to_csv(author) # -> "Dave, Thomas"
"Dave,Thomas"

Note: If we save the protocol definition and protocol implementation in a script file
(.exs), we can load it in the REPL using the c function (compile). This will let us use
the module’s function defined in the script directly in the REPL.

Can we implement the same protocol for other types? Sure—let's do it for a list.

defimpl Printable, for: List do


def to_csv(list) do
Enum.map(list, fn item -> Printable.to_csv(item) end)
end
end

Here we are using the to_csv function that we have defined for the Map, since to_csv for a list
is a list of to_csv for its elements.

iex(1)> c("./samples/protocols.exs")
[Printable.List, Printable.Map, Printable]
iex(2)> author1 = %{first_name: "Dave", last_name: "Thomas"}
%{first_name: "Dave", last_name: "Thomas"}
iex(3)> author2 = %{first_name: "Kent", last_name: "Beck"}
%{first_name: "Kent", last_name: "Beck"}
iex(4)> author3 = %{first_name: "Martin", last_name: "Fowler"}
%{first_name: "Martin", last_name: "Fowler"}
iex(5)> Printable.to_csv([author1, author2, author3])
["Dave,Thomas", "Kent,Beck", "Martin,Fowler"]

In the output, we have a list of CSV strings! But what happens if we try to apply the to_csv
function to a list of numbers? Let's find out.

iex(1)> c("./samples/protocols.exs")
[Printable.List, Printable.Map, Printable]
iex(2)> Printable.to_csv([1,2,3])
** (Protocol.UndefinedError) protocol Printable not implemented for 1
samples/protocols.exs:1: Printable.impl_for!/1
samples/protocols.exs:2: Printable.to_csv/1
(elixir) lib/enum.ex:1314: Enum."-map/2-lists^map/1-0-"/2

34
The error message is telling us that Printable is not implemented for numbers, and the
runtime doesn't know what to do with to_csv(1).

We can also add an implementation for Integer if we think that we are going to need it:

defimpl Printable, for: Integer do


def to_csv(i) do
to_string(i)
end
end

iex(1)> c("./samples/protocols.exs")
[Printable.Integer, Printable.List, Printable.Map, Printable]
iex(2)> Printable.to_csv([1,2,3])
["1", "2", "3"]

Elixir has some protocols already implemented. One of the most popular is the to_string
protocol, available for almost every type. to_string returns a string interpretation of the value.

Behaviors
The other interesting feature that resembles functional polymorphism is behaviors. Behaviors
provide a way to define a set of functions that have to be implemented by a module (a contract)
and ensure that a module implements all the functions in that set.

Interfaces? Sort of. We can define a behavior by using the @callback macro and specifying the
signature of the function in terms of specs.

defmodule TalkingAnimal do
@callback say(what :: String.t) :: { :ok }
end

We are defining an "interface" for a talking animal that is able to say something. To implement
the behavior, we use another macro.

35
defmodule Cat do
@behaviour TalkingAnimal
def say(str) do
"miaooo"
end
end

defmodule Dog do
@behaviour TalkingAnimal
def say(str) do
"woff"
end
end

This resembles the classic strategy pattern. In fact, we can use functions without knowing the
real implementation.

defmodule Factory do
def get_animal() do
# can get module from configuration file
Cat
end
end

animal = Factory.get_animal()
IO.inspect animal.say("hello") # "miaooo"

If the module is marked with the @behaviour macro but the function is not implemented, the
compiler raises an error, undefined behaviour function, stating that it can't find the
declared implementation.

Behaviors and protocols are two ways to define a sort of contract between modules or types.
Always remember that Elixir is a dynamic language, and it can't be so strict like Java or C#. But
with Dialyzer, specs, behaviors, and protocols can be quite helpful in defining and respecting
contracts.

Macros
One of the most powerful features of Elixir are the macros. Macros in Elixir are language
constructs used to write code that generate new code. You might be familiar with the concept of
metaprogramming and abstract syntax trees; macros are what you need to do
metaprogramming in Elixir.

It is a difficult topic, and in this chapter, we only see a soft introduction to macros. However, you
most likely won’t need to write macros in your daily work with Elixir.

36
First of all, most of the Elixir constructs that we already used in our examples are macros: if is
defined as a macro, def is a macro, and defmodule is a macro. Actually, Elixir is a language
with very few keywords, and all the other keywords are defined as macros.

Macros, metaprogramming, and abstract syntax trees (AST) are all related. An AST is a
representation of code, and in Elixir, an AST is represented as a tuple. To view an AST, we can
use the instruction quote:

iex(1)> quote do: 4 + 5


{:+, [context: Elixir, import: Kernel], [4, 5]}

We get back a tuple that contains the function (:+), a context, and the two arguments [4,5].
This tuple represents the function that sums 4 to 5. As a tuple, it is data, but it is also code
because we can execute it:

iex(2)> Code.eval_quoted({:+, [context: Elixir, import: Kernel], [4, 5]})


{9, []}

Using the module Code, we can evaluate an AST and get back the result of the execution. This
is the basic notion we need to understand AST. Now let’s see how can we use an AST to create
a macro.

Consider the following module. It represents a Logger module with just one function to log
something to the terminal:

defmodule Logger do
defmacro log(msg) do
if is_log_enabled() do
quote do
IO.puts("> From log: #{unquote(msg)}")
end
end
end
end

The defmacro is used to start the definition of a macro; it receives a message to be logged. The
implementation checks the value of is_log_enabled function (suppose that this function will
check a setting or an environment variable), and if that value is true, it returns the AST of the
instruction IO.puts.

The unquote function is sort of the opposite of quote: since we are in a quoted context, to
access the value of msg, we need to step out of the quoted context to read that value—
unquote(msg) does exactly that.

What does this module do? This Logger logs the information only if the logging is enabled. If it
is not enabled, it doesn’t even generate the code necessary to log, meaning it does not affect
the application’s performance, since no code is generated.

37
Macros and metaprogramming are difficult topics, and they are not the focus of this book. One
of the main rules of writing macros is to not write them unless you really need to. They are
useful for writing in a DSL or doing some magical stuff, but their introduction always comes at a
cost.

38
The World's Best 

UI Component Suite 
 4.6 out of

5 stars

for Building
Powerful Apps

SHOPMART Filters John Watson


Search for something...

Dashboard Revenue by Product Cate ories g

Laptop: 56%
Orders
Online Orders offline Orders Total users

Products 23456 345 945 65 9789 95

January 2022 Customers Sales

Analytics
Sales Overview Monthly
S M T W T F S
Message
26 27 28 29 30 31 1
Accessories: 19% Mobile: 25%
2 3 4 5 6 7 8 $51,456
OTHER
9 10 11 12 13 14 15 Laptop Mobile Accessories

16 17 18 19 20 21 22 Users
23 24 25 26 27 28 29
Teams Top Sale Products
Cash
30 31 1 2 3 4 5
Setting Apple iPhone 13 Pro $999.00
$1500
Order Delivery Stats
Mobile +12.8%
100K
Completed
120 Apple Macbook Pro $1299.00 50K

In Progress
Invoices New Invoice Laptop +32.8%
25K
24
Order id Date Client name Amount Status Galaxy S22 Ultra $499.99 0
Mobile +22.8% 10 May 11 May 12 May Today
Log Out #1208 Jan 21, 2022 Olive Yew $1,534.00 Completed

Dell Inspiron 55 $899.00

G et our ree
y F .NE T nd a Java c S ript UI Components
syncfusion.com/communitylicense

1,700+ components for Support within 24 hours Uncompromising

mobile, web, and on all business days quality

desktop platforms

20+ years in

Hassle-free licensing 28000+ customers


business

Trusted by the world's leading companies


Chapter 2 The Platform

In the previous chapter we learned how Elixir works, how to use its syntax, and how to write
functions and small programs to do some basic stuff. But the real power of Elixir is the platform
itself, based on the Erlang ecosystem.

In the introduction we said that one of the most used architectures in Erlang is the actor model,
and that everything is a process. Let’s start with the process.

Spawning a process in Elixir is very easy and very cheap. They are not real operating system
processes, but processes of the virtual machine in which the Elixir application runs. This is what
gives them a light footprint, and it’s quite normal for a real-world application to spawn thousands
of processes.

Let’s begin by learning how to spawn a process to communicate with it. Consider this module:

defmodule HelloProcess do
def say(name) do
IO.puts "Hello #{name}"
end
end

This is a basic “Hello World” example that we can execute just by calling
HelloProcess.say("adam"), and it will print Hello adam. In this case, it runs in the same
process of the caller:

iex(1)> c("hello_process.exs")
iex(2)> HelloProcess.say("adam")
"Hello adam"
iex(3)>

Here we are using the module as usual, but we can spawn it in a different process and call its
functions:

iex(1)> c("hello_process.exs")
iex(2)> spawn(HelloProcess, :say, [“adam”])
Hello adam
#PID<0.124.0>

The spawn/3 function runs the say function of the module HelloProcess in a different process.
It prints Hello adam and returns a PID (process ID), in this case 0.124.0. PIDs are a central
part of the Erlang/Elixir platform because they are the identifiers for the processes.

A PID is composed of three parts: A.B.C.

39
A is the node number. We have not talked about nodes yet; consider them the machine in which
the process runs. 0 stands for the local machine, so all the PIDs that start with 0 are running on
the local machine.

B is the first part of the process number, and C is the second part of the process number
(usually 0).

Everything in Elixir has a PID, even the REPL:

iex(1)> self
#PID<0.105.0>
The self returns the PID of the current process, in this case the REPL (iex).

We can use the PID to inspect a process status using the Process module.

iex(1)> Process.alive?(self)
true

We can try with our HelloProcess module:

iex(12)> pid = spawn(HelloProcess, :say, ["adam"])


Hello adam
#PID<0.133.0>
iex(13)> Process.alive?(pid)
false

As we can see, the process is dead after the execution. This happens because there is nothing
that keeps the process alive—it simply puts the string on the console, and then terminates.

The HelloProcess module is not very useful; we need something that do some calculation that
we can spawn to another process to keep the main process free.

Let’s write it:

defmodule AsyncMath do
def sum(a, b) do
a + b
end
end

This module is very simple, but we need it to start thinking about process communication. So we
can start using this module:

40
iex(1)> c("async_math.exs")
[AsyncMath]
iex(2)> pid = spawn(AsyncMath, :sum, [1,3])
#PID<0.115.0>
iex(3)> Process.alive?(pid)
False

As we can see, it simply returns the PID of the spawned process. In addition, the process dies
after the execution, so that we cannot obtain the result of the operation.

To make the two processes communicate, we need to introduce two new instructions: receive
and send. Receive is a blocking operation that suspends the process waiting for new
messages. Messages are the way process communicates: we can send a message to a
process, and it can respond by sending a message.

We can refactor our module like this:

defmodule AsyncMath do
def start() do
receive do
{:sum, [x, y], pid} ->
send pid, {:result, x + y}
end
end
end

We have defined a start function that is the entry point for this module; we will use this
function to spawn the process.

Inside the start function, we wait for a message using the receive do structure. Inside
receive, we expect to receive a message (a tuple) with this format:

{:sum, [x, y], pid}

The format consists of an atom (:sum), an array with an argument for sum, and the pid of the
sender. We pattern match on this and respond to the caller using a send instruction.

send/2 needs the pid of the process to send a message: a tuple with :result, and the result
(sum of x + y).

If everything is set up correctly, we can load the new module and try it in the REPL:

41
iex(1)> c("async_math.exs")
[AsyncMath]
iex(2)> pid = spawn(AsyncMath, :start, [])
#PID<0.151.0>
iex(3)> Process.alive?(pid)
true
iex(4)> send(pid, {:sum, [1, 3], self})
{:sum, [1, 3], #PID<0.105.0>}
iex(5)> Process.alive?(pid)
false

What have we done here? We loaded the async_math module and spawned the process using
the spawn function with the start function of the module.

Now the module is alive because it is waiting for a message (receive…do). We send a message
requesting the sum of 1 and 3. The send function returns the sent message, but not the result.
In addition to this, the process after the send is dead.

How can we get our result?

One thing I have not yet mentioned is that every process in Elixir has an inbox, a sort of queue
in which all of its messages arrive. From that queue, the process dequeues one message at a
time, processes it, and then goes to the next one. That’s why I said that inside a process, the
code is single thread/single process, because it works on a single message at a time.

This mechanism is also at the base of the actor model, in which each actor has a dedicated
queue that stores the messages to process, and an actor works on a single time.

Going back to our example, the queue that stores the messages is the queue of the REPLS,
since it is that process that asks for the sum of 1 and 3. We can see what’s inside the process
queue by calling the function flush, which flushes the inbox and prints the messages to the
console:

iex(6)> flush
{:result, 4}

Here is our expected result: flush prints the messages in the queue (in this case, just one).
The message has the exact shape that we used to send the result.

Now that we have asked for a result, we can try to ask for another operation:

iex(6)> send(pid, {:sum, [5, 8], self})


iex(7)> flush
:ok

42
This time the inbox is empty: it seems like our request or the response to our request has gotten
lost. The problem is that the AsyncMath.start function wait for the first message, but as soon
as the first message is processed, it goes out of scope. The receive do macro does not loop
to itself after a message is received.

To obtain the desired result, we must do a recursive call at the end of the start function:

defmodule AsyncMath do
def start() do
receive do
{:sum, [x, y], pid} ->
send pid, {:result, x + y}
end
start
end
end

At the end of the receive block, we do a recursive call to start so that the process will go in a
“waiting for message” mode.

With this change, we can call the sum operation anytime we want:

iex(1)> c("async_math.exs")
[AsyncMath]
iex(2)> pid = spawn(AsyncMath, :start, [])
#PID<0.126.0>
iex(3)> send(pid, {:sum, [5, 4], self})
{:sum, [5, 4], #PID<0.105.0>}
iex(4)> send(pid, {:sum, [3, 9], self})
{:sum, [3, 9], #PID<0.105.0>}
iex(5)> flush
{:result, 9}
{:result, 12}
:ok
iex(6)>

When we call the flush function, it prints out the two messages in the inbox with the two
results. This happens because the recursive call to start keeps the process ready to receive
new messages.

We have seen a lot of new concepts about Elixir and processes.


To recap:
• We created basic module that, when started, waits for a message and responds with
another message
• We spawned that function to another process
• We sent a message using the send function
• We flushed the inbox of the REPL to view the result

43
Is there a better way to capture the result? Yes, by using the same pattern of the AsyncMath
module.

defmodule AsyncMath do
def start() do
receive do
{:sum, [x, y], pid} ->
send pid, {:result, x + y}
end
start()
end
end

pid = spawn(AsyncMath, :start, [])


send pid, {:sum, [5, 6], self()}

receive do
{:result, x} -> IO.puts x
end

We can put our program in waiting even after the execution of the sum operation—remember
that the messages remain in the inbox queue, so we can process them after they arrive (unlike
with events).

Now we have seen the basics of processes. We also have seen that even if it is cheap to spawn
a process, in a real-world application it’s not very feasible create processes and communicate
with them using the low-level function that we have seen. We need something more structured
and ready to use.

With Elixir and Erlang comes the OTP (Open Telecom Platform), a set of facilities and building
blocks for real-world applications. Even though Telecom is in the name, it is not specific to
telecommunications— it’s more of a development environment for concurrent applications. OTP
was built with Erlang (and in Erlang), but thanks to the complete interoperability between Erlang
and Elixir, we can use all of the facilities of OTP in our Elixir program with no cost at all.

Elixir applications
Until now, we’ve worked with simple Elixir script files (.exs). These are useful in simple contexts,
but not applicable in real-world applications.
When we installed Elixir, we also got mix, a command-line tool used to create and manipulate
Elixir projects. We can consider mix as a sort of npm (from Node.js). From now on, we will use
mix to create projects and manage projects.

Let’s create a new project now:

44
~/dev> mix new sample_app
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/sample_app.ex
* creating test
* creating test/test_helper.exs
* creating test/sample_app_test.exs

Your Mix project was created successfully.


You can use "mix" to compile it, test it, and more:

cd sample_app
mix test

Run "mix help" for more commands.

This CLI command creates a new folder named sample_app and puts some files and folders
inside.

We’ll now have a quick look at some of these files.

45
Mix.exs

defmodule SampleApp.MixProject do
use Mix.Project

def project do
[
app: :sample_app,
version: "0.1.0",
elixir: "~> 1.8",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.


def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.


defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git",
tag: "0.1.0"},
]
end
end

The mix file acts as a project file (a sort of package.json). It contains the app manifest, the
application to launch, and the list of dependencies. Don’t worry if everything is not clear right
now—we will learn more as we go.

There are two important things to note here. First is the deps function that returns a list of
external dependencies from which the application depends. Dependencies are specified as a
tuple with the name of the package (in the form of an atom) and a string that represents the
version.

The other important thing is the application function that we will use to specify with module
should start at the beginning. In this template, there is only a logger.

Remember that comments start with a number sign (#) character.

46
Sample_app.ex

defmodule SampleApp do
@moduledoc """
Documentation for SampleApp.
"""

@doc """
Hello world.

## Examples

iex> SampleApp.hello()
:world

"""
def hello do
:world
end
end

Sample_app.ex is the main file of this project, and by default consists only of a function that
returns the atom :world. It’s not useful; it’s just a placeholder.

Sample_app_test.exs

defmodule SampleAppTest do
use ExUnit.Case
doctest SampleApp

test "greets the world" do


assert SampleApp.hello() == :world
end
end

This is a template for a simple test of the function SampleApp.hello. It uses ExUnit as a test
framework. To run the tests from the terminal, we must write mix test:

47
~/dev> mix test
Compiling 1 file (.ex)
Generated sample_app app
..

Finished in 0.04 seconds


1 doctest, 1 test, 0 failures

Randomized with seed 876926

The other files are not important right now; we will look more closely at some of them in the
upcoming chapters.

To start the application, we must use mix from the terminal:

~/dev> mix run


Compiling 1 file (.ex)
Generated sample_app app

Actually, the application does nothing.

GenServer
One of the most frequently used modules of OTP is the GenServer that represents a basic
generic server, a process that lives by its own and is able to process messages and response to
action.

GenServer is a behavior that we can decide to implement to adhere to the protocol. If we do it,
we obtain a server process that can receive, process, and respond to messages.

GenServer has the following capabilities:

• Creating a server process


• Managing the server state
• Creating a server process
• Manage the server state
• Handling requests and sending responses
• Stopping the server
• Handling failures

It is a behavior, so the implementation details are up to us. GenServer lets us implement some
functions (called callbacks) for customizing its details:

• init/1 acts as a constructor, and is called when the server is started. The expected
result is a tuple {:ok, state} that contains the initial state of the server.
• handle_call/3 is the callback that is called when a message arrives to the server.
This is a synchronous function, and the result is a tuple {:reply, response,

48

You might also like