Some say that life is too short to do everything, on the contrary, it’s also too short to stay in one place and not explore. Especially for software developers, there are so many ecosystems to explore, like node (aka JavaScript), JVM (aka. Java, Groovy, and friends), and CLI (aka, .net, C#) to name a few… from the mainstream.
But when we zoom out, all of them are kind of the same! All use C-like syntax, all are stack-based, all are primarily mutable and for the most part, solve problems similarly. So even if you explore one of them you more or less stay in the same mainstream paradigm. On the one hand, that’s good as you get up to speed faster and you can tap into your experience from the other ecosystem.
Arguably, the most growth lies beyond the mainstream, where you must bend your mind and sometimes even question your previous experience.
The question here is if you want to bend and squeeze the tools you know (like Facebook did with PHP)… Or would you rather prefer to challenge your brain and learn the proper tool for the task? You can answer this for yourself, in your own time. In this blog post, I’ll give you a few highlights from my mind-bending journey into the land of Elixir.
Let’s start with “why”… Why Elixir?
Have you ever heard Java’s initial catchphrase? “Write once, run everywhere”, neither it says anything about “running forever” nor about “running efficiently” or “run on a cluster” either. But it’s now used for all of the possible use cases, from desktop apps to cloud systems.
Whereas Elixir runs on BEAM VM, which also powers Erlang. Which was created by Ericsson with the sole purpose of running telephone centers. It was designed to run forever in clustered mode, with failovers built-in and task separation to limit the scope of failures (that will inevitably happen).
What Elixir adds on top is a “syntactic sugar” with Ruby-like syntax that is easier to learn and understand compared to Erlang. Additionally, it also removes a lot of boilerplate code compared to Erlang.
When do you want to consider Elixir then? When the thing you’re going to build runs 24/7/365 with error isolation, online upgrades, scalability, and performance… Sounds familiar? Or is my explanation still too cloudy?
Pose 1: Pattern matching
Let’s start from the basics: assignment operation. You may wonder how this could be mind-bending… Stay with me 😉
Probably the first thing we learn when we start our journey with coding is assigning variables. After initial struggles, it becomes second nature to assign a variable. In JavaScript, Java, C#, or any other C-like language it’s always:
const myVariable = 6
or
final String hello = "hello"
No surprise here, in this (simple) case it’s also how it works in Elixir, and it’s even less typing:
myVariable = 6
hello = "hello"
Easy, right?
Let’s raise the bar a bit higher and move to destruction. If you wrote any (modern) React code, for sure you used hooks. So to declare a mutable state you’d do:
const [value, setValue] = React.useState(42)
Let’s jump for a moment to Go-lang, as you may know, it doesn’t have exceptions, instead, each function will return a tuple with the first element being a result (or nil) and the second, being an error (or nil).
Elixir has tuples also, but they usually use :ok, and :error as the first elements to distinguish between success and error. With this in mind…
{:ok, file} = File.read("./existing_file")
above code should be self-explanatory. We’re reading file content from the disk and storing it in file variable. But what will happen when the file doesn’t exist? You may recall that File.read will return :error as the first element of the tuple. But what happens then? Let’s try
{:ok, file} = File.read("./NOT_existing_file")
** (MatchError) no match of right hand side value: {:error, :enoent}
(stdlib 4.2) erl_eval.erl:496: :erl_eval.expr/6
iex:1: (file)
MatchError, “assignment” has failed! As this is not an assignment it’s a pattern match! We can “easily fix this” with:
{:error, reason} = File.read("./NOT_existing_file")
Now, this code will not produce the MatchError.
What’s more, you can use pattern matching and destruction in the function parameters! That’s even the preferred way of writing “conditional” logic. Let’s examine this by implementing a sign function. To recap, the sign function returns -1 for all arguments lower than 0, 0 when an argument is 0, and 1 when it’s greater than zero. So in JS, it would look like this
const sign = (arg) => {
if (arg > 0) {
return 1
else if (arg < 0) {
return -1
} else {
return 0
}
}
Easy! Let’s now rewrite it in Elixir
defmodule Example do
def sign(0), do: 0
def sign(arg) when arg > 0, do: 1
def sign(arg) when arg < 0, do: -1
end
> Example.sign(11)
1
> Example.sign(-89)
-1
> Example.sign(0)
0
Mind blown? We’re “overriding” or “shadowing” sign function with different pattern matches and guards (the “when” keyword is called a guard).
This mechanism is really powerful, as you can pattern-match and destruct arguments in one go. You don’t need to have a complicated if-elseif-else block, instead, you get (powerful) pattern matches. It takes some time to get used to it… but when you do, you miss that in other languages, as the function body (with all that matching) is short and easy to comprehend.
Pose 2: Let’s talk about scopes!
We all know how variable scoping works. Even with fu*ked-up scoping in JS, over time we learned how to use it to our advantage. Let’s engine that simple JS code then:
const sumUp = (input) => {
var acc = 0
input.forEach((i) => acc = acc + i)
return acc
}
Now let’s rewrite it in Elixir
defmodule Example do
def sum_up(input) do
acc = 0
Enum.each(input, fn i -> acc = acc + i end)
acc
end
end
> Example.sum_up([1, 2, 3])
0
If we try to use the JS version it will gladly sum up all of the elements of the array. But the Elixir version will always return 0… Why?!
Elixir is a bit special with variable scoping. Meaning that you can read variables from parent scopes, but if you try to override any of them, the overridden value will be only visible from that point below. Meaning it will never override values in parent scopes! That’s why we always get 0 from our Elixir implementation. BTW will you be able to rewrite the Elixir version in a way that it will behave like JS? Could you post your version in the comments?
Pose 3: Managing immutable state
Elixir is immutable by default. All data structures are immutable, the only way to change them is to return a new value from a function. Although all that we do in programming is just data processing, meaning data is coming in we do our magic (aka mutate it) and data is coming out. But we still need to have some mutable state, for instance, to compute and hold a cart value for users in an e-commerce app. But when everything is immutable we can’t do that, right?
Yes, that’s right, but there is a GenServer interface that allows us to mutate immutable data (as counterintuitive as it sounds).
Let’s see how we can implement a counter with GenServer
defmodule Counter do
use GenServer
def start_link() do
GenServer.start_link(__MODULE__, 0, name: :counter_example)
end
@impl true
def handle_call(:value, _from, state) do
{:reply, state, state}
end
@impl true
def handle_call(:increment, _from, state) do
state = state + 1
{:reply, state, state}
end
@impl true
def handle_cast(:increment, state) do
# state = state + 1
{:noreply, state + 1}
end
def value() do
GenServer.call(:counter_example, :value)
end
def increment_sync() do
GenServer.call(:counter_example, :increment)
end
def increment_async() do
GenServer.cast(:counter_example, :increment)
end
end
> Counter.start_link()
{:ok, #PID<0.150.0>}
> Counter.value()
0
> Counter.increment_sync()
1
> Counter.increment_async()
:ok
> Counter.increment_async()
:ok
> Counter.value()
3
We used an integer to keep track of the state. But it can be any data structure like a list, map, struct etc. From the @impl annotation, you may guess that those are implementations of GenServer API and you’re right. The value, increment_sync, and increment_async are so-called interface functions to hide the usage of GenServer. The GenServer.call and GenServer.cast functions take two arguments, the first being a process PID or its name and the second is a message that is being sent to that process. So we’re sending messages (aka actions) to our implementation of handle_call and handle_cast. Both function implementations are pure, meaning that they don’t modify anything, they just return the next valid state for that process.
Talking about the state management, actions, and maps… Doesn’t that GenServer look somehow familiar? Have you heard about a library that uses actions (plus reduces), and a map to keep track of state? … Redux?
Pose 4: Everything is a process
I’ve already mentioned processes while explaining the Counter example. But before we dive deeper, let’s have a quick recap.
We all know that each application in the system is backed by (usually) one or more processes. You may have heard of “fork”s, which would create a copy of the current process to run concurrently, the thing with forks is that they are slow to create and also expensive, as it is a full clone of a process, that includes the memory, file descriptors, etc. What’s more, there is no simple way to communicate between forks.
Then we have threads, which are an improvement over the fork model, as they share the memory with the parent and are created within the process itself. But as the memory is shared, it’s also easier to communicate between threads, as they can just modify the shared memory… and that brings us to the famous concurrency bugs that are hard to reproduce and fix.
Can we do better?
The inventors of BEAM VM thought that we could… Similarly to JVM the BEAM VM will allocate a single chunk of memory for itself, and also it will manage its own internal processes and communication between them.
To make things “simpler”, an internal process in the BEAM VM is called… process. Each process takes ~2 KB of memory (on a 64-bit system). The only way to communicate between processes is to send immutable messages. That means that no memory is shared and each process can consume messages (and send responses) in its own time and not worry that its state is being modified by another process.
Sounds easy and pretty much standard? Wait, there is a twist!
Processes are the main building blocks of Elixir (and Erlang) applications, they are also organized in so-called “supervision trees”. There are two types of nodes in a supervision tree: worker and supervisor. The worker will do the work, but the responsibility of the supervisor is to keep track of the workers. When a worker fails (crashes etc) then (depending on a policy) the supervisor can restart that single process or restart all processes in that node.
This means that a single problematic input data will not bring down your whole app! Errors are scoped to that single process and its supervisor. If the process keeps failing after a few restarts, the supervisor will give up and won’t even try to start it again.
Summary
If any of the above looked odd or unintuitive to you, I would highly recommend giving Elixir a try! Why do you ask? As with the similarities between GenServer and Redux, you may find similar ideas being used in different ecosystems. It then makes it easier to grasp them. A good starting point is the Getting started guide and the book Elixir in Action by Saša Jurić.
In case I’ve got your interest in Elixir, you’ll probably be glad that there’s much much more awesomeness in it and BEAM. Like it is by design cluster-able, there is also a “telemetry” module to monitor your instance(s) and integrate with, hot code deployments, REPL (or interactive shell), mix (build tool and dependency manager) etc.
Hope you learned something interesting.