I recently had a chance to play with Elixir and Phoenix as part of my Cookies Lab. The following is a whistle-stop tour of my first impressions.
Functional, not Object-Oriented
At first glance, Elixir looks like Ruby, but is quite different underneath with the functional language paradigm. Having said that, the semi-familiar syntax does make understanding some of the new concepts easier.
In Elixir’s case, functional (generally) means more explicit – for example, passing around the conn
struct in controllers, rather using instance variables (there are no instances!). While this can mean a bit more boilerplate, the extra clarity can be worth it – of course, on the flip side, you can write Fortran in any language ;-).
One of the ‘disadvantages’ of functional languages is having deeply-nested functions (e.g. foo(bar(baz(1)))
), which need to be read ‘inside-out’ to be understood. Fortunately, Elixir has the pipe operator, which allows us to construct functions as a pipeline (much like the Unix command line) – for example:
String.split(String.reverse(String.capitalize("elixir phoenix")), " ")
can be rewritten as (the much clearer form):
String.capitalize("elixir phoenix") |> String.reverse |> String.split(" ")
# or over multiple lines:
String.capitalize("elixir phoenix")
|> String.reverse
|> String.split(" ")
which compares favourably with the Ruby equivalent:
"elixir phoenix".capitalize.reverse.split(" ")
As part of its’ Erlang heritage, all data in Elixir is immutable (this is part of the Erlang VM’s ‘shared nothing’ method of data sharing for concurrency), which means you can’t alter things on the fly as you might do in a Rails controller (e.g. adding the current user ID to the session) – any changes you make have to be returned by your function.
Pattern matching
One thing that takes some getting used to is Elixir’s pattern matching. On first glance, the syntax for assigning variables looks very familiar – a = 1
– but Elixir isn’t assigning variables in the traditional sense (‘put this data in some address in memory and let me refer to it as “a”‘), since all values are immutable.
You’ve probably come across some pattern matching in Ruby (or CoffeeScript) – this is generally positional and for arrays. For example:
a, b, c = [1, 2, 3]
# gives:
# a = 1
# b = 2
# c = 3
[["a", 1], ["b", 2]].each do |(letter, number)|
puts letter
puts number
end
# gives (on STDOUT):
# a
# 1
# b
# 2
Elixir takes this a step further and allows pattern matching on other data structures (e.g. maps, which are like Ruby hashes), as well as function arguments. This means functions are often ‘defined’ more than once with different arguments, with the runtime deciding which to use based on the arguments passed in. Functions can also be defined multiple times with different arities – this is why Elixir functions are named with a number and slash at the end, indicating arity (e.g. foo/1
, bar/2
etc.).
def belongs_to_user?(%{user_id: user_id}, user_id) do
true
end
# Default case, handling nil or other values
def belongs_to_user?(_, _) do
false
end
I found pattern matching the hardest to grasp and I feel like getting to ‘idiomatic’ usage would take a while. It’s a cool idea, though, and certainly removes a lot of if
s and nesting.
Ecto
Elixir’s ‘equivalent’ of ActiveRecord is Ecto, although it’s quite different. Models are simply structs (typed maps), with validations and changes being handled by changesets. Changesets represent a different way of thinking about your models, but really stand out when dealing with changes to models under different circumstances (create vs. update, admin vs. normal user etc.) – they avoid the problems with conditional validations or callbacks in ActiveRecord, and this is where the idea of a pipeline of functions is certainly appealing. In Rails, form objects and interactors cover similar ideas.
Ecosystem
As is to be expected, Elixir has a relatively immature ecosystem compared to Rails – the language itself is in flux (although it seems pretty stable) and there are not so many battle-tested libraries to just drop into your project. Authentication is an area I found to be pretty sparse compared to Rails – there are a few libraries, but nothing that matches the completeness of Devise. Efforts do seem to be underway, though, so this should improve in the future.
The standard library itself is pretty good, and you have the advantage of interoperating with the Erlang standard library and packages as well. There are similar data types to Ruby (arrays, hashes, strings, symbols, along with a few others), although the terminology is a little different (hashes are “maps”, symbols are “atoms”).
Documentation is also pretty good, with the Hexdocs for the major packages (Phoenix, Ecto etc.) well-documented and understandable for novices.
Developer experience
This feels like another area that’s still developing – error messages can often be unclear (often caused by failing to satisfy a pattern match somewhere and finding where can be difficult). I also found that Elixir doesn’t quite have the Ruby-like property of ‘just try something how you’d expect it to work and it probably will’ (the principle of least surprise) – or at least not for ‘least-surprise’ that’s shaped by Ruby experience.
In contrast, the front-end side feels more advanced than Rails – Elixir offloads handling of the front-end to Broccoli, which gives you code reloading out of the box, along with access to the wider NPM ecosystem (for better or worse).
Conclusion
All in all, I enjoyed my brief foray into Elixir and Phoenix. The language – and framework – seem well-established, although the wider ecosystem has a while to go to match that of Ruby and Rails.
Inevitably, my basis for comparison has been Ruby/Rails (since that’s my day-to-day language/framework), but it’s worth remembering that the two are different languages/frameworks with differing goals, and so a one-to-one comparison will always be a bit reductionist.
If you’re a Rubyist, give Elixir a try!