As a Rubyist, a lot of languages can feel very alien. I've yet to find a language that values readability quite as much as Ruby does. Ruby allows you to create beautiful code easily. While Elixir definitely allows you to create an unreadable mess much easier than Ruby does, it's still capable of creating really nice, flowing code. A lot of the syntax feels very familiar too, which is great when you're just getting started. One thing that is tricky is switching away from thinking in OOP to thinking in functions. Once you do you can start building some really neat stuff.
Pipes
Ruby added a pipeline operator in 2.7 but don't be fooled, they are not the same as the more common interpretation in various other languages. Ruby's pipes are arguably not really pipes. Many have complained about their effectiveness. In Elixir and many other languages, pipes will pass the result of the last function as the first argument of the next function. This allows you to create a series of steps for process-like tasks. Pipes are essentially a mechanism for composing functions.
One thing to point out here is that strings in Elixir must be surrounded with double quotes. Single quoted strings are interpreted as charlist. Something I'm sure a lot of Rubyists get caught out by. I still do occasionally.
The code above is a simple example but you can see how easy it is to understand what is going on.
Pattern matching
One of my favorite things in Elixir is pattern matching. Ruby has added experimental pattern matching in 2.7 which I haven't had an opportunity to play with yet. I have played with Elixir's pattern matching extensively. I love it.
# check if variable contains what we expect
[1, 2, 3] = podium_positions
# check tuple response from a function
{:ok, response} = function()
{:error, response} = function()
# Handle cases based on arguments
case function()
{:ok, :done} -> do_something()
{:ok, result} -> process()
{:error, :unauthorized} -> raise Something
{:error, reason} -> check_reason()
Function definitions
One thing I really love about modern Ruby is the ability to inline protected and private flags for methods. In Elixir this is by design. You can either define a function as public or private and you do that by using def or defp. That's it. It's simple and readable. I love this style of marking function accessibility because it's much easier to read. Especially when you have large files.
In Elixir you also have the ability to do something that I would love to be able to do in Ruby. In Ruby a class or module can only have one method by a given name, however, in Elixir the functions are also tagged with their arity. You'll often see mentions like String.upcase/1. This is referring to the String function named upcase that takes a single argument. This allows you to create functions that have the same name with additional arguments. In Ruby you can do something similar by having a default value of nil and checking if that parameter is present or not. However, Elixir's syntax makes function definitions a lot cleaner.
def call_function(function_name, arguments) do
apply(__MODULE__, function_name, arguments)
end
def call_function(module, function_name, arguments) do
apply(module, function_name, arguments)
end
We can also do other things with functions. We can use guards to create multiple versions of the same function based on the arguments passed
def empty_map?(map) when map_size(map) == 0, do: true
def empty_map?(map) when is_map(map), do: false
def empty_map?(map), do: raise NotAMapError
Elixir comes with a lot of built in guards but you can also create your own. However, because Elixir is a compiled language there are some rules so remember to look into that when doing so.
Elixir processes
Elixir's process structure is pretty incredible. Well to be precise, BEAM is pretty incredible. Due to the way the that BEAM was designed, it's possible to run a lot of processes seemingly in parallel. Even if one of your processes starts hogging 100% of the CPU, your other processes will still be running at near to 100% efficiency. This is because BEAM will automatically switch processes continuously, ensuring each process gets a fair share of the available resources.
This alone blows my mind. In Ruby, if you have a CPU hogging process you can lose a server. You of course still need to be aware that one of your processes is running at 100% and rectify it but in the short term your whole system stays healthy.
Each process is also completely independent so you don't have to worry about thread safety like you do in Ruby. You do still need to think about database locking and how files might be manipulated from multiple processes for example, but those are handled in different ways.
Simplicity
Elixir doesn't hide anything and it doesn't do anything particularly magic. In Rails you can often spend years not understanding some of the mechanisms underneath. Elixir is really easy to understand. The abstractions are clear and because we're dealing with immutable structures passed through functions there's no strange unexpected side-effects tripping us up.
Documentation
The Elixir docs are very comprehensive and easy to understand once you understand the language. There's also a great guide on the language itself.
Documentation is written inline with your function definitions similar to RDoc or Yard, however, they are baked directly into the language itself and encouraged in the community. Releasing a package on Hex will automatically compile the docs and make them available on hexdocs.pm
You can also write examples in the docs that will compile into real tests. These are referred to as DocTests
This documentation system makes it really easy to document modules well and makes reading the docs accessible while editing or implementing elsewhere.
Ports
How could I forget Ports! Elixir handles communicating with the external system nicely. With Ports you can send commands to the underlying system through messages, the same way we do with Processes.
With Port you can spawn an executable in a process and then send messages directly to the binary. This allows you to interact with Python, Ruby, Node.js etc as if you were talking to an Elixir Process.
Is there anything I don't like?
Yes of course, there will always be weaknesses in any language. Most of the ones I've experienced so far have been more about convenience than actual weaknesses so take this with a pinch of salt.
Finding Function Definitions in Elixir vs Finding Method Definitions in Ruby
In Ruby, pry provides us the show-source helper to show where the method is defined and the actual definition. We can also call object.methods to list all of the methods defined within the object.
However because Elixir is a compiled language, the functions we've defined don't exist at runtime - they are converted into BEAM bytecode. This makes it impossible to find definitions from within a pry session.
To find functions in Elixir comes down to you being able to search in the right places. If you've imported modules into the current module then you'll need to search through those modules to figure out where the function is defined. Thankfully documentation in Elixir is highly respected so this doesn't cause many problems 99% of the time.
Debugging with Pry During a Request in Phoenix
By default, when you start the server in an interactive Elixir session (i.e. iex -S phx.server) you only have a short amount of time to do the debugging before the request will terminate and continue. You can increase this timeout in your configs but of course if you need the timeout to be short then that also becomes an issue. In Rails we don't have this problem - pry halts the execution indefinitely.
Footnote
This list is definitely not exhaustive - I could go on for pages. These are just a few of the features that have jumped out at me during my transition from Ruby OOP to Elixir POP and Functional Programming in general.
When I started writing Elixir, I didn't think I would enjoy it as much as I do. It's really nice to work with. Maybe it's because I'm still finding lots of interesting concepts to learn which is stimulating. Either way, it's definitely a great language.
Phoenix is a great framework and I have high hopes for live-view.