There’s an abundance of back-end technologies you can pick from, the most popular being Java, C#, Node, and plenty of others. But there’s a nice functional language, a hidden gem, that rarely gets talked about – Elixir.
Even though it’s rarely the first thing that comes to mind when thinking about which language to pick, Elixir is the second most loved language, according to the 2022 StackOverflow Survey, while Phoenix, Elixir’s most popular web framework, ranks #1 among the most loved web frameworks. Let’s see what this hype is all about.
What is Object-oriented programming (OOP)?
Before we get to Elixir, let’s briefly talk about OOP. What is OOP actually? According to Wikipedia, it’s a programming paradigm based on the concept of objects, where we map real-world objects and processes to digital counterparts. Typically, we use classes to define objects. That’s pretty much what the modern concept of OOP is, and the most popular answer that you will get in a junior Java interview. But the original idea of OOP was quite different.
“Object-oriented programming” was coined as a term in the early 60s by Alan Kay, a biologist, and a computer scientist. His main idea was to replicate the way biological cells communicate with each other by sending messages, while each cell has its own internal state, which is not visible to other cells.
This concept proved to be quite useful. In fact, that’s how the Internet works – we have many devices that send messages to each other while hiding their data. Microservices also use the same pattern – each microservice has its own database while communicating with other microservices by passing messages to a queue, or by directly calling each other via API calls.
But where are the classes?
But the above description doesn’t mention anything about classes. If you think about it, classes are a weird structure – you can have data (fields) and functionality (methods). But you can also have static fields/methods, that don’t belong to a particular object instance, you can have nested classes, enums, and so on. That makes the class a container for code, rather than a language construct with a single purpose, which, ironically, violates the single-responsibility principle, one of the core principles in OOP. As James Gosling, Java’s creator put it, “If I could do Java over again, I would leave out classes”.
Let’s also think about the following quote by Alan Kay: “OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things”. We see that the original idea has nothing to do with classes, and it’s all about hiding the internal state (encapsulation) and message passing.
Let’s talk about encapsulation
But what exactly is encapsulation? In Java, we typically implement encapsulation by wrapping private property with public getters and setters.
Bad encapsulation (left) and good encapsulation (right)
If we zoom out a little bit, what is actually the difference between the two code samples? The answer is: there is none. Both pieces of code produce the same functionality, it’s just that in the second example we write more code. By providing public getters and setters, we expose the internal state of the class and we do not have encapsulation.
The whole point of encapsulation is avoiding shared mutable states. From my experience as a developer, around 50% of the bugs come from having a mutable object being passed around, where each method can set a property on that object, and there’s no control of that object’s state. How do we achieve proper encapsulation then? Forget about getters and setters, they’re not the answer. The best solution is to have immutable data structures (we have those in the form of Records, starting with Java 14). We’ll see how Elixir solves this problem a bit later in the article.
It all started with Erlang
But before we get to Elixir, we’ll have to briefly look into another language – Erlang.
In the 80s, telecommunication companies faced a scalability problem, since a lot of people started extensively using their services. The telecoms needed a software solution that is scalable (so millions of people can use their phones at the same time), fault-tolerant (if a phone tower was down or a cable somewhere was cut off, this shouldn’t bring the telephone system of the whole country down) and highly available (so users can access the telephone systems at any time).
The solution to this problem was BEAM – a virtual machine, developed by Ericsson, that had all of these properties (we’ll see how BEAM achieves all of this a bit later). The language used for writing applications for BEAM was Erlang.
In fact, Erlang is still used today. Applications with millions of users, like WhatsApp and RabbitMQ, are written in Erlang. Also, it’s being used by about half of the telecommunication companies. But if the Erlang language is so powerful, then why it’s not so popular? It’s because of the syntax:
Hello world in Erlang
From the above example you see that, while not the ugliest language out there, it definitely looks weird.
In 2012, the Elixir language was created by Jose Valim, a Ruby programmer. Jose was a huge Erlang fan, so he combined the best of both worlds – the power of Erlang’s platform (BEAM) and the simplicity of Ruby’s syntax. Elixir quickly became popular with companies that needed to support millions of concurrent users and is used by Discord, Spotify, and Pinterest.
Hello world in Elixir
Elixir: Main features
Elixir is a functional language that runs on the BEAM virtual machine. Some of its main features are immutable data structures, pattern matching, REPL, doctests, and more. Let’s look at some of them in more detail.
There’s a huge debate about which one is better: functional programming or OOP, with a lot of good arguments on both sides. I wouldn’t engage in that battle, but coming from OOP languages like Java and C#, it was a huge relief for me that with Elixir you focus on functions (business logic) rather than on object instances.
Java (left) vs Elixir (right)
Let’s imagine the following case: we have the ID of a newly registered user, and we want to send them a welcome email. In the Java example, you’ll need at least 3 dependencies: a repo to get the user information from the database, an email factory service that generates the email body, and an email sender service that actually sends the email. You’ll have to make sure those classes have instances that are managed by the dependency injection container, and you’ll have to manually inject those into your service. In the Elixir example, you simply call those functions from their corresponding modules – you don’t have to worry about instances. As a bonus, you get the pipe operator (|>). It has a simple usage – take the left operand and pass it as a first argument to the right operand, which should be a function. This allows you to easily pipe functions in the same order that the data flows and makes the code much more readable.
The real encapsulation
In Elixir everything is immutable!
Immutability in Elixir
In the above example, we define a map with a single property. When we try to update that property, we get a compilation error. In order to update it, we need to call a special function that takes the map as an argument, as well as the property name and the new property value. Rather than updating the property, the function returns a new map with the property set to the new value. This is the best way to achieve encapsulation since you cannot modify an object that is shared across multiple functions.
Pattern matching is a popular technique in a functional language. It allows us to check whether an object matches a given structure, so we can make our control flow statements cleaner. We can also use pattern matching to de-structure an object and take whatever we need from it.
Consider the following examples:
We have a function that accepts an object as a parameter, then performs some logic based on that parameter. In the first piece of code, we use a traditional if/else approach, while in the second example we use pattern matching. The latter looks much cleaner, and the logic is easy to follow.
Elixir takes this even further, allowing us to use pattern matching in function definitions:
Pattern matching in function definitions
This way, instead of having one big function, we have several smaller ones, which makes the code even more readable. Another advantage of the above approach is that if we want to add another condition, we simply create a function with the appropriate pattern, and that’s it! No need to modify complex if/else chains.
Let’s be honest, nobody likes to write documentation! And half of the developers don’t like to write tests as well. With doctests, we can make both easier and more fun to write.
Let’s consider the following case: we sell movie theater tickets, and we want to calculate the discount percentage based on the user’s age. Non-adult users (younger than 18) get a 20% discount, while everybody else gets no discount. This is how the code would look in Elixir:
Calculate the user’s discount percentage
We have a function called calculate, which takes the user as a parameter, and returns the discount percentage. We have added documentation for that function with the @doc annotation, and we also give an example of what would happen if we call that function in the Elixir shell (we start the shell with the iex command) with some sample values (a user aged 15 and a user aged 30).
Great, we have our documentation, how do we write the tests now? Well, those are our tests! When we execute the mix test task in our project, the test runner will recognize those examples in the documentation as tests and will run them. Those are doctests, they save us time, and with them, we are sure that the documentation and the tests are up to date with the actual code.
BEAM concurrency model
Earlier, we talked about Erlang/Elixir’s high availability, scalability, and fault tolerance, but we never explained how it all works. The secret is the BEAM virtual machine and its concurrency model.
In Java, if we want to run two tasks in parallel (eg. two database queries, two API calls), we use threads. When we create a new thread in Java, that maps to a new thread on OS level, which makes it a heavy operation (one OS thread is around 1MB in memory), and we also must rely on the OS for managing the thread and doing the context switching, which is quite slow. That’s why many languages have the concept of “green threads”, which are lightweight user-space threads (we can have many such threads in a single OS thread) managed by the platform itself. For example, in Go we have goroutines, in C# we have tasks, in Kotlin we have coroutines, and so on. In the latest Java version (19 as of the current date) we have virtual threads as a preview feature. That improves quite dramatically the performance, especially when we have many I/O operations that can be run in parallel.
In BEAM languages (Erlang, Elixir) that concept is taken to the next level, by introducing “lightweight processes”, which are extremely efficient (one process can be as small as 0.5KB). BEAM creates a scheduler for every OS thread, and each scheduler can run and manage many processes.
Beam concurrency model
Another key feature of BEAM processes is that they don’t share any state with each other, or with the rest of the application. They are completely isolated, and if you pass them data when you initialize them, they receive a copy of the object, rather than a reference. The processes can send messages to each other if they want to exchange information.
So, we have isolation and message passing… does that sound familiar? Yes, those are the core principles of OOP, as it was imagined by Alan Kay. In fact, one of Erlang’s creators, Joe Armstrong, was inspired by Alan Kay’s ideas. That’s why we can say that Elixir implements the original idea of OOP much better than modern OOP languages like Java or C#.
Concurrency model benefits
Here are some of the advantages that isolation and message passing give us:
With no shared state, we don’t have to worry about locks, which are a slow operation. In fact, in Elixir you don’t have to implement the traditional concurrency patterns, like mutexes or semaphores, since the processes work with copies of the data.
When a process is completely isolated, that means that it cannot affect other parts of the system if it terminates unexpectedly, and it cannot bring the whole application down (in Go, for example, an unhandled panic in one process can crash your app).
A process in BEAM can send a message to any other process, even if the receiver is part of another node of the same application. Coupled with the fact that each process works with a copy of the data, that means we can easily distribute work across application nodes. In fact, Erlang has some built-in modules for load balancing that we can use out of the box.
Since each process is isolated, we can replace modules of the system without having to stop the application. The processes that are already running will continue using the old modules and their copies of the data, and won’t be affected by our changes. This is extremely helpful when you have millions of users using the application at any time, and you want to eliminate downtime. WhatsApp uses hot deployment for releasing new versions of their popular messaging client.
With each process starting from half a kilobyte, a single instance of BEAM can run up to 268 million processes at the same time
Elixir: A success story
One of Accedia’s clients was on the verge of launching their first public product, but they faced a challenge: they had an enormous dataset (tables with hundreds of gigabytes in a relational database). The data was already used in an internal application, where the main language was Ruby. The database layer was as optimized as it gets: indexes, partitions, raw, and optimized SQL queries, but the requests were still slow.
This is when Elixir came to the rescue. We used Elixir and Phoenix as the application framework, which are similar syntactically to Ruby and Rails. The difference, however, is in the parallel processing capabilities: with Elixir, we were able to perform multiple smaller and faster database requests in parallel, then we would process the data on the server. This resulted in a huge performance improvement, where the user would wait just for a few seconds, not minutes.
Why Choose Elixir?
In summary, if any of the following points are important to you:
- Code clarity
- Fewer bugs
- Development experience
Тhen you should definitely consider using Elixir for developing your next application. With its growing popularity and expanding community, it’s a good bet on the future.
If you want to learn more on the topic of Elixir and how it can be used in your software development project, please don’t hesitate to reach out!