Ivan Yurov is a friend of the GenUI family and is in Seattle for a summer internship.
As a Master’s student at EPFL — the Swiss Federal Institute of Technology in Lausanne — he not only has experience working with Elixir but also shared a new perspective on this language and its exciting possibilities in his August 2019 Tech Talk.
I’ll get to the point: Elixir is awesome and it’s easy to come up with more than ten reasons to love it. I just limited this to ten for the sake of an introduction – think of this as your Elixir appetizer. I encourage anyone interested in learning more to look further into what’s on the menu.
1. Beam VM
Beam is a virtual machine, similar to JVM but much smaller, simpler, and super optimized for resilience. Originally built by Ericsson, Beam VM was intended to power communication equipment (meaning that, by design, it was reliable and resilient off the shelf). The main benefit for us, since we work in web application development and deal with serving many requests from different clients, is that we can spawn as many processes as desired because they’re lightweight. It’s similar to Green Threads, Fibers, Coroutines… pretty much any language has a similar concept.
Each Elixir process is a virtual thread that is scheduled across some number of physical threads and Beam VM efficiently manages them.
“Originally built by Ericsson, Beam VM was intended to power communication equipment (meaning that, by design, it was reliable and resilient off the shelf).”
I like to let the numbers do the talking: if you try to run anything on a Java virtual machine (JVM), it requires a minimum of 300-500 megabytes. Then it will say something like, “OK, I’m out of memory,” or, “Sorry, busy with garbage collection.”
But Beam takes as low as 30 megabytes to run — it’s that lightweight.
2. Actor Model and Message Passing
Beam relies on an actor model. Every concurrent abstraction from OTP (the main library of processes in Erlang) is based on actors. An “actor” is a process that can accept and send messages to other processes, allowing entities to talk to each other and build more complex compositions of processes.
“One of the most important properties of actors is that you have a mailbox for incoming messages so there is no way that actor processes more than one message at a time. This is amazing for making concurrency reliable and to avoid a condition race or similar issues.”
Here is how processes in Elixir and Erlang work and a basic example of how it’s implemented. The core of an actor is a “receive block” — a program that waits for an incoming message, which is being pattern matched on arrival, and then leaves the block. To make it an actor we just need to loop back to listening. Here’s an example of catching a single message, notice how it was actually sent before running receive block, mailbox is actually storing messages before they are actually processed.
Having rerunning that loop over and over makes it possible to even have some state that can be updated upon incoming message. Within the processing code in the loop, you can send messages to other processes the same way. One of the most important properties of actors is that incoming messages are queued in a mailbox so there is no way that actor processes more than one message at a time. This is amazing for making concurrency reliable and to avoid a condition race or similar issues.
3. Resilience and Reliability
Elixir/Erlang follows “let it crash” paradigm. Now, people tend to misinterpret that and say, “Okay, this crashed. I have some bugs in my code but I am going to keep it this way and not catch errors.”
This is not the desired interpretation here.
The “let it crash” paradigm means that that, yes, your code might crash in some particular place, and that might crash your process, but Elixir and Erlang have helpful internal tools like native Supervisors that allow you to build the process tree and set up different recovery strategies. So if your process crashes, it can help recover to some extent. It helps if the error is rare and purely depends on external factors, but if the code itself causing this, restarting the process indefinitely might not be a right thing to do. For the cases like this Supervisors offer various policies, such as propagating the crash if it repeats with some defined frequency. This functionality is embedded into the language as an internal feature allowing you to cheaply and easily build applications that heal themselves and use callbacks that allow you to analyze what happened and perform curative actions.
4. Immutability throughout
A quick confession that this reason is my favorite (and especially appealing for people coming from Ruby). Elixir looks similar when you view it at the syntax level, but it’s entirely different because there’s no mutability in the language at all! None. Zero. You cannot reassign a variable. You can only assign it once and process it. Now, you can reuse the same name of the variable but it’s going to be completely different variable, this is called shadowing. How is shadowing different from mutation? Shadowed values are accessible in the same scope, while mutable variables can bring changes to some region of memory across different scopes.
Okay, but how do we deal with state? Remember: we have actors that might maintain an internal state. If you want something that contains your precious piece of information but you need to mutate it somehow, you use a special process provided by OTP. It’s called Agent and is extremely simple to use. You spawn it, get the process ID, and every time you want to update the state, you send messages there with a new value. Mailbox guarantees that messages are coming one after the other and it helps us make sure there are no condition races or other weird problems.
“It’s much easier to build a language that doesn’t support mutation, its intermediate representation is much easier to optimize.”
The benefits of this approach are obvious. It’s much easier to build a language that doesn’t support mutation, and its intermediate representation is much easier to optimize. For users of the language, it’s also beneficial: if you know that there’s no mutation then you know that your function will either be pure or have external side effects related to data access or a request to other services — all of which are under control with proper testing setup. Pure functions, ones that take a value and return a value, are easy to test, because they are completely independent of the external state of the universe. Besides classic unit testing, I’d like to mention so called property-based testing, which is perfect for testing pure functions. Property-based testing for Erlang and Elixir is provided by PropEr testing framework. The key idea that instead of providing concrete test-data, we describe rules which function is expected to follow. For instance, if I have a function that appends element to the list, instead of asserting append([1, 2], 3) == [1, 2, 3] I can think of properties that have to be true, such as that length of resulting list must be one element longer than the source list. PropEr then stochastically generates testing data and runs it against the actual function implementation. There’s some heuristics helping to specifically address edge cases.
Testing processes can be a bit more difficult because all you get is a process ID and there’s only an interface it provides. Sometimes it might be useful to have a remotely controlled process that will serve as a probe, by translating everything it accepts to the main testing process. An attempt to solve this issue is a little library called TestProbe.
5. Pattern Matching and multi-clause functions
Here is an example of an ALU implementation from one of my labs in a computer architecture class.
These are actual MIPS instructions. First two arguments are already converted into flags, but the last argument is still a plain binary string containing an OpCode. Elixir allows for multiple function clauses (sort of overloading but much more flexible). First clause implements addition, second is for subtraction etc. Pattern in argument describes how the string should be composed, so that pattern was considered a match. This should be read like follows:
- Take first 32 bits and assign them to `l` as signed int
- Take next 32 bits and assign them to `r` as signed int
- Ignore next 26 bits
- Check that next 6 bits equal to 100000 (in binary representation)
You can use pattern matching everywhere — assignment, function signature, case expression. As was demonstrated with ALU OpCode parsing, you can overload functions in a super-advanced way that allows you to create as many functions as desired per use case.
Beware though, while pattern matching can be used in assignments, I urge you not to do that. Unmatched patterns simply crash the process where it occurs. I only go that route in tests where I want it to crash.
#6. Macros and metaprogramming
This is part of a library I created to mimic Result and Option types that are widely used in typed languages like Scala and Rust. It’s a simple macro that allows you to type, okay(some_value) and that expands to tuple. Basically when the code using these macros gets executed by Beam VM it’s completely cleared of all the magic.
The quote keyword here is how macros are defined. Whatever is inside the quote, is taken as is and unwrapped whenever the macro is used. Unquote allows you to embed some values from the scope of usage.
There are some difficult limitations. For example, you cannot pass lambda to macros because lambdas might depend on external scope (relative to its declaration) and macros cannot capture it.
#7. Dialyxir and static analysis
Elixir is amazing and I hope it’s already obvious, but nevertheless I’d like to see it statically typed. I like type systems (despite the fact that I can’t claim to understand them properly). I learn them using Scala — the language that’s working on JVM — which is known for having a super-advanced type system. Plus, Scala 3 is coming this year or the next and will take the type system to another level. Type systems give you guarantees or, at least, more confidence in your code. They catch most of the errors at compile time.
Elixir is still somewhat inadequate in this regard and the question remains of what can be done about that. Fortunately, someone came up with a static analysis tool and annotations that allow you to mimic typing and check it on compile time. This works well and shows whether you call the function with incompatible arguments or you’re doing something completely legal. It looks like type definitions (including type variables) so you can construct more complicated types and use structures of the language inside of that definition.
Here is an example of the full typing for how we defined macros from the last section.
There are two sides to it. One is a successful value. The other is an error. It binds them together. When you construct a more complicated spec, it helps not to lose or weaken the shape of your data.
#8. Full compatibility with the Erlang ecosystem
Elixir is based on Erlang (which is powered by Beam VM). Erlang is from the late 80s. Elixir is younger and started as a macro dialect of Erlang. It’s now almost independent and Elixir people are actually contributing to the Erlang community and Beam VM development.
We can use any library from the Erlang ecosystem inside of Elixir code without changes or adaptation and, since they share the same type system, things remain absolutely transparent.
#9. Amazing libraries and mature ecosystem
Elixir also has many cool libraries to offer and is still getting bigger in this regard. When it started, we mainly had to use Erlang libraries but their syntax can be convoluted. I’m going to talk about these amazing Elixir projects, that are mainly responsible for a growth of popularity of the language. The winners are obviously Phoenix (web application framework) and Ecto (SQL and not only data layer that is used in Phoenix).
Ecto and the structures it operates are much lighter than Rails’s Active Record. Record instantiation in rails introduces significant overhead, with Ecto it’s instant, because it operates purely functional structure. All operations are provided as modules and functions (honestly, everything in Elixir is modules and functions).
Flow is another interesting library for processing streams using a backpressure technique. Whenever you have computing powers process something, you just say, “Okay, give it to me.” (If you’re interested in learning more then you should google “benchmarks of flow”. People are managing to handle millions of items on one machine in a short time.)
Swarm is a library that simplifies the management of clusters. Beam VM is not only great at managing internal processes but it’s also great in combination with other instances of Beam VM. They pretty much talk to each other natively. You can spawn as many instances of Beam VM as you want and connect them together into a cluster.
This last reason is probably the most important. Most of the Elixir community came from Ruby and the Ruby community is famous for being friendly and open. I don’t know what community can compete with Ruby in this way. C/C++ people won’t even talk to you if they suspect that you’re a newbie; with Scala people, you need to have a Ph.D!
“Elixir … has [a] nice, open spirit. Elixir’s creator Jose Valim will personally answer questions …”
Elixir, however, has taken after Ruby’s nice, open spirit. Elixir’s creator Jose Valim will personally answer questions … even stupid ones. And we know that everyone asks these kinds of questions. I once asked just such a question (or two) and he responded personally to me. Maybe this should have been my first reason because community accessibility and their willingness to engage in dialogue and help are essential features of this fantastic language.