Elixir Wants to Change the Way You Think About Programming

While it doesn't fit every organization, there's a lot to learn from Elixir's approach to programming.

Elixer logo over a river mouth

One of the key benefits of working as a software engineer at 10,000ft is the generous professional development budget. (By the way, if you're looking for a job that supports you to continually learn the newest technology, go to conferences, and participate in the community, look no further.)

To take advantage of these funds, I recently attended ElixirConf 2017 just across Lake Washington in Bellevue. We're not currently using Elixir in our tech stack, but I've long been intrigued by its performance, robustness, scalability, and familiar-looking syntax. So I approached all the talks I saw at the conference, and just about every conversation I had during the breaks, as an opportunity to explore one fundamental question: What can I learn from Elixir's programming model?

In other words, I know the likelihood of adopting Elixir in our production environment is small, given our current team size and problem domains. However, I can still grow substantially as an engineer by borrowing some of the ideas from Elixir, whether it's the language in particular or its approach to functional programming in general.

As Alan Perlis famously noted, “A language that doesn’t affect the way you think about programming is not worth knowing.” Here, we'll take a look at a few of Elixir's features that have deeply changed the way I think about programming. Coming from a Ruby and JavaScript background, those features are immutability, data transformations, and syntactic expressiveness.

Immutable Data and Functional Programming Patterns

The functional programming paradigm is hardly new; indeed, it has been around and gaining steam for some time. While we're interested in the concept, we've had a hard time incorporating it into our Rails code.

Because Ruby is largely a pass-by-reference language, the objects you are passing into methods are the objects themselves, not a copy of their value. This means that if side-effects are performed on these objects, later methods can't guarantee that the object they're operating on contains the same state as it had previously. This has huge implications for concurrency, because if you're passing an object around with shared mutable state, it gets incredibly cumbersome to keep that object in sync across processes and/or servers. With functional programming, however, functions are ideally designed to be small, self-contained programs that operate on or transform data, but because the data is immutable (i.e., cannot be changed or mutated), you can make guarantees about the data being passed around, even if that data is shared across different servers in different regions of the world.

Our React codebase, however, has given us ample opportunity to embrace the functional paradigm. We use Redux to handle our complex state management, and immutability is required by the library. Redux uses equality checking to decide whether changes to the props passed to a component need to trigger a re-render, and equality checks between two variables that reference the same object will always be equal, even if the object has been mutated[1].

Here, like in Elixir, we're realizing that mutable state can create code that's hard to reason about because it's impossible to guarantee the state of any object in the system. When you can make guarantees about an object not being mutated, it opens up possibilities for performance and concurrency that can bring new efficiencies to your programming environment.

Data Transformations: The Bases of Programming

After establishing a strong level of comfort with Ruby, approaching a language like Elixir can be a significant change in mindset. I still remember the feeling I had when I read this worldview-changing sentence from Dave Thomas's seminal text Programming Elixir:

The bases of programming are not assignments, if statements, and loops.

Thomas later goes on to say that programming is fundamentally about tranforming data.

I vividly recall reading that sentence on the bus, looking up from the book, and watching the landscape scroll by as I considered abandoning — or at least fundamentally changing — my entire understanding of structuring a program. It's an opinionated view of the world, but it can lead to some interesting conclusions.

When you start thinking in terms of transforming data rather than objects passing messages to each other, it's often easier to write smaller, more modular code. While this isn't always true, I've started to consider how, rather than thinking in terms of class hierarchies, we can think in terms of small, composeable functions that mutate data and leave no side effects.

For instance, in the Rails request cycle, the app handles a route, which maps to a controller method. In processing the request, the controller will inherit behavior from ActionController::Base[2]. It will likely instantiate a model that inherits from ActiveRecord::Base to implement most of its functionality. If we were rendering templates on the server, Rails would then build an HTML representation of this data to pass to the client through another inheritance chain.

On the React side, a user may click on a button which triggers an AJAX call which will return with data and an event will be fired. Through what amounts to a pub-sub system, a reducer will respond to this event. The reducer is essentially a glorified function which will perform transformations on the data with the use of small, composeable helper functions, and the store will change, which will cause the re-render of a React component. And here's the key: this component is likely a pure function which, given state expressed as an object, will lean on the React.DOM to render HTML based on the data passed to it.

These are two different approaches to creating markup to render in a web page. The notable piece is that, since rolling out our React front end to our first customers in production in the past eight months, the number of bugs arising from changes to a mutable object have effectively been zero.

Syntactic Expressiveness

Elixir allows you to express powerful functional concepts with expressive syntax. But "expressive" is a word that's as commonly used as it is subjective. For our purposes, let's define "expressive" as the ability to say a lot in little space. It's the result of a fine balance between boilerplate and excessive terseness. It's expressive in the sense of being able to communicate the intent of the developer without a great deal of ceremony. And yet, it's important to keep in mind that fewer bytes does not necessarily mean better.

To take a good look at what it means to be expressive, let's take a look at the difference between a sample Elixir program compared with the equivalent Erlang.

% erlang -module(hello_module). -export([some_fun/0, some_fun/1]). % A "Hello world" function some_fun() -> io:format('~s~n', ['Hello world!']). % This one works only with lists some_fun(List) when is_list(List) -> io:format('~s~n', List). % Non-exported functions are private priv() -> secret_info. # elixir defmodule HelloModule do # A "Hello world" function def some_fun do IO.puts "Hello world!" end # This one works only with lists def some_fun(list) when is_list(list) do IO.inspect list end # A private function defp priv do :secret_info end end

There are a number of things going on here, so we'll point out the aspects that feel related to our discussion of expressiveness.

First, the Elixir example looks familiar to developers coming from Ruby: functions are set off with the def and end keywords and are nested in some type of larger structure (a module, which is a familiar concept to Rubyists, even if it's not exactly analogous to Ruby's module). There's also a puts function, and it's even clear from the first function that functions that don't take arguments don't require parenthesis. These may seem like small and inconsequential details, but Matz, the creator of Ruby, was clearly onto something when he designed the languge for developer happiness. The syntax doesn't get in your way. Elixir's creator, José Valim, was a member of the Rails core team, and Ruby's influence on much of Elixir's core syntax is clear.

Unlike the Erlang example, the Elixir snippet doesn't contain a lot of punctuation and non-obvious operators (~). It also explicitly differentiates between public and private functions, whereas the Erlang version requires a comment. Elixir visually denotes the containment of the functions in the module with nesting, whereas Erlang defines its module with a declaration at the top of the file. Finally, in Elixir, the interface to the IO module takes a string. This is more intuitive than the Erlang version, which takes a series of characters, and apparently requires a list, even if you just want to print a string.

In sum, then, it's clear that Elixir is able to express the same ideas as the Erlang code (the two code snippets should theoretically compile down to the same bytecode), but it's able to do so in a syntax that promotes productivity without being overly laconic.

Pattern Matching and Guard Clauses

Pattern matching is an absolute cornerstone of Elixir development – one that, as a Rubyist, I found the most difficult to wrap my head around initially. One of the first mind-expanding experiences for many people coming to Elixir involves the pattern matching operator. It's one we've seen in most other languages: C, C++, Java, PHP, Python, JavaScript, Ruby, and others:


When a developer coming from another language sees this operator, she thinks of assignment. The Ruby assignment operator likely appears in the first few pages of any introductory Ruby text:

a = 1

Here's where it gets weird. That code will run in Elixir. And after it's run, and a is later evaluated, it will indeed return 1. But that's only because Elixir was able to find a matching pattern. If the arguments had differed, Elixir would've thrown an error rather than assigning. This is a concept best illustrated:

# elixir > [a, b] = [1,2,3] # => ** (MatchError) no match of right hand side value: [1, 2, 3] # ruby > a, b = [1,2,3] > a # => 1 > b # => 2

In the first example, Elixir couldn't find a match for the 3 value in the list, on the left hand side of the expression, so it refused to assign. Ruby, however, assigned what values it could from the array, and ignored the rest.

You could argue that this is a point in favor of Ruby's flexibility, but pattern matching has huge implications for the language. Consider how you can augment pattern matching in a function signature. In Elixir, this is expressed through a guard clause, a close relative of pattern matching.

Here's a simple RecursiveSum module:

defmodule RecursiveSum do def sum(0), do: 0 def sum(n) when n > 0 do n + sum(n - 1) end end

This module implements factorials (4! = 4 * 3 * 2 * 1) with a guard clause in the function signature. Here, rather than using control flow within the function body, it's right in the function signature. When n is 0, the code path in the second function definition isn't even run. Getting into fewer expressions means code that's easier to reason about. And if you can implement this with an elengant construct like a guard clause, that's the essence of expressiveness.

Pipe Operator

It's hard to talk about Elixir's expressiveness, or as Dave Thomas calls it, "The Magic Pipe Operator." Developers with a background in Linux/Unix will recognize the concept of pipes, which take the output of one operator and send it to another. It's no different in Elixir, where each sequential function call in a pipeline receives the return value of the previous function call as its first argument. Let's take a look:

items |> add_sales_tax |> calculate_order_total |> send_confirmation_email(customer) |> redirect_to_homepage(customer)

This is a succinct way to express a sequence of events. It also reinforces the idea that programming is about transforming data. This code reads like synchronous code, but any of these operations could be asynchronous because we know that, for instance, customer is going to come out the other side unchanged.

Further, it encourages developers to think of the functions they create as composable units. In fact, that's the principle behind Ecto, the DSL for writing queries in Elixir. Consider this (slightly modified) example from the Ecto docs, describing the ten lowest temperatures:

def ten_lowest_temps_in(city) do Weather |> where(city: city) |> order_by(:temp_lo) |> limit(10) |> Repo.all

It reads quite nicely, and you're able to combine the clauses of queries in this manner without executing them until you hand them off to the Repo module.

The Limits of Our Worlds, Expanded

I've found that even dabbling in a new programming environment can cause you to think about the codebases you work in daily in a new way. While the chances of us undergoing a wholesale rewrite of our main app from Rails to Elixir and Phoenix are incredibly small, the benefits of exploring Elixir have been vast for me. Wittgenstein famously said, "The limits of my language are the limits of my world." If our worldview is limited to the object oriented paradigm, there are thoughts — and, by extension, new and more creative ways of solving problems — that we're simply not capable of having. A new language as different from Ruby as Elixir can fundamentally challenge our worldview. And that change can bring about new approaches to incorporating these great ideas into our existing codebase. We can also take advantage of some of these patterns and practices to make our code more performant, easy to reason about, or maintainable. It's those qualities, I'd argue, that contribute the most to developer happiness.


[1] For an interesting discussion of this, see Redux immutability. [back]

[2] In our case, the controller in question is likely an API controller that inherits from a base API controller, which inherits from ApplicationController, which inherits from another controller that implements our authorization logic, which in turn finally inherits from ActionController::Base. [back]

Perfectly reasonable business advice, delivered to your inbox.