bakgrunnseffekt

Introduction to Elixir

Marius Kalvø

Seniorkonsulent

portrait image decoration

Publisert Mandag 28. juni 2021
Sist oppdatert Fredag 8. oktober 2021

Introduction

Elixir is a functional, dynamically typed language that runs on the Erlang VM (BEAM). It is a language that uses the battle tested features of Erlang, while providing the comfort of a modern programming language. Elixir’s language design is similar to that of Ruby, but it is also inspired by e.g. Clojure and Erlang.

The Erlang VM is known for its capabilities of running fault-tolerant, distributed and low-latency applications. By running on the Erlang VM, these capabilities are available when writing Elixir applications.

History and background

While Elixir is a relatively new language, its foundation was created decades ago, with Erlang/OTP. Erlang is a functional programming language created for developing fault-tolerant, distributed and low-latency applications, specifically for the telecom industry. It was developed at Ericsson in the late 1980s and was used for their telephone switching systems. Erlang is often used as a general term of the language Erlang and Erlang/OTP. OTP, or the Open Telecom Platform consists of the Erland runtime, including the Erlang VM, libraries and middleware you can use in your applications, and a set of design principles on how to develop Erlang applications.

Elixir was developed by Josè Valim, and first appeared in 2012. Inspired by languages such as Ruby, Clojure and Erlang, Jose created a new language that had the capabilities of Erlang and its ecosystem, with a developer friendly syntax, lowering the threshold for developers to take advantage of the Open Telecom Platform. Since the appearance of Elixir, many companies has taken use of the language and its underlying platform. For instance, Discord uses Elixir and OTP in its chat infrastructure, where they have used Elixir services to serve more than 12 million concurrent users, sending more that 26 million WebSocket events to clients every second. Check out the blog post here.

Language features

Structuring code using functions and modules

Functions in Elixir can be either named or anonymous. Anonymous functions can be assigned to variables, in a similar fashion as Javascript.

Elixir
    
  

Named functions are defined using the def keyword, followed by the function name and the function parameters. The function scope is surrounded by a do and end block.

Elixir
    
  

Named functions must live inside a module. We use modules to group functions together, similar to a namespace in other programming languages, such as Java and C#. Wrapping our function inside a module, leaves us with the following result:

Elixir
    
  

To call a function from outside our defined module, we simply have to specify the module and function names. Greeter.greet(“John Doe”). Structuring code in Elixir is very simple. We create contexts and boundaries using modules which contain functions.

The return value of a function is always the last expression evaluated in the function scope. Given the input "John Doe", the return value of the Greeter.greet function is "Hello John Doe", even though we did not explicitly specify that the value would be returned, which is common on other languages.

Pattern matching

What is usually the assignment operator, = in most languages, is called the match operator in Elixir. The match operator will attempt to match the right and left and side of the = sign, and throw an error if no match can me made. This is useful for assigning multiple values or destructuring a complex object, similar to Javascript destructuring.

E.q., matching a list of unbound values in a list with a list of values on the right hand side, will assign the unbound values.

Elixir
    
  

In the example about, a will be assigned 1, b assigned 2 and c assigned 3.

If we wanted to use the match operator for destructuring, we would use the following syntax.

Elixir
    
  

We initially set the fields name and age in the person map. Afterwards, we match the left and right hand side by saying some field age should be matched to daves_age.

While we are not able to mutate existing data, we can rebind variables with new completely new values.

Elixir
    
  

If we want to disallow variables being assigned when pattern matching, we can use the pin operator. Using the pin operator, the patterns on the left and right hand side will use the variables value to pattern match instead of trying to bind a new value to the variable.

Elixir
    
  

Pipe operator

The pipe operator is used to chain output from one expression as the input to another function, as the first argument.

If we wanted to nest functions and use the output from one function as the input of another, we would have to write something like this in many other languages.

Elixir
    
  

The equivalent using the pipe operator could be something like this:

Elixir
    
  

The pipe operator allows us to easily compose simple expressions into complex ones, while maintaining a readable code structure.

Working with enumerables

Enumerables in Elixir are collections that can enumerate over, such as lists, maps and ranges. Elixir has the abilities to work with enumerables that you would expect to exist in a functional programming language. Three commonly used functions are map, filter and reduce, which all can be found in the Enum module.

Map

In order to map over a collection of elements in Elixir, we would use the map/2 function. It accepts two arguments, a enumerable and a function. The function is applied to each item, producing a new collection based on the functions return value.

If we wanted to double every number in a collection, we could simply map over the collection with a function that doubles each number.

Elixir
    
  

Filter

If we want to filter out some elements from our enumerable, we can use the filter/2 function. Filter accepts to arguments, an enumerable and a function which is evaluated for every item in the collection. If the function returns true, the element is included in a new collection, otherwise it is omitted.

If we wanted to exclude all non even numbers from a list, we could do the following:

Elixir
    
  

Reduce

