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

Control flow


We're now introducing control-flow constructs. In Elixir, they aren't used as often as in traditional imperative languages, because we can fulfill our control-flow needs, using a mix of pattern matching, multi-clause functions, and guard clauses. Whenever you're about to use one of the constructs we're presenting in this section, stop and check whether it's possible to employ a more functional approach. Code without these traditional control-flow constructs is usually easier to understand and test. If you get to a point where you have nested conditionals, it's almost guaranteed you can simplify it by using one of the approaches I mentioned earlier. Either way, you'll occasionally use these constructs, so it's important to know they exist.

if and unless

These two constructs can be used with the following syntax:

if <expression> do
  # expression was truthy
else
  # expression was falsy
end

unless <expression> do
  # expression was falsy
else
  # expression was truthy
end

As with the def construct, they can be inlined. For if, you'd do this:

if <expression>, do: # expression was truthy, else: # expression was falsy 

For both constructs, the else clause is optional. They will return nil if the main clause doesn't match and no else clause was provided.

cond

cond can be seen as a multi-way if statement, where the first truthy condition will run its associated code. This may substitute chains of if ... else if blocks. Let's see this with an example on IEx:

iex> x = 5
5
iex> cond do
...>   x * x == 9 -> "x was 3"
...>   x * x == 16 -> "x was 4"
...>   x * x == 25 -> "x was 5"
...>   true -> "none of the above matched"
...> end
"x was 5"

true in a condition will serve as a default condition, which will run when no other clause matches.

case

case accepts an expression, and one or more patterns, which will match against the return value of the expression. These patterns may include guard clauses. These patterns are matched (from top to bottom), and will run the code associated with the first expression that matches. Here is a simple example:

iex> case Enum.random(1..10) do
...>   2 -> "The lucky ball was 2"
...>   7 -> "The lucky ball was 7"
...>   _ -> "The lucky ball was not 2 nor 7"
...> end
"The lucky ball was not 2 nor 7"

Note that your output may differ when running this example, as we're matching against Enum.random/1. In here, the default condition is represented by using _ in the pattern, which will match anything. Although a bit more condensed, the case construct is similar to a multi-clause function.

with

This control-flow construct, introduced in Elixir 1.2, accepts one or more expressions, a do block, and optionally an else block. It allows you to use pattern matching on the return value of each expression, running the do block if every pattern matches. If one of the patterns doesn't match, two things may happen: If provided, the else block will be executed; otherwise, it will return the value that didn't match the expression. In practice, with allows you to replace a chain of nested instances of case or a group of multi-clause functions.

To demonstrate the usefulness of with, let's see an example:

iex> options = [x: [y: [z: "the value we're after!"]]]               
[x: [y: [z: "the value we're after!"]]]
iex> case Keyword.fetch(options, :x) do                              
...>   {:ok, value} -> case Keyword.fetch(value, :y) do              
...>     {:ok, inner_value} -> case Keyword.fetch(inner_value, :z) do
...>       {:ok, inner_inner_value} -> inner_inner_value             
...>       _ -> "non-existing key"
...>     end
...>     _ -> "non-existing key"
...>   end
...>   _ -> "non-existing key"
...> end
"the value we're after!"

We're using the Keyword.fetch/2 function to get the value of a key from a keyword list. This function returns {:ok, value} when the key exists, and :error otherwise. We want to retrieve the value that's nested on three keyword lists. However, let's say that if we try to fetch a key that doesn't exist on the keyword list, we have to return "non-existing key". Let's achieve the same behavior using with, operating on the same options list as the preceding example:

iex> with {:ok, value} <- Keyword.fetch(options, :x),                
...>      {:ok, inner_value} <- Keyword.fetch(value, :y),            
...>      {:ok, inner_inner_value} <- Keyword.fetch(inner_value, :z),
...>      do: inner_inner_value                                      
"the value we're after!"

Note that, since our expression is really small, we're using the shorthand do: syntax (but we can also use a regular do ... end block). As you can see, we're getting the same result back. Let's try to fetch a key that doesn't exist:

iex> with {:ok, value} <- Keyword.fetch(options, :missing_key),      
...>      {:ok, inner_value} <- Keyword.fetch(value, :y),            
...>      {:ok, inner_inner_value} <- Keyword.fetch(inner_value, :z),
...>      do: inner_inner_value                                      
:error

Since we didn't provide an else block, we're getting back the value that didn't match, which is the return value of Keyword.fetch/2 when a key doesn't exist in the keyword list provided. Let's do the same, but by providing an else block:

iex> with {:ok, value} <- Keyword.fetch(options, :missing_key), 
...> {:ok, inner_value} <- Keyword.fetch(value, :y), 
...> {:ok, inner_inner_value} <- Keyword.fetch(inner_value, :z) do
...> inner_inner_value
...> else
...> :error -> "non-existing key"
...> _ -> "some other error"
...> end
"non-existing key"

Since we're now providing an else block, we can now handle error cases accordingly. As you can see, else takes a list of patterns to match on. As you do with case, you can use _ as a default clause, which would run when the patterns above (if any) didn't match.

As you can see, with is a very helpful construct, which allows us to create very expressive code that is concise and easy to read. Moreover, you can control how to handle each error separately, using pattern matching inside the else block.

Note

The first expression provided to with has to be on the same line of with itself, you'll get a SyntaxError otherwise. If you do want to have with on its own line, wrap the expressions provided to it in parentheses.

Exceptions

Much like if and else, exceptions in Elixir aren't used as much as in other popular imperative languages. Exceptions aren't used for control flow, and are left for when truly exceptional things occur. When they do, your process is usually running under a supervision tree, and upon crashing, the supervisor of your process will be notified and (possibly, depending on the strategy) restart it. Then, upon being restarted, you're back to a known and stable state, and the effects of the exceptional event are no longer present. In the Elixir and Erlang communities, this is usually referred to as the "Let it crash!" philosophy. We'll be examining this in greater detail in Chapter 3, ProcessesThe Bedrock for Concurrency and Fault Tolerance, when we talk about processes, supervisors, and supervision trees.

For now, I'll list the traditional error-handling constructs. You can raise an error with the raise construct, which takes one or two arguments. If you provide only one argument, it will raise a RuntimeError, with the argument as the message. If you provide two arguments, the first argument is the type of error, while the second is a keyword list of attributes for that error (all errors must at least accept the message: attribute). Let's see this in action:

iex> raise "Something very strange occurred"
** (RuntimeError) Something very strange occurred
iex> raise ArithmeticError, message: "Some weird math going on here"
** (ArithmeticError) Some weird math going on here

You can rescue an error by using the rescue construct (you can rescue from a try block or from a whole function, pairing it with def). You define patterns on the rescue clause. You can use _ to match on anything. If none of the patterns match, the error will not be rescued and the program will behave as if no rescue clause was present:

iex> try do
...> 5 / 0
...> rescue
...> e in ArithmeticError -> "Tried to divide by 0."
...> _ -> "None of the above matched"
...> end
"Tried to divide by 0."

Since we're not doing anything with the error, and just returning a string, we could just use ArithmeticError in the pattern. Only use this syntax if you want to capture the error itself. When none of the patterns match, we get the error back in our console:

iex> try do
...>   5 / 0
...> rescue
...>   ArgumentError -> "ArgumentError was raised."
...> end
** (ArithmeticError) bad argument in arithmetic expression

Furthermore, you can also pass an else and/or an after block to the try/rescue block. The else block will match on the results of the try body when it finishes without raising any error. As for the after construct, it will always get executed, regardless of the errors that were raised. This is commonly used to clean up some resources (closing a file descriptor, for instance).

Note

We've mentioned in this section that we don't use exceptions to control the flow of our programs, but in fact there's a special construct in Elixir for this. The syntax is similar to the one shown earlier, but you use throw instead of raise, and catch instead of rescue. As mentioned in the official Getting Started guide (http://elixir-lang.github.io/getting-started/try-catch-and-rescue.html) this should be used in situations where it is not possible to retrieve a value unless by using throw and catch. It's also mentioned that those situations are quite uncommon in practice.