Book Image

Mastering Elixir

By : André Albuquerque, Daniel Caixinha
Book Image

Mastering Elixir

By: André Albuquerque, Daniel Caixinha

Overview of this book

Running concurrent, fault-tolerant applications that scale is a very demanding responsibility. After learning the abstractions that Elixir gives us, developers are able to build such applications with inconceivable low effort. There is a big gap between playing around with Elixir and running it in production, serving live requests. This book will help you fll this gap by going into detail on several aspects of how Elixir works and showing concrete examples of how to apply the concepts learned to a fully ?edged application. In this book, you will learn how to build a rock-solid application, beginning by using Mix to create a new project. Then you will learn how the use of Erlang's OTP, along with the Elixir abstractions that run on top of it (such as GenServer and GenStage), that allow you to build applications that are easy to parallelize and distribute. You will also master supervisors (and supervision trees), and comprehend how they are the basis for building fault-tolerant applications. Then you will use Phoenix to create a web interface for your application. Upon fnishing implementation, you will learn how to take your application to the cloud, using Kubernetes to automatically deploy, scale, and manage it. Last, but not least, you will keep your peace of mind by learning how to thoroughly test and then monitor your application.
Table of Contents (18 chapters)
Title Page
Dedication
Packt Upsell
Contributors
Preface
5
Demand-Driven Processing
Index

Typespecs and behaviours


As already mentioned in the beginning of this chapter, Elixir is a dynamic programming language. As such, we don't declare the type of each variable, as it depends on the value each variable is bound to at each moment.

Usually dynamic programming languages yield higher productivity, as programmers don't need to declare types and can focus on developing the logic of an application. However, this comes at a cost: Certain errors, which in statically-typed languages would be caught at compile-time, may only be caught at runtime in dynamic languages. The time saved by using a dynamic language is then used (often in excess) on debugging in production.

We're not advocating for statically-typed languages–this book is about Elixir, after all. But what if you could have the best of both worlds?

It turns out you can! Type specifications, or typespecs, are a mechanism to annotate function signatures with the respective types (arguments and return values). Typespecs are also used to create custom data types. Let's explore them in more detail, before jumping into behaviours.

Typespecs

Typespecs are written using the @spec module directive, followed by function_name(argument_type) :: return_type. This module directive is placed right before the definition of the function we're annotating.

To demonstrate how to apply typespecs to a function, let's bring back our palindrome? function:

$ cat examples/string_helper.ex
defmodule StringHelper do
  def palindrome?(term) do
    String.reverse(term) == term
  end
end

Given that the module name is StringHelper, and that it's using functions from the String module, we can see that this function receives a string. As it uses the == operator, and also hinted at by the trailing ? on the function name, we know this function returns a Boolean. With this information, writing the typespec for this function is straightforward:

$ cat examples/string_helper_palindrome_with_typespec.ex
defmodule StringHelper do
  @spec palindrome?(String.t) :: boolean
  def palindrome?(term) do
    String.reverse(term) == term
  end
end

You can also define your own types to use in typespecs, by using the @type directive within a module (usually the module where that type is used the most). Let's say that you frequently use a tuple in your module that contains the name of a country on the first element, and its population on the second, as in {"Portugal, 10_309_573}. You can create a type for this data structure with this:

@type country_with_population :: {String.t, integer}

Then, you could use country_with_population in typespecs as you'd use any other type.

Defining a type in a module will export it, making it available to all modules. If you want your type to be private to the module where it's defined, use the @typep directive. Similar to the @moduledoc and @doc directives (to document modules and functions, respectively), you can use the @typedoc directive to provide documentation for a certain type.

Note

In typespecs, the string type is usually represented by String.t. You can also use string but the Elixir community discourages this, with the goal of avoiding confusion with the charlist type, which represents strings in Erlang. If you use string in typespecs, the compiler will emit a warning.

Typespecs also provide great documentation, as we can quickly grasp what types this function accepts and returns. This example only shows a subset of the types you can use–please refer to the official documentation to get a full list, at

https://hexdocs.pm/elixir/typespecs.html.

