Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Mastering Elixir

You're reading from   Mastering Elixir Build and scale concurrent, distributed, and fault-tolerant applications

Arrow left icon
Product type Paperback
Published in Jul 2018
Publisher Packt
ISBN-13 9781788472678
Length 574 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
André Albuquerque André Albuquerque
Author Profile Icon André Albuquerque
André Albuquerque
Daniel Caixinha Daniel Caixinha
Author Profile Icon Daniel Caixinha
Daniel Caixinha
Arrow right icon
View More author details
Toc

Table of Contents (13) Chapters Close

Preface 1. Preparing for the Journey Ahead 2. Innards of an Elixir Project FREE CHAPTER 3. Processes – The Bedrock of Concurrency and Fault Tolerance 4. Powered by Erlang/OTP 5. Demand-Driven Processing 6. Metaprogramming – Code That Writes Itself 7. Persisting Data Using Ecto 8. Phoenix – A Flying Web Framework 9. Finding Zen through Testing 10. Deploying to the Cloud 11. Keeping an Eye on Your Processes 12. Other Books You May Enjoy

Functions and Modules

Despite not being mentioned in the data types section, functions in Elixir are a type as wellin fact, they are a first-class citizen, as they can be assigned to a variable and passed as arguments to other functions.

As with most functional programming languages, functions are an important type, hence they justify having their own section, away from other built-in types.

We will start by exploring anonymous functions, followed by an explanation of modules and named functions, and then we'll end this section with a quick tour of module attributes and directives.

Anonymous functions

Anonymous functions, usually called lambdas, are created with the fn keyword, as we can see in the following example:

iex> plus_one = fn (x) -> x + 1 end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex> plus_one.(10)
11

Here, we are defining a function that takes one argument, which we've named x, and simply adds one to the provided argument. We then bind this anonymous function to a variable named plus_one, and execute it with 10 as the argument, using the syntax we can see in the preceding snippet. As expected, we get 11 back.

There is no return keyword in Elixirthe return value of a function is the value returned by its last expression.

An anonymous function can also have multiple implementations, depending on the value and/or type of the arguments provided. Let's see this in action with an example:

iex> division = fn
...> (_dividend, 0) -> :infinity
...> (dividend, divisor) -> dividend / divisor
...> end
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> division.(10, 2)
5.0
iex> division.(10, 0)
:infinity

Imagine that we want a special division function, that, instead of raising ArithmeticError when dividing by 0, would just return the :infinity atom. This is what the anonymous function we see here achieves. Using pattern matching, we say that when the second argument (the divisor) is 0, we simply return :infinity. Otherwise, we just use the / arithmetic operator to perform a normal division.

Aside from the lambda with multiple bodies, notice that we prefix the unused variable with an underscore (_), as in _dividend. Besides increasing the readability of your code, following this practice will make the Elixir compiler warn you when you use a supposedly unused variable. Conversely, if you don't use a certain variable but don't prefix it with an underscore, the compiler will also warn you.

Parentheses around arguments of an anonymous function are optional. You could write the plus_one function we introduced earlier as fn x -> x + 1 end.

Beyond accepting arguments, anonymous functions can also access variables from the outer scope:

iex> x = 3
3
iex> some_fun = fn -> "variable x is #{x}" end
#Function<20.99386804/0 in :erl_eval.expr/5>
iex> some_fun.()
"variable x is 3"
iex> x = 5
5
iex> some_fun.()
"variable x is 3"

As you can see, our anonymous function can access variables from the outer scope. Furthermore, the variable can be bound to another value, and our function will still hold a reference to the value that the variable had when the anonymous function was defined. This is usually called a closure: the function captures the memory locations of all variables used within it. Since every type in Elixir is immutable, that value residing on each memory reference will not change. However, this also means that these memory locations can't be immediately garbage-collected, as the lambda is still holding references to them.

We'll end this section on anonymous functions by introducing a new operatorthe capture operator (represented by &).

This operator allows you to define lambdas in a more compact way:

iex> plus_one = &(&1 + 1)
#Function<6.99386804/1 in :erl_eval.expr/5>
iex> plus_one.(10)
11

This syntax is equivalent to the one presented before for the plus_one function. &1 represents the first argument of this lambda function—and, more generally, &n will represent the nth argument of the function. Similar to what happens in the fn notation, the parentheses are optional. However, it's better to use them, as in a real-world application, these lambda functions become hard to read without them.

Besides providing a shorter way to define lambda functions, the capture operator can also be used with named functions. We'll explore this further in the next section.

Modules and Named Functions

In Elixir, modules group functions together, much like a namespace. Usually, functions that reside in the same module are related to one another. You create a module using the defmodule construct:

iex> defmodule StringHelper do
...> def palindrome?(term) do
...> String.reverse(term) == term
...> end
...> end
{:module, StringHelper,
<<70, 79, 82, 49, 0, 0, 4, 0, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 119, 0,
0, 0, 11, 19, 69, 108, 105, 120, 105, 114, 46, 83, 116, 114, 105, 110, 103,
72, 101, 108, 112, 101, 114, 8, 95, 95, ...>>, {:palindrome?, 1}}
iex> StringHelper.palindrome?("abcd")
false
iex> StringHelper.palindrome?("abba")
true

In the preceding example we're also creating a function inside the StringHelper module, using the def construct, that checks whether a given string is a palindrome. This is a named function, and contrary to the anonymous functions, must be created inside a module.

Function names, like variable names, start with a lowercase letter, and if they contain more than one word, they are separated by underscore(s). They may end in ! and ?. The convention in the Elixir community is that function names ending in ! denote that the function may raise an error, whereas function names ending in ? indicate that that function either returns true or falsewhich is the case of our palindrome? function.

Note that unlike anonymous functions, we don't need to put a dot between the function name and the parenthesis when calling named functions. This is deliberate and serves to explicitly differentiate calls to anonymous and named functions.

As the implementation of our palindrome? function is very small, we can inline it with the following syntax:

def palindrome?(term), do: String.reverse(term) == term

This works with other constructs that use the do ... end syntax, such as defmodule or if. We will explore if (and other classical control flow mechanisms) in the Control-flow section.

Before we go any further, as our examples are getting bigger, we must discuss how you can write Elixir code in files. As you can see in the previous example, you can define modules on an IEx sessionhowever, any typo while writing them results in having to start from the beginning.

