Is Elixir programming really that hard?
How to survive in a post-apocalyptic Elixir world devoid of classes, objects and methods that we've grown to depend on so much?
Recently, I've had a chance to talk with some fellow Ruby on Rails developers about a growing prospect of using Elixir instead of Ruby for some upcoming projects or micro-services. They all seem to be pretty much sold on things like all-so-famous performance gains, syntactic appeal or the great momentum of the language. There's one problem though: making them actually believe they can code in something as foreign, abstract and strange as a functional language.
Actually, I can relate to that. I've been there too - I didn't use any functional language before getting serious with Elixir a few months back and I've considered all those Haskells and Lisps to be more of academic toys than serious everyday coding tools. Ditching the OO syntax and learning to live without mutability or loops may indeed raise fears about the switch, like:
- Will I be able to solve simple tasks that I'd do without thinking in OO?
- Will I be able to (or unnecessarily forced to) come up with simple algorithms?
- Will I be able to organize the code, especially in large or multi-service projects?
- Won't all of the above result in a massive drop in my productivity?
Here's my take on how it really is on the other side of the mirror.
Myths about functional programming
1. Functional programming is hard
You may have heard tales of those crazy Haskell, Erlang or Lisp programmers. They code with madness in their eyes. They look at object oriented devs with disbelief and compassion. And their code looks like a mathematical formula glued together by a college professor to harass his students. Just take a look at coding styles of popular functional languages and use your imagination to extrapolate those samples to something bigger. There's no way you would willingly switch your cozy Ruby, Swift or Java to something like that, would you?
And that's how Elixir may be perceived too. You'll easily convince your peers that functional programming is cool (pattern matching), powerful (EVM efficiency and scalability), fault-tolerant (OTP) or highly concurrent (cheap and safe processes). But they'll instantly think you're tricking them when you describe Elixir as productive and pleasant. Phoenix authors surely use the same shameless trick when they describe it as a "productive web framework".
Fortunately, you'll find an Elixir excerpt among those examples on Wikipedia as well. Here it is:
lang:elixir defmodule Fibonacci do def fib(0), do: 0 def fib(1), do: 1 def fib(n), do: fib(n-1) + fib(n-2) end
It's nothing short of beautiful (especially in comparison to others). It just can't get simpler and more expressive. You may think it's math so it has a natural advantage over OO whereas a serious problem XYZ can't be expressed so efficiently without classes. We'll get to that later. For now let's just stick to the fact that Elixir code may look compact, expressive and beautiful.
Side story about C and C++
There's a funny aspect to non-OO programming being generally perceived as harder by those who just didn't dare to leave the safe OO turf. It's about C and C++ - 2nd and 3rd most popular programming language in the world, respectively (according to TIOBE index). You may have heard opinions about coding in C being a nightmare compared to C++. According to those, C is particularly hard to reason about, hard to compile and just so damn primitive. This sounds suspicious considering that C is basically a syntactic subset of C++. How can something that is more complicated on paper be so widely recognized as easier to use?
This comparison may be a bit far fetched as C does indeed lack some of C++'s conveniences like smart pointers added in C++11, but lots of those opinions about C being harder may not really come from missing the OO meat of C++, but from more prosaic reasons like its stricter compiler and scarier API for working with memory.
So, how can Elixir that narrows down Ruby's basic building blocks (
class, inline) to just one (
defmodule) can be perceived as harder? This brings us to the next chapter.
2. Classes are the only way
Looking at Ruby on Rails projects, I often wonder how many of all those classes should really be classes. How many of them make a convincing use of encapsulation? How many have a sense-making behavior? Is the use of polymorphism conscious or is it "just because"? It's an unhealed scar that I took from my brief adventure with game development, where I witnessed all those C++ game engines in many of which 90% of classes end up as tightly coupled singletons.
Don't get me wrong. Just like there are justified singletons in game engines, there are convincing class implementations in every Ruby on Rails project. But in general, I'd say that we've grown too fond of our code hanging around in classes without a SOLID ground for it these days. And too lazy to even consider alternatives. Consider this example:
lang:ruby class UsersController < ApplicationController def index @users = User.all end end
How is an instance method fit for holding an action code? Of course, in this case it's a choice made by the Rails team and not by us. For sure, they had a reason for it. Maybe it's the best way to guarantee the interoperability of before filters, actions and views in the context of single request. Maybe it's about inheritance. But the part that we're left out with makes me wonder.
I'm not blaming Rails for being Rails and doing its controller magic as these are exactly the things that allow us to get the work done so efficiently with the framework. I'm just questioning the way of thinking where classes are the best and the only way for everything. Here, the issues include:
- By hiding the constructor and instantiating the controller for us, Rails make the class context a bit mystical and the term "behavior" harder to define. We're loosing the one greatest key to understanding our controller as an object with behavior.
- If anything, the state that's supposed to be encapsulated (instance variables) gets overexposed by getting invisibly shared with the view layer. I've discussed practical consequences of that in Phoenix vs Rails: Views and helpers article.
Also, Phoenix did manage to cover most of what Rails controllers can do - including thread-safe passage of each request,
before_filters, preloading things in a DRY way and taking state to the view layer - all without unnecessarily bringing classes into it without a clear gain. Here's the Phoenix version of the above:
lang:elixir defmodule MyApp.UserController do use MyApp.Web, :controller def index(conn, _params) do users = Repo.all(User) render conn, users: users end end
It has clearer context (
conn aka. HTTP connection) and behavior (explicit
render into that
conn). Is it possible that this humble module makes for a better class that an actual class from a Rails app? Not if we assume that
class == OOP. But the key question is: should we?
3. Can't be object oriented anymore
Well, here's a marvelous article, Object Orientation in Ruby and Elixir, that explains thoroughly what object orientation really is, digging back to the author of the term. Just to summarize, the sole fact that the language is not called "object oriented" doesn't mean that you can't achieve all the traits of object oriented design in it. With Elixir processes, you can:
- Encapsulate the state (with Agent module being the simplest way to do it).
- Execute behavior asynchronously (with message passing to GenServer process).
- Execute behavior synchronously (with Task module being the simplest way to do it).
- Achieve inheritance (by spawning parent process along with child and delegating messages).
- Achieve polymorphism (by just sending the same message to different processes).
Of course, some of these things require more effort compared to OO language like Ruby. On the other hand you get all those concurrency, safety and fault-tolerance perks that are arguably harder to achieve in a language without immutability, OTP and EVM.
There's one thing I'm missing in this otherwise excellent article - a mention of Elixir object oriented features that don't depend on processes. They're useful when there's no need for separate state or supervision tree. These include:
defstructand pattern matching for structure definition and differentiation.
defimplfor structure polymorphism.
importfor function inheritance between modules.
defpfor function encapsulation in a single module.
Note: It may seem outrageous that I dare to use the term "inheritance" for describing what Elixir's
import does, but keep in mind that the line between Ruby class inheritance and module inclusion is also very thin.
It's not a 1:1 replacement of your battle-tested object oriented idioms but these constructs will ease your post-OO trauma. And they come with their own unique advantages like the power and flexibility of pattern matching when combined with recursion or the explicitness of imports.
As you can see, each paradigm has its benefits and drawbacks. It all depends on what you'll benefit from the most. But for sure you don't get to say goodbye to object orientation in Elixir. And when the darkness comes, you can always add a
class macro like this guy did.
4. There's no state in Elixir
I've already covered it in the previous section, but I'm hearing this one so often that it deserves a separate mention. This is perhaps the most unfortunate misunderstanding of Elixir as an immutable, functional language. Let's make it explicit example by example. Here's how you live with your immutable state (variables) in the most regular scenario of processing some data:
lang:elixir defmodule MyApp.UpdateUserService do def process_user(user) do # process data by re-binding it: username = user.username username = String.strip(username) username = String.downcase(username) # or, the cooler way, with the pipe: username = user.username |> String.strip |> String.downcase # now, let's return the new user: Map.put(user, :username, username) end end
Depending on a case, you'll combine this with recursion,
Enum.reduce and list comprehensions in order to cover your basic state mutation needs. And should the day come when you need your state to be shared and/or persistent, you just move it to a separate process:
lang:elixir Agent.start_link(fn -> 1 end, name: :my_app_version) Agent.update(:my_app_version, fn version -> version + 1 end) Agent.get(:my_app_version, fn version -> version end) # => 2
That's it (for starters). If your state will ever grow to require fault-tolerance or special tending during hot code reload, don't you worry - OTP and supervision trees will have you covered.
So, why is this misunderstanding about immutability and "the lack of state" so unfortunate in my opinion? Because it leads into thinking that Elixir code is not just harder to write but also harder to reason about, while it's often exactly the reverse. It just can't be easier to reason about this:
...than about this:
lang:elixir user |> User.put_some_changes |> Repo.update |> UserNotification.send
Immutability doesn't mean "no state" - it's more like "explicit state changes". This is why React became so successful these days, right? And again, it doesn't mean having objects and calling methods on them is worse. It's just good to understand each paradigm's merits properly.
Here are some points that I hope to have proven to some degree in this article:
- Many myths and fears have emerged around functional programming. Many of them simply don't apply to Elixir which really does deliver on the promise of productivity.
- It gets a lot of knowledge, responsibility and smart decision making to really make a good use of OO. Otherwise it leads to its overuse, misinterpretation or worse.
- Elixir, having less on its plate when it comes to available building blocks, can't really be more complicated than Ruby at its core. That's only logical, isn't it?
- It is totally possible to go OO in Elixir. It requires some extra work at times but it gives back when thread safety and fault tolerance start to matter.
- Ruby syntax is still very nice for rapid, expressive development of synchronous applications that are object oriented down to their bones. Just like it always was.