Elixir Concepts : What You Need to Know Now

By Robert Boone | Aug 17, 2017

An image of a lightbulb on a chalkboard, sparking ideas

Elixir is a language built on the Erlang virtual machine. It was written in 2011 by José Valim. Elixir has a Ruby-like syntax but it behaves in a much different way. In this blog post we won’t go over syntax. There are many resources that go into great detail on Elixir syntax, so I want to talk about the what makes the Elixir language intriguing.

We are going to cover the following concepts:

  • Immutability
  • Pattern Matching
  • Pipelines
  • Concurrency

My hope is that these topics will spark some curiosity about Elixir and the Erlang platform.

Immutability

Variables that don’t change seem like a contradiction. And on first pass they don’t seem useful. But look at the following Ruby example:

flight_id = 5

current_flight = -> () { flight_id }

current_flight.call

5

I want a closure that wraps a value and returns it. I do this because `flight_id` will change but `current_flight` may be replaced with an object later and I can keep the `callable` interface. That all works as expected, but what if later in the code,`flight_id` is updated? Let’s see what happens:

flight_id = 3

current_flight.call

3

We didn’t directly change the value in `current_flight` but it has been updated unexpectedly. Let’s try this in Elixir:

flight_id = 5

current_flight = fn -> flight_id end

current_flight.()

5

So far everything looks the same. Now lets update the `flight_id`:

flight_id = 3

current_flight.()

5

This is the behavior that we expect. `current_flight` keeps it’s copy of the original `flight_id` it was given.

While there are ways in Ruby and other languages to prevent this type of bug from happening, by using a language that has immutable variables by default you avoid this class of bug with little effort. Another major benefit of immutability emerges when you have shared state in a threaded environment. We will see some of this later when we cover concurrency.

Pipelines

Because Elixir is a functional language it is common to pass the output of one function as the input of the other.

last_function(second_function(first_function(data)))

While this works it isn’t the easiest code to read. Elixir’s solution to this is the pipeline operator `|>`.

Lets rewrite the code with the pipeline operator:

data
|> first_function
|> second_function
|> last_function

Now you don’t have to read the code backwards to get the flow of data.

The pipeline operator takes the value on the left and passes it to the function on the right as the first argument.

It is possible to employ functions that use multiple arguments as long as the function’s first argument is the one being passed in by the pipeline operator.

data
|> function(second_argument)

This is just one of many optimizations made with developer happiness in mind!

Pattern Matching

x = 1

In most programming languages the `=` is used to assign a value to a variable. In Elixir, `=` is the match operator. If `x` is unbound it will assign the value to `x`. This becomes more apparent when you reverse the order of the statement:

1 = x

This expression only works if `x` matches the value. Otherwise, you get an error:

x = 2
1 = x
** (MatchError) no match of right hand side value: 2

On first glance this feature doesn’t look that useful. But it allows for some very expressive code. For example…

{:ok, value} = some_function()

This is a very common idiom in Elixir. Like destructuring in other languages, I’m expecting a data structure–in this case a `Tuple`–and I want to extract values from it. In the above case I want to grab what ever the second value is. But the value in the tuple returned from the function must have the symbol `:ok` as the the first  value or else you get an error message. A more expansive example would look like this:

defmodule Test do
  def some_function do
    if :rand.uniform(5) == 3 do
      {:ok, 3}
    else
      {:error, "Value was not 3"}
    end
  end
end


case Test.some_function() do
 {:ok, value} -> IO.puts(value)
 {:error, reason} -> IO.puts(reason)
 _  -> IO.puts("Unexpected error")
end

The case expression tries to match the value that comes from the function call. If the Elixir term in the tuple is not a variable it must match exactly. The `_` matches all values and acts as a catchall if the value falls through the first two patterns. Pattern matching isn’t limited to function output or case statements. Function arguments can also use pattern matching as well. Let’s define a function that uses pattern matching:

defmodule Example do
  def tuple_concat({first, second}) do
    first <> " " <> second
  end
end

Example.tuple_concat({"hello", "world"})
hello world

It is pretty obvious what is happening here. But with some special operators for lists, pattern matching becomes really useful. Now let’s build a function that counts items in a list:

defmodule Example do
  def count([]) do
    0
  end

  def count(list) when is_list(list) do
    count(list, 0)
  end

  def count([_|tail], acc) do
    count(tail, acc + 1)
  end

  def count([], acc) do
    acc
  end
end

Example.count([]) 
0


Example.count([1,2,3])
3

To break down what’s going on here:

  1. The first thing you will notice is multiple functions can have the same name and a different number of arguments.
  2. The first `count` function pattern matches an empty list. It just returns a 0.
  3. The next count takes a single argument and a guard that checks to see if the argument is a list. If it is a list, it calls the `count` function that takes
    two arguments, a list and an accumulator.
  4. The body of the function has some new syntax. You can use the `|` operator to separate a list into its first item and the rest of the items. So if
    you have `[head | tail] = [1,2,3]` `head` will have the `1`. And `tail` will have the list `[2,3]`. In this case we don’t care about the first argument
    and recursively call `count` with the `tail` and increment the accumulator.
  5. Once the list in the first argument is empty, it just returns the accumulator.

You may have noticed something else about the code– recursion! Elixir has no looping constructs. There are modules that have higher order functions to provide loop-like functions, such as the Enum module. But within the core of Enum, all the functions use recursion. Elixir does have a `for comprehension` that will allow you to loop over a list and filter values at the same time. Pattern matching is a core concept in Elixir, and proper understanding of this feature is key to mastering the language.

Concurrency

Sometimes when you’re coding, you have a problem where the run-order of things doesn’t matter. One such problem is downloading multiple files: Say you have 3 files. The first two files take 5 minutes to download. The last file takes 8 minutes. If you have to download them one at a time it would take 18 minutes to finish. But if you download them all at the same time, the overall duration is reduced to the download time of the largest file. In this case, 8 minutes:

defmodule Example do
  def download(name, url) do
    case HTTPoison.get(url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:ok, file} = File.open name, [:write]
        IO.binwrite file, body
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        IO.puts "Not found"
      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.inspect reason
    end
  end
end

[{name1, url1}, {name2, url2}, {name3, url3}]
|> Enum.each(fn {name, url} ->
  spawn(Example, :download, [name, url])
end)

The above code would launch three Elixir processes. Each would download a file and save the file to disk. In Elixir, there is also another use of processes: storing state. If you spawn a function that is recursive, the process does not exit and as long as the function is tail-call recursive there will be no memory leak. It is common that recursive functions have an argument that accumulates a result. With Elixir processes, that accumulator maintains state. Let’s make a process that holds a value that can be incremented.

defmodule Example do
  def counter(current \\ 0) do
    receive do
      :inc ->
        IO.puts(current)
        counter(current + 1)
      _  -> counter(current) 
    end
  end

  def start do
    spawn(Example, :counter, [0])
  end
end

pid = Example.start

send pid, :inc
0

send pid, :inc
1

send pid, :inc
2

Let’s look at what’s going on here:

  1. The `start` function configures and starts the process the same way the previous example did. Once started and inside the `counter` function, the first
    statement is the `receive` function.
  2. `receive` is blocking until it gets a message. Once the process has the message, it pattern matches against the clauses.
  3. If a `:inc` message is received, it prints the current counter to stdout then recursively calls itself with the current counter state plus one.
  4. If a message doesn’t match the `:inc` clause, the catchall clause is matched. It then does the recursive call with the current state.
  5. The `send` function is how processes send messages to each other.

This is only the start of what is possible with Elixir’s concurrency primitives. I didn’t talk about OTP. OTP is a set of libraries that provide a framework for common patterns in concurrent and distributed programs. WhatsApp, which handles one billion users a day, is powered by OTP.

Any concurrent tasks you may have whether large or small can be handled by Elixir processes.

Conclusion

  • We’ve seen how immutability makes code easier to reason about.
  • The pipeline operator makes code easier to read.
  • Pattern matching makes control flow obvious and allows us to destructure data in expressions and in function arguments, while aiding in functional decomposition.
  • Elixir’s concurrency primitives allow for large scale applications to be built efficiently.

While this post doesn’t cover all of Elixir’s features. I hope these topics have increased your interest in Elixir overall!

Get in touch

Marketo Form