Elixir Succinctly Páginas 2
Elixir Succinctly Páginas 2
Elixir Succinctly Páginas 2
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:
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:
All these functions can be called using a more compact syntax. For example, the map function:
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.
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:
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:
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:
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?
This way of returning results as tuples is quite useful when used in conjunction with pattern
matching.
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 test_conditional do
# true case
else
# false case
end
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:
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
# 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.
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:
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.
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()
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.
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.
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:
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:
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:
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
Laptop: 56%
Orders
Online Orders offline Orders Total users
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
G et our ree
y F .NE T nd a Java c S ript UI Components
syncfusion.com/communitylicense
desktop platforms
20+ years in
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.
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).
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
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.
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.
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:
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.
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:
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.
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
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.
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
cd sample_app
mix test
This CLI command creates a new folder named sample_app and puts some files and folders
inside.
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
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.
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
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
..
The other files are not important right now; we will look more closely at some of them in the
upcoming chapters.
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.
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