Dialyzer

Dialyzer (http://erlang.org/doc/man/dialyzer.html) is a tool that ships with Erlang and performs static analysis of code. It analyses compiled .beam files, making it available for all programming languages that run on the Erlang VM (such as Elixir!). While Dialyzer can be helpful on projects that don't have typespecs (as it can, for instance, find redundant code), its power is maximized on projects that have their functions annotated with typespecs. This way, Dialyzer is able to report on typing errors, which brings you closer to the security you can get on a statically-typed language.

Although we won't be exploring Dialyzer in this book, we highly recommend its usage, as it can be very helpful. Particularly, we feel that the Dialyxir library (https://github.com/jeremyjh/dialyxir), is a great way to integrate Dialyzer into Elixir projects, as it abstracts away part of the complexity of dealing with Dialyzer directly.

Behaviours

Behaviours provide a way to describe a set of functions that have to be implemented by a module, while also ensuring that the module implements the functions in that set. If you come from an object-oriented programming background, you can think of behaviours as abstract base classes that define interfaces. After declaring the behaviour, we can then create other modules that adopt this behaviour. A behaviour creates an explicit contract, which states what the modules that adopt the behaviour need to implement. This way, we can have our business logic tied to abstractions (the behaviour) instead of concrete implementations. We can swap two implementations of the same behaviour with very little and localized change in our application–the place where we define the implementation of the behaviour we're going to use.

Let's demonstrate this concept with an example. We'll define a behaviour, called Presenter, that has only one function: present. We'll then define a module that adopts this behaviour, called CLIPresenter. We could also have other modules that would adopt this behaviour, such as a GraphicalPresenter. A behaviour is created using the @callback directive inside a module, providing a typespec for each function this behaviour contains. Thus, to define our Presenter behaviour, we use the following:

$ cat examples/presenter.ex
defmodule Presenter do
  @callback present(String.t) :: atom
end

And now we define the module that will adopt this behaviour:

$ cat examples/cli_presenter.ex
defmodule CLIPresenter do
 @behaviour Presenter

 @impl true
 def present(text) do
 IO.puts(text)
 end
end

We can see this module working in the next snippet:

iex> CLIPresenter.present("printing text to the command line")
printing text to the command line
:ok

From Elixir 1.5 onwards, we may use the @impl directive to mark the functions that are implemented as callbacks for a behaviour. In our case, we'd put the @impl directive on top of the present function:

$ cat examples/cli_presenter_with_impl_annotation.ex
defmodule CLIPresenter do
  @behaviour Presenter

  @impl true
  def present(text) do
    IO.puts(text)
  end
end

We can be even more specific and use @impl Presenter, to state that this function is a callback from the Presenter behaviour. This brings two major advantages:

  • Increased readability, as it's now explicit which functions make up part of our API and which functions are callback implementations.
  • Greater consistency, as the Elixir compiler will check whether the functions you're marking with @impl are part of a behaviour your module is adopting.

Note that when you set @impl on a module, you must set it on all callback functions on that module, otherwise a warning will be issued. Furthermore, you can only use @impl on functions that are callbacks, otherwise a compilation warning will be issued as well.

Note

You can use @optional_callbacks to mark one or more functions as optional when adopting a certain behaviour. You need to provide a keyword list, with function names as keys, and arity as the value. If we wanted our present/1 function to be optional in the Presenter behaviour, we would use:$ cat examples/presenter_with_optional_callbacks.exdefmodule Presenter do  @callback present(String.t) :: atom  @optional_callbacks present: 1end

If we didn't implement all of the functions declared in the Presenter behaviour (by commenting the CLIPresenter.present/1 function, for instance), the Elixir compiler would emit the following warning:

warning: undefined behaviour function present/1 (for behaviour Presenter)

Behaviours help us follow the open/closed principle, since we can extend our system without modifying what we already have. If, in the future, we would need a new type of presenter, we'd just have to create a new module that adopts the Presenter behaviour, without having to change the behaviour or its current implementations. We'll be using behaviours in the application we'll build in this book, and you'll see them in action again in the next chapter.