left-icon

Elixir Succinctly®
by Emanuele DelBono

Previous
Chapter

of
A
A
A

CHAPTER 2

The Language

The Language


Now that Elixir is installed, we can start using it to explore its capabilities. Elixir is a functional language with immutable data structures and no concepts like objects, classes, methods, or other constructs of object-oriented programing (OOP). Everything is a function or a data structure that can be manipulated by a function. Functions are grouped in modules.

The easiest way to start is to type iex in a terminal. This command starts a REPL console in which you can start exploring the language.

Basic types

Elixir is dynamically typed, and the basic types are: numbers, Booleans, strings, atoms, lists, and tuples.

We can play with these types inside the REPL:

Erlang/OTP 21 [erts-10.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> 42

42

iex(2)>

nil

iex(3)>

Since Elixir is a functional language, every expression must return a value, so if you type 42, the result of the expression is 42. If you just type Enter, the result of the expression is nil (null value).

With numbers, we can do arithmetic:

iex(3)> 73 + 45

118

We can also evaluate logical operations:

iex(4)> 42 == 44

false

iex(5)> true && false

false

Atoms are constant values, a sort of tag or label that never changes values. They can be compared to symbols in Ruby:

iex(6)> :an_atom

Atoms are preceded by a semicolon. The convention is to use snake_case.

Lists are just a series of values contained in square brackets:

iex(9)> [1, "hello", :foo_bar]

[1, "hello", :foo_bar]

Remember that, unlike with other programming languages (such as JavaScript), lists have no methods—they are just data structures. To work and manipulate lists, we must use the modules List or Enum.

The last basic type is the tuple:

iex(9)> {1, "hello", :foo_bar}

{1, "hello", :foo_bar}

As we can see, tuples are very similar to lists: the difference is that a list is stored as a linked list, while tuples are stored in a contiguous memory space, so updating a tuple is more expensive.

In Elixir tuples are often used as a return value. A classic way to return a value from a function is to use a tuple with two values: an atom ( :ok or :error), and the value:

{:ok, 42}

{:error, "cannot calculate value"}

In addition to these basic types in Elixir, we can use more structured types, like maps. Maps are complex structures composed of keys and values:

iex(11)> %{name: "emanuele", country: "italy"}

%{country: "italy", name: "emanuele"}

Maps can also be nested to build more complex structures.

iex(12)> %{name: "emanuele", address: %{street: "via branze", city: "brescia"}}

%{address: %{city: "brescia", street: "via branze"}, name: "emanuele"}

We’ll look further into these types later in this chapter.

Strings

The string type needs a special mention because strings in Elixir are binaries, meaning a list of bytes. Binaries in Elixir are built using this syntax:

iex(1)> list_of_bytes = <<97, 98, 99, 100>>

"abcd"

As we can see, the REPL shows that list of bytes as a string "abcd" since 97, 98, 99, and 100 are the code points of the characters a, b, c, and d.

This means the following operation matches correctly and returns the right part:

iex(2)> <<97, 98, 99, 100>> = "abcd"

Elixir, like most programming languages, supports string interpolation using the Ruby syntax:

"#{:hello_atom} elixir"

Strings can be concatenated by using a special operator:

iex(3)> "hello" <> "elixir"

"helloelixir"

String is also a module (see next chapter) that contains a set of useful functions for working with strings.

Modules and functions

I’ve already mentioned that Elixir is a functional language. The types presented so far can be used to build complex structures that represent our data, but to manipulate them, we need a set of functions, since we don't have objects, classes, etc.

Like many languages (such as Python and Perl), Elixir statements can be placed in a file and executed. Simply use any text editor, type Elixir statements, and save the file with a .exs file extension ("Elixir script"). Then, run the file from a shell by typing elixir followed by the file name. For example:

> elixir say_hello.exs
"hello Elixir"

To better organize code in real-world applications, Elixir functions can be defined along a module that acts as a sort of container (namespace) for the functions that it contains.

Let's see an example:

defmodule MyFunctions do

  # functions here

end

To create a module, we use the defmodule macro, whose name we will use to call the function that it contains.

defmodule MyFunctions do

  def sum(a, b) do

    a + b

  end

  def sub(a, b) do

    a - b

  end

end

Here we have defined a module (MyFunctions) that contains a couple of functions: sum and sub, each with two arguments (arity).

Note: Arity is an important aspect for Elixir functions. A function in Elixir is determined by its name and arity (the number of arguments it takes). That means that sum/2 is different from sum/3. One of the message errors that appears quite often is: ** (UndefinedFunctionError) function MyFunctions.sum/3 is undefined or private. Did you mean one of  * sum/2?

The implementation of these functions is straightforward; it simply applies the operation onto the argument. Functions don't need an explicit return; the last expression is the return value.

To call these functions, we have to use the full name:

MyFunctions.sum(4, 7)

Note: The parentheses are not mandatory in Elixir; we can still call the function when omitting them: MyFunctions.sum 4, 7.

Functions are the building block of Elixir applications. Applications are subdivided into modules to group functions. The functions in a group can be called using the "full name" (ModuleName.function_name), but since this can be awkward, there are other macros that help.

Since Elixir is a functional programming language, functions are first-class citizens in the world of Elixir. This usually means that we can treat functions as values to pass around. For example, we can have a function that receives a function as argument.

def print_result(f) do

  IO.puts f.()

end

Note: We omitted the module for brevity.

When the function is very short (a one-line-function), a compressed syntax can be used to define the function:

def sum(a, b), do: a + b

This is the same as def with do end, but written with the special one-line syntax. With functional programming languages, the number of “mini-functions” is very high, and using this syntax to define them is quite common.

The print_result function receives a function as an argument. In its body, it executes the function (it is a function with no parameters) and puts the result to the terminal. (IO is the input/output core module. Consider IO.put like a console.log in JavaScript).

The .() is the way to call functions that are defined like values.

How can we use this print_result function? To use it, we have to create a function and pass it to print_result. The idiomatic way to do this in Elixir is:

a = fn -> 42 end

print_result(a)

The fn keyword is used to define an anonymous function. The previous example shows a simple function that returns 42. The arity of this function is 0, and we match the function with the variable a so that we can pass it to the print_result function.

The fn syntax to define anonymous functions is quite useful for defining simple one-line functions that are needed inside the scope. With fn we can also define functions that receive parameters:

iex(1)> sum = fn (a, b) -> a + b end

#Function<12.128620087/2 in :erl_eval.expr/5>

iex(2)> sum.(4, 5)

9

After the function declaration, the REPL shows the returned value that is a Function type.

Import

In real-world applications, functions from one module are often in another module. To avoid the use of the full name, we can use the import macro:

defmodule MyFunctions do

  def sum(a, b) do

    a + b

  end

  def sub(a, b) do

    a - b

  end

end

defmodule DoSomeMath do

  import MyFunctions

  def add_and_subtract(a, b, c) do

    sub(sum(a, b), c)

  end

end

As you can see, we are using sum and sub without their full names. This is thanks to the import directive: we are saying to the compiler that all functions from module MyFunctions are available in the module DoSomeMath.

Imports can be more selective. Some modules are quite big, and sometimes we only need one or two functions. The import macro can be used with additional arguments that specify which function should be imported:

import MyFunctions, only: [sum: 2]

The atom only specifies that we want only sum/2 functions.

Note: Functions are identified in Elixir by name/arity. In the previous example, we are saying that we want function sum with two arguments.

Pattern matching

One great (and possibly the best) feature of Elixir is pattern matching. We already said that Elixir is a functional language. In functional programming values are immutable, which means that this code should not work:

iex(1)> a = 1

1

iex(2)> a = 2

2

If you try iex, this code actually works, so it seems that variables are mutable. But not quite.

In Elixir, the = character is not an assignment operator, but a match operator. In the previous example, we are binding the variable a to the right side of =, the first time with 1, the and second time (rebind) with 2.

What really happens is:

  1. The expression on the right side is evaluated (1 or 2)
  2. The resulting value is matched against the left side pattern (a)
  3. If it matches the variable on the left side, it is bounded to the right-side value

This means we can write the following code:

iex(1)> a = 3

3

iex(2)> 3 = a

3

The second line is pattern matching in action; we know that a is bounded to 3 (the first statement), so 3 = a matches, and the value on the right-hand side is returned.

It could happen that the values don't match:

iex(1)> a = 3

iex(2)> 7 = a

** (MatchError) no match of right hand side value: 3

In this case Elixir returns a match error, as we expected.

There is one last thing to know about pattern matching: the pin operator. In some cases, instead of rebounding a variable to a new value, we want to verify the match. The pin operator (^) can help in these cases:

iex(1)> a = 3

iex(2)> ^a = 3

iex(3)> ^a = 7

** (MatchError) no match of right hand side value: 7

Using the pin operator, we are not going to rebind the variable—we are checking if the actual bound matches against the right-hand side.

Pattern matching is powerful, and is used a lot in everyday programming with Elixir. One example is using it to avoid conditionals:

defmodule Bot do

  def greet("") do

    IO.puts "None to greet."

  end

 

  def greet(name) do

    IO.puts "Hello #{name}"

  end

end

The Bot module has two greet functions: the first uses pattern matching to match against an empty string, and the second is matched in the other cases. This means we can avoid checking the value of name when deciding what to print.

Pay attention to the order in which the functions are declared—they are evaluated from top to bottom.

Pattern matching can be used to decompose lists:

iex(1)> [head | tail] = [1, 2, 3, 4]

[1, 2, 3, 4]

iex(2)> head

1

iex(3)> tail

[2, 3, 4]

In this example, the right side is a list with four values, and the left side is a couple of variables that are going to be bound to the head (1) and the tail (2,3,4) of the list.

We can also match the single elements:

iex(4)> [a, b, c] = [1, 2, 3]

[1, 2, 3]

iex(5)> a

1

iex(6)> b

2

In this case it’s important to match all the elements in the list. If we miss one, the match fails:

iex(4)> [a, b] = [1, 2, 3]

** (MatchError) no match of right hand side value: [1, 2, 3]

If we don't care about an element in the list, we can use the underscore character (_) as a match:

iex(15)> [a,b, _] = [1,2,3]

[1, 2, 3]

The _ means that I don't care about the value and don't capture it, so 3 is not bounded to a variable.

Since it works on lists, it can work on hash maps, too:

iex(16)> %{a: a, b: b} = %{a: 4, b: "hello", c: :foo}

%{a: 4, b: "hello", c: :foo}

iex(17)> a

4

iex(18)> b

"hello"

In this example, we are matching the keys a and b against the map on the right. The values of a and b are bounded in the variables a and b.

Recursion

Recursion is not strictly tied to Elixir; is a general programming concept, but it is a pattern that is used quite often in functional programming. Elixir is no exception, and pattern matching is another peculiarity that helps in writing recursive programs.

Consider the example of Factorial. Elixir does not have loop statements like for or while; there are some functions in the Enum module, but none is a real loop. Recursion is a way to emulate loops.

defmodule Factorial do

  def do_it(0) do

    1

  end

  def do_it(n) do

    n * do_it(n - 1)

  end

end

Programs written with recursion always have the same pattern:

  1. Determine and implement the basic case
  2. Determine and implement the general case

In Factorial, the basic case is when the number is 0, in which the Factorial is 1. The first do_it function does exactly that.

In general, we multiply n for the Factorial of n-1.

Tail-call optimization

Coming from non-functional languages, it could be a little scary writing recursive functions like Factorial. We might think that the stack will be filled with returning addresses.

Elixir can manage this issue using tail-call optimization: if the very last thing the function does is the recursive call, the runtime can optimize the stack without adding a new frame, using the result as the new parameter for the next call. So if the condition is satisfied, there’s no need to get worried about stack overflows.

Is the previous Factorial function optimized for tail calls? No! Even if the recursive call is the last instruction, it’s not the last thing the function does. The last thing the function does is to multiply n for the factorial of n-1. So, to make the Factorial function tail-optimized, we must do a little bit of refactoring:

defmodule Factorial do

  def do_it(n) do

    internal_do_it(n, 1)

  end

 

  defp internal_do_it(0, acc) do

    acc

  end

  defp internal_do_it(n, acc) do

    internal_do_it(n - 1, acc * n)

  end

end

Moving the multiplication inside the call to internal_do_it resolves the optimization problem, and the recursive call is now the last thing the function does.

Take special care when writing recursive functions, since the stack overflow problem can be a subtle error to find.

Helpful modules

The Elixir core library comes with lots of helpful modules that accomplish most of the basic functionalities needed. To use them, you can start with the documentation and read through the modules to find something suitable for your needs.

Since these modules are in the standard library, you don't need to explicitly import them.

List and Enum

List and Enum are two modules that contain functions for manipulating enumerable lists of things. Lists in Elixir are linked lists, and you can think of them as recursive structures, like this:

iex(1)> [1 | [ 2 | [3 | [ 4 ]]]]

[1, 2, 3, 4]

List are composed of pairs (Lisp uses the same approach), and can be concatenated using the special operator ++:

iex(3)> [1, 3, 5] ++ [2, 4]

[1, 3, 5, 2, 4]

Lists can also be subtracted:

iex(4)> [1, 2, 3, 4, 5] -- [1, 3, 5]

[2, 4]

We already see that we can pattern-match on a list to extract the head:

iex(5)> [head | tail] = [1, 2, 3, 4]

[1, 2, 3, 4]

iex(6)> head

1

iex(7)> tail

[2, 3, 4]

This operator is very useful when writing recursive functions. Consider the following function, which sums up the elements in the list.

defmodule ListUtils do

  def sum([]) do

    0

  end

  def sum([h | t]) do

    h + sum(t)

  end

end

There are two sum functions: the first is called with an empty list, and the sum of an empty list is 0. The second is called when the list is not empty. The list is decomposed in head and tail, and a recursive call is executed to add the value of the head to the sum of the rest of the list.

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.

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:

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).

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

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.

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.

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:

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.

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.

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

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.

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.

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.

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.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.