Put the contents of the last example in a filelet's call it "string_helper.ex" (we usually name the file with the name of the module we're defining in it). Elixir source code files may have two extensions: .ex or .exs. The difference between them is that the former is compiled to disk (creating .beam files), while the latter is compiled only in memory. We mostly use the .ex extension when working on a real application, except for the test files that use the .exs extension (as there's no point in compiling them to disk).

Having your file created, you can use the elixirc command in your terminal to compile it, passing the name of the file whose contents you want compiled. More interestingly, you can pass the filename to the iex command (iex string_helper.ex in our case). This will make Elixir compile your file, which will make our StringHelper module (and its functions) available in the IEx session. If you're already inside the IEx session and want to compile a new file, you can use the c command, passing the filename as a string:

iex> c("examples/string_helper.ex")
[StringHelper]

You can also nest modules:

$ cat examples/nesting_modules.ex
defmodule Helpers do
defmodule StringHelper do
# StringHelper code goes here
end
end
In the preceding example, the line starting with # is commented. That's the syntax to comment lines in Elixir. There's no syntax for multi-line commentsif you want to comment a block of code, prepend each line of that block with #.

However, during compilation, Elixir will prepend the outer module name to the inner module name, and separate them with a dot. This is just an amenity, as there is no relationship between these two modules. This syntax is equivalent to the following one, which is used much more in Elixir applications:

$ cat examples/nesting_modules_inline.ex
defmodule Helpers.StringHelper do
# StringHelper code goes here
end

We'll now explain the concept of arity, with our palindrome? function as an example. Named functions in Elixir are identified by their module name, the function's own name, and their arity. The arity of a function is the number of arguments it receives. Taking this into account, our palindrome? function is identified as Helpers.StringHelper.palindrome?/1, where /1 represents the arity of the function. You'll be seeing this notation a lot when browsing through Elixir documentation.

This concept is important because functions with the same name but different arities are, in effect, two different functions. However, for a human, it wouldn't make much sense that two functions with the same name (but different arities) are unrelated. As such, only use the same name for different functions when they are related to one another.

The common pattern in Elixir is to have lower-arity functions being implemented as calls to functions of the same name but with a higher arity. Let's extend our module with an emphasize function:

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

def emphasize(phrase) do
emphasize(phrase, 3)
end

def emphasize(phrase, number_of_marks) do
upcased_phrase = String.upcase(phrase)
exclamation_marks = String.duplicate("!", number_of_marks)
"#{upcased_phrase}#{exclamation_marks}"
end
end

Here, we can observe it in action:

iex> StringHelper.emphasize("wow")
"WOW!!!"
iex> StringHelper.emphasize("wow", 1)
"WOW!"
We've used the def construct to create functions. By using it, our functions are exported and can be called in other modules. If you want to change this behavior, and make a function only available within the module where it's defined, use the defp construct.

The function with an arity of 1 is implemented by simply calling emphasize/2. This is useful when you want to offer a broad interface on your module, which allows you to have some clients that simply want to call emphasize/1 and not have to specify the number of exclamation marks, but also have some other clients that want to call emphasize/2 and specify the number of exclamation marks.

When the code is as simple as in this example, this multitude of functions is not necessary, as you can achieve the same end result using default arguments. We do that by using the \\ operator in front of the argument name, and then the default value it should have:

$ cat examples/string_helper_emphasize_with_default_args.ex
def emphasize(phrase, number_of_marks \\ 3) do
upcased_phrase = String.upcase(phrase)
exclamation_marks = String.duplicate("!", number_of_marks)
"#{upcased_phrase}#{exclamation_marks}"
end

This will generate two functions with the same name and different arities, as in the last snippet. If your function has multiple bodies, as in the next example, you must define a function header with the default arguments defined there:

$ cat examples/string_helper_emphasize_with_function_header.ex
def emphasize(phrase, number_of_marks \\ 3)
def emphasize(_phrase, 0) do
"This isn't the module you're looking for"
end
def emphasize(phrase, number_of_marks) do
upcased_phrase = String.upcase(phrase)
exclamation_marks = String.duplicate("!", number_of_marks)
"#{upcased_phrase}#{exclamation_marks}"
end

In this example, we're also seeing an example of how we can use pattern matching in named functions. Note that the order in which we define our functions matters. Elixir will search from top to bottom for a clause that matches. If we had put the clause where we're matching against 0 on the second argument at the end, that definition of the emphasize function would become unreachable, as the other definition is more general and always matches. Elixir will help you avoid these situations, as it will emit a warning during compilation, alerting you of this situation.

Apart from using pattern matching (as we've seen in this example and on anonymous functions), on named functions we can use guard clauses, which extend on the pattern matching mechanism and allow us to set broader expectations on our functions. To use a guard clause on a function, we use the when clause after the list of arguments.

To see an example of this, we will use a guard clause on our palindrome? function. Up to this point, we were accepting an argument of any type. If we passed an integer to this function, an error would be raised, as we would be trying to call String.reverse on an integer. Let's change that:

$ cat examples/string_helper_palindrome_with_guard_clause.ex
def palindrome?(term) when is_bitstring(term) do
String.reverse(term) == term
end
def palindrome?(_term), do: {:error, :unsupported_type}

We now state that we're expecting bitstring as an argument. We've also created a new definition of our function, which runs when the match doesn't occur on the first definition. Here it is in action:

iex> StringHelper.palindrome?("abba")
true
iex> StringHelper.palindrome?(123)
{:error, :unsupported_type}

Using guard clauses in our functions can lead to a lot of duplication, since we may be repeating the same clause over and over again. To combat this, Elixir 1.6 introduced the defguard construct, which allows us to define clauses that can be reused.

Moreover, using this construct may improve the readability of your code, since we can extract complex guard clauses and give them descriptive names. Let's see the previous example implemented using defguard:

$ cat examples/string_helper_palindrome_with_defguard.ex
defguard is_string(term) when is_bitstring(term)

def palindrome?(term) when is_string(term) do
String.reverse(term) == term
end
def palindrome?(_term), do: {:error, :unsupported_type}

In this simple example, there's no clear advantage to using this construct. However, as your modules, along with your guard clauses, grow more complex, this technique becomes incredibly useful. Note that you can use defguardp to define a guard clause that is not exported, and can only be used within the module where it's defined.

You can use other type-checking functions in guard clauses, as well as comparison operators, and also some other functions. You can find the full list at https://hexdocs.pm/elixir/guards.html.

To end this section, we will now showcase one of the most eminent features of the language: the pipe (|>) operator. This operator allows you to chain function calls, making the flow of your functions easy to read and comprehend. This operator takes the term that's at its left, and injects it as the first argument on the function at its right. This seemingly insipid feature increases the readability of your code, which is amazing since code is read many more times than it is written. To see this operator in action, let's add some more logic to our palindrome? function: We will now remove leading or trailing whitespaces from the term we're checking, and we'll also make our comparisons case-insensitive. This is the result:

$ cat examples/string_helper_palindrome_with_pipe_operator.ex
def palindrome?(term) do
formatted_term = term
|> String.trim()
|> String.downcase()

formatted_term |> String.reverse() == formatted_term
end

While the impact may seem negligible in this simple example, you'll see the expressiveness this operator brings as we build our application throughout the book.

Module attributes, directives, and uses

Modules in Elixir may contain attributes. They're normally used where you'd use constants in other languages. You define a module attribute with the following syntax:

$ cat examples/string_helper_with_module_attribute.ex
defmodule StringHelper do
@default_mark "!"

# rest of the StringHelper code
end

Then, we can use the @default_mark module attribute inside the functions of this module. This attribute only exists at compile time, as it's replaced by its value during this process.

There are some other use cases for module attributes: you can register them, which makes them accessible at runtime. For instance, Elixir registers the @moduledoc and @doc attributes, which can be used to provide documentation for modules and functions, respectively. This documentation can then be accessed at runtime by other Elixir tools, as we'll explore in the Tooling and ecosystems section.

We're now mentioning macros for the first time. Macros are Elixir's mechanism to do meta-programminggenerating code that writes code. We will not touch macros in this introductory chapter, as they will be properly examined in Chapter 6Metaprogramming – Code that Writes Itself.

Elixir provides three lexically scoped directives to manage modules, plus a macro called use. We'll describe them now:

  • alias is used to create aliases for other modules. You use it as alias Helpers.StringHelper, as: StrHlp, and you can then refer to that module as StrHlp. The as: portion is optional, and if you don't provide it, the alias will be set to the last part of the module name.
  • We use require when we want to invoke what's defined as macros in a given module. As stated in the official documentation, is_odd/1 from the Integer module is defined as a macro. To use it in another module, you have to require it: require Integer.
  • When we want to access functions from other modules without having to use the fully-qualified name, we use import. When we import a given module, we're also automatically requiring it. If we're constantly using String.reverse/1 for instance, we can import it: import String, only: [reverse: 1]. Now we can just use reverse directly in our module. Apart from only:, you can also use except: to import all but a given number of functions from a module. Besides function names, only: and except: also accept :modules and :functions (which are self explanatory). You can also just use import without any option, but this isn't recommended, as it pollutes the scope of your modulealways try to pass the only: option when using import.
  • Last, but not least, we have use, which is a macro. This is commonly used to bring extra functionality to our modules. Beneath the surface, use calls the require directive and then calls the __using__/1 callback, which allows the module being used to inject code into our context.

For now, you don't need to know how all of this works. It is enough to know that you have these constructs to deal with modules. When we dive into macros later in this book, all of this will become much clearer.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime