Elixir for Rubyists

A gentle introduction for Ruby developers intrigued to learn more about Elixir.

Elixir took the Erlang virtual machine, BEAM, and put a sensible face on it. It gives you all the power of Erlang plus a powerful macro system. – Dave Thomas

On the surface, Elixir and Ruby share a similar syntax. This familiarity may at first hide the striking differences between the two languages.

Created by José Valim, Elixir is a dynamic, functional language designed for building scalable and maintainable applications. It leverages the Erlang VM; known for running low-latency, distributed and fault-tolerant systems. Elixir exists to enable higher extensibility and productivity in the Erlang VM, while keeping compatibility with Erlang’s ecosystem. Erlang functions can be called from Elixir without run time impact, due to compilation to Erlang bytecode, and vice versa.


What’s different about Elixir?

  • Shared nothing concurrent programming via message passing (Actor model).
  • Immutable state.
  • Emphasis on recursion and higher-order functions instead of side-effect-based looping.
  • Pattern matching.
  • Polymorphism via a mechanism called protocols.

Processes

Elixir code runs inside lightweight threads of execution, called processes. They are almost like objects as they are used to hold state. Processes are isolated and exchange information via messages:

current_process = self()

# spawns an Elixir process to send a message back to the current process
spawn_link(fn ->
  send(current_process, {:message, "hello world"})
end)

# block current process until the message is received
receive do
  {:message, message} -> IO.puts(message)
end

Produces the following output in Elixir’s interactive shell (iex):

hello world
:ok

A process in Elixir is not the same as an operating system process. Instead, it is extremely lightweight in terms of memory and CPU usage. An Elixir application may have hundreds of thousands of processes running concurrently on the same machine. They provide the basis for concurrency, and support building distributed and fault-tolerant programs.

Each process is isolated and independently garbage collected in Elixir. Whereas Ruby uses a mechanism combining full and partial garbage collection. In Ruby, the GC stops the world and stalls the application.

The Erlang VM will take advantage of all available CPU cores to intelligently distribute processes amongst them. Since inter-process communication is based on message passing, a process may be distributed amongst a cluster of nodes.

Processes encapsulate state

State is held by a process while it runs an infinite receive loop.

defmodule Counter do
  def start(initial_count) do
    loop(initial_count)
  end

  defp loop(current_count) do
    new_count = receive do
      :increment -> current_count + 1
      :decrement -> current_count - 1
    end
    IO.puts(new_count)
    loop(new_count)
  end
end

Usage:

iex> counter = spawn(Counter, :start, [0])
#PID<0.184.0>
iex> send(counter, :increment)
1
iex> send(counter, :increment)
2
iex> send(counter, :increment)
3
iex> send(counter, :decrement)
2

This pattern is so commonly used, that it is provided by the following reusable abstractions available in Elixir and OTP:

  • Agent - Simple wrappers around state.
  • GenServer - Generic servers (processes) that encapsulate state, provide sync and async calls, support code reloading, and more.
  • Task - Asynchronous units of computation that allow spawning a process and potentially retrieving its result at a later time.

Building blocks

Defining a module

Modules are define using defmodule:

defmodule PubSub do
  @moduledoc """
  Basic publish-subscribe messaging
  """

  @doc """
  Publish a message to a given list of process identifiers (PIDs)
  """
  def publish(message, recipients) do
    for recipient <- recipients do
      send(recipient, message)
    end
  end
end

Usage:

# publish a message to myself
iex> PubSub.publish("hello", [self()])
["hello"]
iex> flush()
"hello"
:ok

Executing functions

Functions exist within a module in Elixir, which must be specified when executing the function.

Elixir
iex> String.upcase("Thanks, José")
"THANKS, JOSÉ"
Ruby
irb> "Thanks, José".upcase
=> "THANKS, JOS"

Maps

Elixir
iex> grades = %{"Jane Doe" => 10, "Jim Doe" => 6}
iex> grades["Jane Doe"]
10
iex> Map.get(grades, "Jane Doe")
10
iex> grades = Map.put(grades, "Jane Doe", 20)

Since Elixir is immutable you must match the value returned by a function.

Ruby
irb> grades = { "Jane Doe" => 10, "Jim Doe" => 6 }

irc> grades["Jane Doe"]
#=> 10

Pattern matching using the match operator

In Elixir, the = operator is actually called the match operator. It can be used to match against simple values, but is more useful for destructuring complex data types.

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

Using the ^ operator prevents variables from being rebound.

iex> [^a, ^b, ^c] = [4, 5, 6]
** (MatchError) no match of right hand side value: [4, 5, 6]

Case statements

Use pattern matching within a case statement to execute a matched expression:

def buy_ticket?(age) do
  case age do
    age when age >= 18 -> true
    _ => false
  end
end

This example could also be written by pattern matching on function arguments:

def buy_ticket?(age) when age >= 18, do: true
def buy_ticket?(age), do: false

Structs

Structs are typed maps that provide compile-time checks and default values.

defmodule User do
  defstruct [
    name: nil,
    email: nil,
    active: false,
  ]
end

Usage:

iex> user = %User{name: "Ben", email: "ben@startlearningelixir.com", active: true}
%User{active: true, email: "ben@startlearningelixir.com", name: "Ben"}
iex> user.name
"Ben"
iex> user.foo
** (KeyError) key :foo not found in: %User{active: true, email: "ben@startlearningelixir.com", name: "Ben"}

Updating structs:

iex> updated = %User{user | active: false}
%User{active: false, email: "ben@startlearningelixir.com", name: "Ben"}

Pattern matching with structs

defmodule User do
  def greeting(%User{name: name}) do
    "Hello, #{name}"
  end

  # ... or more succinctly
  def greeting(%User{name: name}), do: "Hello, #{name}"
end

Usage:

iex> User.greeting(user)
"Hello, Ben"

Chaining function call using the pipe operator

Use the pipe (|>) operator to chain method calls together, to replace nested function calls that can be difficult to understand.

iex> String.split(String.upcase("learn Elixir"))
["LEARN", "ELIXIR"]

Becomes:

iex> "Learn Elixir" |> String.upcase() |> String.split()
["LEARN", "ELIXIR"]

Getting started with Elixir

Follow the installing Elixir guide on the language’s official website to get up and running.

Mix

Mix is a build tool that ships with Elixir that provides tasks for creating, compiling, testing your application, managing its dependencies and much more. It’s Ruby equivalent would be rake tasks.

$ mix help
mix                   # Runs the default task (current: "mix run")
mix app.start         # Starts all registered apps
mix app.tree          # Prints the application tree
mix archive           # Lists installed archives
mix archive.build     # Archives this project into a .ez file
mix archive.install   # Installs an archive locally
mix archive.uninstall # Uninstalls archives
mix clean             # Deletes generated application files
mix cmd               # Executes the given command
mix compile           # Compiles source files
mix deps              # Lists dependencies and their status
mix deps.clean        # Deletes the given dependencies' files
mix deps.compile      # Compiles dependencies
mix deps.get          # Gets all out of date dependencies
mix deps.tree         # Prints the dependency tree
mix deps.unlock       # Unlocks the given dependencies
mix deps.update       # Updates the given dependencies
mix do                # Executes the tasks separated by comma
mix escript           # Lists installed escripts
mix escript.build     # Builds an escript for the project
mix escript.install   # Installs an escript locally
mix escript.uninstall # Uninstalls escripts
mix help              # Prints help information for tasks
mix new               # Creates a new Elixir project
mix run               # Runs the given file or expression
mix test              # Runs a project's tests
mix xref              # Performs cross reference checks
iex -S mix            # Starts IEx and runs the default task

Creating a new Elixir project

Mix is used to create a new Elixir project.

mix new PATH [--sup] [--module MODULE] [--app APP] [--umbrella]

The --sup option is used to generate an OTP application, including a supervision tree. This will include an application callback, used to define the top-level supervisor and bootstrap the app. Excluding this option is used to build a library application; it will only expose functions to be used by other applications. As an example, a Phoenix web application would include the super library for Markdown parsing would typically

Umbrella projects, created with --umbrella, allow you to create an application composed of one or more applications. You can then run mix commands, such as mix test, from the umbrella project to execute it for each contained application.

$ mix new example --sup
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/example.ex
* creating lib/example/application.ex
* creating test
* creating test/test_helper.exs
* creating test/example_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd example
    mix test

Run "mix help" for more commands.

