Let's embark on a journey, let's leave behind the world we know and head to something new and different. We quest to learn a new programming language, Elixir, and new paradigm of programming, functional. We set out leaving behind most of what we know, and attempt to think differently.
Elixir is a functional, dynamic language built on top of Erlang and the Erlang VM (BEAM). Erlang is a language that was originally written in 1986 by Ericsson to help solve telephony problems, including distribution, fault-tolerance, and concurrency, among others. Elixir, written by José Valim, extends Erlang and provides a friendlier syntax into the Erlang VM while maintaining interoperability with Erlang and Elixir without imposing performance costs.
Elixir's roots in Erlang provide some really indispensable functionality for developing distributed and fault-tolerant applications. Developing in Elixir, we can have all that and then some.
That is, Elixir provides and exposes to us the means and tools to create applications that can truly run with nine nines of reliability. Those are a fail-fast by default design of the runtime with the concept of process supervision, which enables strong fault-tolerance, the inherent concurrency of message passing, and a functional language that also enables distribution. We will discuss all of these topics and concepts by the conclusion of this book.
But before we get into these excellent features of Elixir and Erlang, let's take a dive into functional programming and why it's useful in creating a system that has these features.
I assume you're familiar with imperative languages such as Perl and Java. Furthermore, you're likely familiar with the concept of static typing and dynamic typing, as in Python. But what is functional programming? Moreover, why should we care about it?
Functional programming is a paradigm of programming, a means of structuring and reasoning about code. It is, in essence, about composing functions that transform data. That is, when writing functionally, we write simple functions that transform data in a particular manner. Then we later write some other functions that use our previous functions as building blocks for more complicated transformations. This may not sound all too foreign.
In the object-oriented world, programming is about maintaining state in some controlled fashion. We create object hierarchies to define the world and we operate on some methods of those objects to manipulate the world around us. That is, we compose objects to model and, if we're lucky, we solve problems.
These are both methods of and for abstraction. We write simple components and compose. When a simple component is defined, we can forget its details and begin thinking about bigger components that result from the combination of those smaller ones.
However, there are several problems that creep up on us in the object-oriented and imperative worlds. They are subtle and they rarely, if ever, show themselves directly. These are the problems that are hard to find, hard to debug, and hard to fix. Although, we can see their symptoms.
We notice the symptoms when we attempt to conceptualize or interpret our own code using an ideal or imaginary interpreter. We notice the symptoms when we attempt to test large components. We notice the symptoms when we attempt to split execution paths. Something, somewhere, inevitably fails.
Objects and imperative code are usually, relatively easy to understand on paper. So why is this understanding only on the surface and so easily shattered when we dig further?
Imperative code is certainly testable. We can certainly get to correct solutions. But why is it so difficult to write good, testable code? Why are the correct answers so hidden from view?
Clearly imperative code is composable; yet, why is it often difficult to compose objects and existing functions? What is hindering our ability to do this well?
We can also write concurrent, imperative code. Why, then, is it seemingly so daunting and nearly impossible to get correct?
The lurking monster hiding behind our questions is usually the one thing that makes programming actually useful: side-effects or state. Functions in the imperative world usually encapsulate implicit changes to variables, objects, files, and, in other words, changes to state. These changes usually cost nothing to program and may, in fact, be pinnacle to the function they originate from. How useful would printing text to the console be if you were unable to write a stream of bytes to the character device, that is, your terminal?
It is these hidden, out-of-mind side-effects that can make programming so dangerous to understanding, correctness, and composability, not to mention, concurrency. To overcome this, we can't forget the side-effects lurking in our code. That is, when composing components in the object-oriented world, we still fail to release ourselves from the burden of implementation. We must still page the details of our objects to use them effectively. Our escape is functional programming.
Functional programming allows us to escape from these problems by forcing us to confront the issue of changing states. It gives us guidelines for how to construct our components and not to forget that state changes are inevitable and how to handle them appropriately, without compromising on composition, understandability, and testability. Furthermore, well managed side-effect inducing code, is nearly trivial to make concurrent.
The functions implemented in functional languages must return the same output for the same input. Any dependence of the output on the state, outside of what we give the function, must not change the output of the function. Changes of state are handled in a very controlled manner: they must be marshaled through some channel. Most functional languages handle this in similar ways, but this can also depend on the level of purity of the language. However, this affords us easy-to-understand and easy-to-personally interpret code, and lets us create well defined modularity and testable components. Also, by restricting changes of state (or disallowing them entirely), making code concurrent is essentially free.
Best of all, functional programming isn't something restricted to a particular language; it's a concept that can be followed and used in any language. It's more of a state-of-mind than being partial to a particular set of languages.
Of course, I can't ethically speak about the benefits of functional programming without also warning you of some potential limitations, particularly of the new learning curve and some performance considerations.
Functional programming can feel very limited to newcomers. It's generally difficult to do anything useful in purely functional programming languages, and often, this isn't helped by examples that only show the functional bits in elementary examples.
When transforming data in purely functional languages, we must create new structures for the modified data. If we could otherwise modify the existing data structures, we would be violating our immutability invariant. Thus, when modifying some data structure, we must usually create a new structure. This has fairly obvious performance issues, namely, the memory copies are required to create the new structure on top of performing the actual modifications.
However, there is hope for both of these concerns. Firstly, with respect to performance considerations, functional programs can typically have more sophisticated tooling because it is not only easier for us to understand as mere mortals, but also the tools we write to parse, compile (translate), and execute the functional code written. Some interesting compiler optimizations can be achieved from the shared nothing, process separation, and message passing processing model enforced by the underlying runtime.
That is, functional code, generally, is easier to parse, both by humans and compilers. Therefore, the compiler and runtime can flatten, unroll, and take our functional code and rewrite it into a safe, optimized form. The runtime can also take advantage of object graph information to decrease the number of copies needed to modify data. The graph can later be checked by the runtime and compressed if necessary; this is the basis for garbage collection.
Secondly, with respect to the learning curve, I hope to teach functional programming through more than elementary or intermediate examples by the conclusion of this book.
Before we can truly begin our journey into the depths of Elixir, we need to install it and make sure our environment is sane. I will cover some basic installations for most OSes. As far as hardware requirements are concerned, there really are none. However, if you happen to not have a multi-core CPU, you may miss out on the inherent speed benefits of the runtime.
Most distributions, today, will have Elixir in their repositories and this is the preferred way to install Elixir. Installing Elixir from your distribution's repositories will also take care of installing Erlang.
If you are using a Red Hat-based distribution of GNU/Linux, you can use the yum package manager tool to install Elixir:
# yum install elixir ... Transaction Summary =========================================================================== Install 1 Package (+14 Dependent packages) Total download size: 16 M Installed size: 31 M Is this ok [y/d/N]: y ... Complete!
If, on the other hand, you use a Debian-based distribution, you will need to add the Erlang Solutions repository and install Elixir using dpkg
and apt-get
:
$ wget {.deb for your distribution} $ sudo dpkg -i {downloaded version of erlang}.deb $ sudo apt-get update $ sudo apt-get install elixir ... The following extra packages will be installed: erlang-asn1 erlang-base erlang-crypto erlang-inets erlang-mnesia erlang-public-key erlang-runtime-tools erlang-ssl erlang-syntax-tools Suggested packages: erlang erlang-manpages erlang-doc erlang-tools The following NEW packages will be installed: elixir erlang-asn1 erlang-base erlang-crypto erlang-inets erlang-mnesia erlang-public-key erlang-runtime-tools erlang-ssl erlang-syntax-tools 0 upgraded, 10 newly installed, 0 to remove and 0 not upgraded. Need to get 12.9 MB of archives. After this operation, 23.8 MB of additional disk space will be used. Do you want to continue? [Y/n] y ... Setting up erlang-crypto (1:17.5) ... Setting up erlang-mnesia (1:17.5) ... Setting up erlang-runtime-tools (1:17.5) ... Setting up erlang-syntax-tools (1:17.5) ... Setting up erlang-asn1 (1:17.5) ... Setting up erlang-public-key (1:17.5) ... Setting up erlang-ssl (1:17.5) ... Setting up erlang-inets (1:17.5) ... Setting up elixir (1.0.4-1) ...
Note
The .deb
file you download will be specific to your distribution. The Erlang Solutions download page has as many to choose from.
Or, if you're like me and you're running Arch Linux, you can install Elixir with pacman
:
$ sudo pacman -S elixir erlang-nox resolving dependencies... looking for conflicting packages... Packages (2) elixir-1.0.4-1 erlang-nox-17.5-1 Total Installed Size: 107.70 MiB :: Proceed with installation? [Y/n] y (2/2) checking keys in keyring [#################] 100% (2/2) checking package integrity [#################] 100% (2/2) loading package files [#################] 100% (2/2) checking for file conflicts [#################] 100% (2/2) checking available disk space [#################] 100% (1/2) installing erlang-nox [#################] 100% Optional dependencies for erlang-nox erlang-unixodbc: database support java-environment: for Java support lksctp-tools: for SCTP support (2/2) installing elixir [#################] 100%
I'm suggesting the non-X (erlang-nox
) version as Arch separates the Erlang releases based on whether it has GUI libraries included or not, and we will not need them for this book. If you later decide that you want or need them, you can simply install the regular Erlang package and tell pacman
to remove the non-X version.
For Apple Mac OS X, you are hopefully using Homebrew or MacPorts.
Use the following command to install Elixir via Homebrew:
$ brew update; brew install elixir
Use the following command to install Elixir via MacPorts:
$ sudo port install elixir
If you're using Microsoft Windows, you can download a precompiled binary from the Elixir INSTALL (http://elixir-lang.org/install.html) page. Go through the installation wizard to complete the installation.
Manual installation should really be avoided if at all possible, but I'll include it in case your system isn't listed here or on the installation page, or for some other unforeseeable reason.
First, you will need to download and install an Erlang binary provided by Erlang Solution (https://www.erlang-solutions.com/downloads/download-erlang-otp). Next, you will need to download a precompiled ZIP file from Elixir's releases page. Unpack the ZIP folder to the location of your choice. Once unpacked, you should update your PATH
variable to include the bin
directory of the Elixir release.
Another option with respect to manually installing Elixir is to build Elixir from source and, by extension, build and install Erlang from source.
The latest source of Erlang can be found on its GitHub page (https://github.com/erlang/otp). After building and installing a satisfactory version of Erlang, download and build the source for Elixir, also available on GitHub (https://github.com/elixir-lang/elixir-lang.github.com).
Now that Elixir is installed on your machine, let's fire up the interactive Elixir prompt. Open a shell/terminal emulator and run iex
.
You should see the following text printed on the terminal:
$ iex Erlang/OTP 17 [erts-6.4] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] Interactive Elixir (1.0.4) - press Ctrl+C to exit (type h() ENTER for help) iex(1)>
Before we continue, let's talk about some of the output of running iex
. The line starting with "Erlang/OTP..." contains the Erlang emulator information tags. Each tag describes something about the underlying runtime VM. Here are some short explanations for each tag shown on my machine:
Erlang/OTP 17
: This tells us the current version of Erlang.[erts-6.4]
: This is the version of the Erlang runtime system.[source]
: The Erlang emulator was compiled from source. This is typical if you or your package maintainer built Erlang from source (and didn't use the official precompiled binaries from Ericsson).[64-bit]
: This means the emulator is built to take full control of 64-bit memory addressing.[smp:8:8]
: This tells us how many CPU's and schedulers are available and online.[async-threads:10]
: This gives us the asynchronous threads available to the runtime.[hipe]
: This tells us that the Erlang emulator is compiled with the high performance extensions enabled.[kernel-poll:false]
: This informs us that the kernel polling is disabled.
For the majority of these, however, you don't necessarily need to concern yourself with until you get into system and performance tuning, which will be out of the scope of this book. There are also many more options that can be listed here, so you may want to, if you need, look at the Erlang BEAM emulator source (https://github.com/erlang/otp/blob/maint/erts/emulator/beam/erl_bif_info.c). A Stack Overflow question (http://stackoverflow.com/questions/1182025/what-do-the-erlang-emulator-info-statements-mean) has the same information as well, in a, perhaps, more accessible format.
Next, the line following the break tells us the version of Elixir installed, how to quit, and about a helpful command for getting, err, help.
Infamously, we can try typing, "Hello, World!"
, and we should see it echoed back to the screen:
iex(1)> "Hello, World!" "Hello, World!" iex(2)>
We can also do some basic arithmetic:
iex(2)> 40 + 2 42 iex(3)>
Best of all, we can get help documentation right in our shell by executing the following command:
iex(3)> h IEx.Helpers Welcome to Interactive Elixir. You are currently seeing the documentation for the module IEx.Helpers which provides many helpers to make Elixir's shell more joyful to work with. This message was triggered by invoking the helper h(), usually referred to as h/0 (since it expects 0 arguments). There are many other helpers available: • c/2 — compiles a file at the given path • cd/1 — changes the current directory • clear/0 — clears the screen • flush/0 — flushes all messages sent to the shell • h/0 — prints this help message • h/1 — prints help for the given module, function or macro • l/1 — loads the given module's beam code and purges the current version • ls/0 — lists the contents of the current directory • ls/1 — lists the contents of the specified directory • pwd/0 — prints the current working directory • r/1 — recompiles and reloads the given module's source file • respawn/0 — respawns the current shell • s/1 — prints spec information • t/1 — prints type information • v/0 — prints the history of commands evaluated in the session • v/1 — retrieves the nth value from the history • import_file/1 — evaluates the given file in the shell's context
Help for functions in this module can be consulted directly from the command line. As an example, try:
h(c/2)
You can also retrieve the documentation for any module or function. Try these:
h(Enum) h(Enum.reverse/1)
To discover all available functions for a module, type the module name followed by a dot, then press Tab to trigger autocomplete. For example:
Enum.
To learn more about IEx as a whole, just type h(IEx)
:
iex(4)>
I'll let you try h(IEx)
.
To exit the interactive prompt, you can press Ctrl + C twice, or you can press Ctrl +G + Q + Enter.
As a quick aside, notice the numbers following the methods, for example, h/0
. What is the number? The number stands for the arity or number of parameters the function expects. So, h/0
means the h
function expects no parameters. This is often how we will see and talk about functions in Elixir (and in Erlang).
Now we are going to try something else. We are going to continue with some more introductory examples and some code, modules, and functions we will use throughout the book.
Let's fire up our interactive Elixir prompt again:
$ iex iex(1)>
This time, we are going to try "Hello, World!"
with the IO.puts
function:
iex(1)> IO.puts("Hello, World!") Hello, World! :ok iex(2)>
Well, this is different. What happened? First of all, notice that "Hello, World!"
is written to the screen without the quotes. Further, what is this :ok
thing?
It just so happens that the IO.puts
function is a function with side-effects; it writes its parameter's value to the screen. Since Elixir statements are all expressions, every statement must return a value. The value returned in this example, :ok
, is an atom, and we will cover exactly what these are in the next chapter. For now, what is important is that this return value signifies to the caller, us, that the operation has been successful. It is very common for Elixir code that either succeeds or fails to return either the atom, :ok
, or the atom, :error
.
We can try this function with different data and should see similar results:
iex(2)> IO.puts(42) 42 :ok iex(3)> IO.puts([]) :ok iex(4)>
When we call IO.puts
with 42
, we get what we expect—the number 42
is written to the screen and we get the :ok
return value. But what about the next example? It seems to return an empty string and :ok
. What is going on here? Well, as it turns out, Elixir is interpreting the empty list as an empty list of characters. And certainly, we can print an empty list of characters as an empty string. We will discuss this more when we go over lists in the next chapter.
Another function we will often use while developing and debugging Elixir code is the inspect/2
function. From the help, type h(inspect/2)
in your iex
:
def inspect(arg, opts \\ []) Inspect the given argument according to the Inspect protocol. The second argument is a keywords list with options to control inspection.
We will cover protocols more specifically in Chapter 9, Metaprogramming – Doing More with Less. For now, let's check the documentation of the Inspect
protocol (http://elixir-lang.org/docs/v1.1/elixir/Inspect.html).
The Inspect
protocol is responsible for converting any Elixir data structure into an algebra document. This document is then formatted, either in a pretty printing format or a regular one.
Essentially, the inspect/2
function allows us to peer into our data structures and see what's inside, in a readable format.
The inspect/2
function is a useful function for viewing the internal values or states of some data structures of our programs. These can also be used in print statement style debugging. To some degree, you may think of the inspect/2
function as a to string for most types. However, do not use this function for that purpose!
Read:
The Elixir GETTING STARTED page (http://elixir-lang.org/getting-started/introduction.html)
Do:
Try out the interactive interpreter (IEx).
Try some more basic arithmetic.
Try out the help command some more.
Try entering
'Hello, World!'
in IEx. What may be the difference between single quotes and double quotes? (No fret, we will cover this in the next chapter!)