If we wanted to transform our enumerable to a single value, we could apply the reduce/3 function. It accepts 3 arguments; a collection, an initial value for the accumulator and a function, which is called for each element in the collection with the value and the current accumulator for each step of iteration.

If we wanted to multiply all the values in an enumerable, we could do the following:

Elixir
    
  

Chaining transformations using the pipe operator

Elixir is excellent at transforming data due to its abilities in composability by chaining expressions together. If we had different operations that needs to be applied to a collection in sequence, we could achieve this using the pipe operator.

If we wanted to perform all the operations described above using the map, filter and reduce functions, we could compose a larger expression by doing the following:

Elixir
    
  

If you would like to have a further look at the available functions in the Enum module, have a look at the official documentation.

Control flow

if-else

We have different ways of working with control flow in our Elixir code. One that will be familiar to most programmers is the if-else operator, which does exactly what it says; it branches the flow into a if, and if present, an else block.

Elixir
    
  

case

Similarly, we have the case operator, which compares values against patterns until a matching pattern is found, using the pattern matching capabilities of Elixir.

Elixir
    
  
Elixir
    
  

The case operator will try to match the input with a pattern, and return the expression on the right hand side of the arrow. It is common to have a "catch-all" clause at the end _ -> , which catches all the cases that cannot be matches. This allows us to create case clauses that can remain pure, always returning a value or executing some computation for all input passed.

In the example above, the following output would be produced from the input passed. The three first lines are able to match the right and left hand sides respectively, with the last call evaluated using the catch-all clause.

cond

The cond operator will try to match a set of conditions until it finds the first one that returns true. It will do so from top to bottom. It is also common to use a catch-all clause with the cond operator, to ensure that every input is handled.

Elixir
    
  
Elixir
    
  

In the example above, the cond block will try to evaluate each block from the top to bottom until it returns true. If no blocks return true, we have a catch-all by having a true -> block at the end, which will always be evaluated if none of the blocks evaluates to true.

Assignment

All the control flow operators described above can be used for assignment, even using if-else.

Elixir
    
  

The content of odd_even_message will be assigned either "This is even" or "This is odd" based on the value of x. The same pattern can be used for case and cond, assigning a value based on the evaluation of the control flow operator.

Concurrency

To solve the challenges of concurrency, Erlang has adopted the actor model pattern. The actor model is a conceptual model of computation design to help solve the challenges of computational concurrency. The actor is the computational entity in our software. A single actor can send messages, receive messages, create new child actors or terminate any existing child actors. At the root of our application, we have our root actor, which is responsible for creating more actors which eventually creates a process tree which represents the desired state of our application.

The actor model is implemented in Elixir and Erlang through BEAM processes. Processes in the Erlang VM are lightweight, and must not be confused with operating system processes. They are low-cost processes managed by the virtual machine, which are fast to create, terminate and have little memory overhead. This model allows for applications running on the Erlang VM to scale very well.

Other language feature resources

It would be challenging to write about all the features of a programming language in a single blog post. If you are courious about more of Elixirs language features, elixirschool.com is a great resource for learning, along with the official documentation at elixir-lang.org.

Phoenix

There are many frameworks available for different problem domains, ranging from web development and numerical computing to IoT programming. Phoenix is probably one of the more known frameworks available in the Elixir ecosystem. Phoenix is a web framework, which offers high developer productivity, performance and concurrency. It has become a go-to web framework for Elixir, similar to what Ruby on Rails is for Ruby.

Just like Ruby on Rails, Phoenix allows rapid development with a clear convention on how to architecture our applications. It ships with scaffolding tools, which allows us to quickly create the our application resources.

Phoenix ships with a web technology called LiveView. Using LiveView, we are able to create rich, real-time web applications without writing Javascript. LiveView uses a message architecture, where messages can change the state of the users application. UI changes are reflected in the UI using pieces of static HTML which is sent over websockets and pieces into the DOM in real time.

Phoenix is a battery included web framework, which allows developers to work very quickly in creating applications while maintaining clean code due to its clear convention and great documentation.

Why Elixir?

Elixir with its ecosystem provides a way to create reliable, fault-tolerant and distributed applications, with a low entry barrier for developers. It gives us a programming model with great solutions to concurrency challenges out of the box, with little overhead.

Elixir is a great entry to functional programming. Learning functional programming and the coding style it encourages, may help developers increase their skills in other imperative and object oriented programming languages, e.g. by writing small, pure and testable functions and keeping a clear division between your business logic and side effects.

One of the challenges with using Elixir is adoption and finding developers. While the technology may be battle tested used for decades, without existing adoption, it may often be difficult for stakeholders to take the risk of adopting new technologies. While the risk may be high, developers and stakeholders have a opportunity to define tomorrows mainstream technologies, by not only exploring established options.

As a final note, I highly recommend to have a look at Erlang the movie, a relic from the 90s to advertise the capabilities of Erlang and OTP.