Our team has been using Elixir for the past 24 months, and I thought it would be a good idea to write down my thoughts on Elixir vs Ruby and why our engineering team decided to stop programming with Ruby altogether and put all of our efforts behind Elixir.
I talk in full details about our Elixir VS Ruby experience here, but for this post I want to focus on the following:
- Development Velocity
- Safety and Correctness
- Hiring Quality
Let’s be honest — we are excited about Elixir’s effortless handling of concurrency, but it’s not the main reason we stick with it. Elixir is a highly-productive language. Here are some reasons for that claim:
Simplified development setup — Utilizing the BEAM virtual machine, one can expect to have a consistent development experience on almost any machine—even Windows. While this can also be true for Ruby, as a project progresses, a typical web application in Ruby accumulates more dependencies that make it increasingly difficult to get it up and running, while an Elixir web application would remain relatively easy to set up.
Predictable codebase — Elixir enforces true lexical scoping and encourages explicit codes. What this means at the code level is that Elixir makes data transformations, code organizations, and application interfaces much more predictable and easier to reason with—thus saving a lot of development time.
Lightweight and fast testing — This is probably the most undermentioned, yet most powerful, benefit of using Elixir. Elixir provides developers access to super-fast, concurrent testing with relatively minimal impact on hardware performance. Ruby testing, on the other hand, can be painfully slow and hardware-intensive—to the point that it could bog down the whole system for those with underperforming machines. Elixir completely takes away the headache by making tests faster.
Useful built-in features — We have used Elixir to build a quick MVP project to building large-scale projects. In both scenarios, Elixir provides powerful built-in features to satisfy the requirements for a typical modern web application such as caching, background processing, and process monitoring. On a platform such as Ruby on Rails, these solutions usually come with tightly-coupled dependencies on services such as Redis, Resque, and Foreman.
I will admit, at first, I was extremely frustrated with Elixir.
I struggled with pattern matching, which was touted as this revolutionary new concept. After getting used to it, and all of the other advantages of Elixir, I have found myself coding with less effort and frustration to achieve the same exact output as Ruby. There are two reasons for this.
Functional languages, as opposed to object-oriented languages like Ruby, are “beautiful” and simplify programming. I greatly favor functional languages for several reasons:
- It leads to coding faster and more predictably.
- No state to keep track of! State makes it extremely difficult to track down problems. There are no reference errors. Everything is a pure function, which is wonderful.
One of the biggest advantages and differentiators of Elixir is pattern matching. Pattern matching comes from its Erlang roots and is one of those things you wish was also incorporated into other languages.
One of our very talented engineers put together a code sample comparison between Elixir and Ruby using a simple ranking example.
In that example, pattern matching makes the code more elegant, expressive, and easier to read. It may seem like a small thing, but I find myself using pattern matching any chance I get.
Programming is a mixture of fun and frustration. I find Elixir to be slightly more fun and a lot less frustrating than Ruby. Elixir, a dynamically-typed language, has type-safe and guards built-in, along with pattern matching, which significantly reduces the bugs.
This results in fewer compiler errors. And for the bugs that do exist, they are easier to find because of the compiler. Compiled code is a huge safety feature. In Ruby, bugs can remain hidden until someone hits them; in Elixir, the compiler will find most of them.
Safety → Fewer Bugs → Easier to Track Down Bugs → More Fun → Happier Team → Happier Me.
Elixir compiles into BEAM bytecode, so it inherits all the performance benefits of Erlang with almost zero penalties. On the other side, Ruby requires an interpreter that reads its code line-by-line during runtime. While choosing the right kind of production-ready Ruby interpreter for your application is already an uphill task by itself, squeezing more performance out of Ruby often requires strict adherence to Ruby’s do’s and don’ts. Want to concatenate strings? Sure. Want to make it run faster? Use string interpolation instead!
Elixir liberates programmers from having to carefully craft their codes based on an endless list of “best practices” for the sake of making their codes run just a tiny bit faster, and focuses more on producing codes that satisfy the needs of your business.
To be fair, Elixir may also have a similarly long list of performance best practices, but they come at a much later stage of development. Moreover, since Elixir is a compiled language, most performance issues can be caught by the compiler automatically, reducing the time and effort to write efficient, performant code.
Remember how I said Elixir was fun? Well, the way concurrency is handled in Elixir is one of the reasons I don’t see myself programming in other languages (unless I absolutely need to).
If you’ve ever had to deal with concurrency, you can understand how frustrating it is. The same program can generate different results if not handled correctly. You have to deal with locking, race conditions, and bugs that are difficult to solve.
The beautiful part of Elixir is that concurrent libraries are built into the language. You won't need third-party dependencies to achieve scalable, safe, concurrent programming.
These two reasons are a HUGE selling point for Elixir.
WhatsApp, built on Elixir, has over one billion active users, and it processes over 65B messages per day! src
Ruby applications take up a lot of memory on a server because you have to run many instances of that app to handle multiple requests. Why? Ruby is effectively single-threaded because it has something called the Global Interpreter Lock that enforces only one thread run at a time.
Elixir applications can handle magnitudes more requests per instance because it's concurrent and compiled (much quicker execution time). You can run a single instance of an Elixir application on a server to handle a huge number of requests. That lowers your memory utilization considerably.
When it comes to data processing, both Elixir and Ruby are considered expensive solutions in terms of memory compared to languages such as C, Rust, and Go. But in the world of concurrent data processing, Elixir and Erlang have a robust pre-built infrastructure.
With the smaller door (Ruby), you’ll have to wait longer for people in front of you to get in. On a wider door (Elixir), more people can get in at the same time.
Elixir handles concurrency in a really efficient way, allowing for more concurrent connections with the same hardware.
Elixir is built on top of Erlang, a battle-tested 40-year-old language. Erlang runs highly concurrent applications like WhatsApp.
This title and quote from Wired is a great Erlang case study:
Additionally, Elixir has Phoenix, a world-class web framework for modern web apps modeled on Rails, which we all know and love.
Elixir has a large ecosystem of packages you can leverage while building your application. Not everything is available in an Elixir package, and for those cases, you can run any Erlang package natively from your Elixir application.
It’s also growing. It hasn’t caught up to Ruby, in terms of job posts on Hacker News, but as you can tell from this graph below, it is trending upwards while Ruby is trending downwards.
From my experience, since Ruby has been around for much longer and is easier to learn, there are a lot of great Ruby programmers and many who have a lot to learn. This makes it a little bit harder to hire Ruby engineers since you’ll have more candidates to filter through.