Mix configuration

The default mix configuration, mix.exs, for Elixir 1.4 is below. This file is used to define the application, such as to configure the application module that will bootstrap the app and its dependencies.

defmodule Example.Mixfile do
  use Mix.Project

  def project do
    [app: :example,
     version: "0.1.0",
     elixir: "~> 1.4",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    # Specify extra applications you'll use from Erlang/Elixir
    [extra_applications: [:logger],
     mod: {Example.Application, []}]
  end

  # Dependencies can be Hex packages:
  #
  #   {:my_dep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    []
  end
end

Dependencies

You list any external dependencies using the deps function. I’ve shown three examples: using the Hex package manager; an umbrella app dependency; and a local file system dependency. Hex is the Elixir equivalent to Ruby’s gems.

defp deps do
  [
    {:ecto, "~> 2.1"},
    {:umbrella_dependency, in_umbrella: true},
    {:local_dependency, path: "~/src/local_dependency"},
  ]
end

Once the dependencies have been listed, as above, you run mix deps.get:

$ mix deps.get
Running dependency resolution...
Dependency resolution completed:
  decimal 1.3.1
  ecto 2.1.3
  poolboy 1.5.1
* Getting ecto (Hex package)
  Checking package (https://repo.hex.pm/tarballs/ecto-2.1.3.tar)
  Fetched package

Then compile them using mix deps.compile.

Supervision

“Let it crash” is a well known mantra to Erlang, and Elixir, programmers. The view is that you don’t need to program defensively. If there are any errors, the process is automatically terminated, and this is reported to any processes that were monitoring the crashed process. In fact, defensive programming in Erlang is frowned upon.

Letting processes crash is central to Erlang: it’s the equivalent of unplugging a device, and plugging it back in.

Wipe the slate clean

A process terminates if there’s an execution error, and the reason for the termination is reported. The monitoring process can then restart the process or take other action. At its most basic, the action could be to simply restart the process. This will initialise the process with a known, good, starting state. As long as you can get back to a known state, this turns out to be a very good strategy.

You build supervision trees to support this approach. A supervisor will decide how to deal with a crashed process. It will restart the process, or possibly kill some other processes, or crash and let someone else deal with it. In Elixir, you decide up front how to deal with these errors. It cleanly separates the code you write to perform a task, from the code that deals with detecting and correcting errors.

Supervising Elixir

An Elixir application defines a top-level supervisor, who is responsible for constructing the supervision tree.

defmodule Example.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    # Define workers and child supervisors to be supervised
    children = [
      worker(Example.Worker, [arg1, arg2, arg3]),
    ]

    opts = [strategy: :one_for_one, name: Example.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

The supervisor must specify the restart strategy for its children.

  • :one_for_one - if a child process terminates, only that process is restarted.
  • :one_for_all - if a child process terminates, all other child processes are terminated and then all child processes (including the terminated one) are restarted.
  • :rest_for_one - if a child process terminates, the “rest” of the child processes, i.e., the child processes after the terminated one in start order, are terminated. Then the terminated child process and the rest of the child processes are restarted.
  • :simple_one_for_one - similar to :one_for_one but suits better when dynamically attaching children. This strategy requires the supervisor specification to contain only one child. Many functions in this module behave slightly differently when this strategy is used.

Macros

Elixir’s built-in unit testing framework, ExUnit, takes advantage of macros to provide great error messages when test assertions fail.

defmodule ListTest do
  use ExUnit.Case, async: true

  test "can compare two lists" do
    assert [1, 2, 3] == [1, 3]
  end
end

The async: true option allows tests to run in parallel, using as many CPU cores as possible.

Running the failing test produces a descriptive error. The equality comparison failed due to differing left and right hand side values.

mix test

1) test can compare two lists (ListTest)
     test/list_test.exs:13
     Assertion with == failed
     code:  [1, 2, 3] == [1, 3]
     left:  [1, 2, 3]
     right: [1, 3]

Finally, once started, Erlang/OTP applications are expected to run forever.


Further reading

If your interest in Elixir has been piqued by this article then take a look at the best books, screencasts, and tutorials to learn Elixir. As recommended by the Elixir enthusiasts from the popular Elixir Forum.

The following articles are also aimed at Elixir for Rubyists, and well worth reading.