5 tips for starting an Elixir journey
I have been fortunate enough to start my Elixir journey in a commercial setting, some 8 months ago. I had neither heard of or dabbled with the language until then. It has become my third significant language change. I started out with C#, then I joined the JavaScript craze (which I’m still a member of), and now I’m a relative early adopter of Elixir.
I believe in the concepts of “learning in public” and “paying it forward”, so here are my 5 tips for others starting an Elixir journey.
Start with a purely functional problem
This was by luck really. The first thing I did with Elixir happened to be a specialised HTTP client for InfluxDB, making use of HTTPotion. Just a module with a bunch of functions for abstracting away InfluxQL and the Line Protocol used to write points. This allowed me to concentrate on Elixir the language, i.e. no processes, no OTP.
For example, take a list of the following struct:
and reduce it to the line protocol required to write to an InfluxDB measurement called points
.
Favour pipelines and function heads over conditionals
My code doesn’t pass review and get merged to Master when it includes conditional statements, such as if
, case
and cond
. This can be frustrating at times, but the resulting refactor to pipelines of functions is always easier to understand.
Improve the readability of a module’s public functions by using the |>
operator to chain private functions together, thus creating pipelines. Group public functions at the top of a module and private at the bottom. Use Railway macros to elegantly handle error scenarios without making a pipeline harder to read.
Your future self, working on a growing codebase, will thank you. They’ll be able to scan a module written several months ago and quickly comprehend what it’s public functions are doing. If they want to understand the nitty-gritty details, they can scroll down and study the private functions. This is priceless.
I’ll attempt to illustrate with a contrived example, as follows:
Understand processes before reaching for abstractions
Learn from my mistakes. Understand how to use processes with Kernel.spawn
and receive do ... end
blocks first. Then move on to the GenServer
, Agent
and Task
abstractions.
My first attempt at concurrent design started by studying the excellent “Designing a Concurrent Application” chapter of Learn You Some Erlang. I couldn’t grasp how to construct a single GenServer
that used a pool of short-lived helper processes to achieve concurrency. Perhaps attempting to port Erlang to Elixir was a step too far at that time. More likely I simply didn’t understand the basics of processes.
So before reaching for a GenServer
, Agent
or Task
abstraction I recommended trying to solve the problem without them first.
The Supermarket
example I introduced previously forces each caller to maintain state. Converting Supermarket
to an Agent
would allow it to maintain it’s own state. But as a learning exercise can we achieve the same improvement using Kernel.spawn
and receive do ... end
blocks?
The answer is yes, as follows:
Aid mental separation by enforcing one process per module
It may be my OO background, but for a long time I associated a single module to a single “thing”. In reality a module is just a method for organising functions within a codebase. A “thing” is of course a process. I struggled to visualise what was going on when there were multiple processes to a module.
Our learning exercise with Kernel.spawn
and receive do ...end
blocks has served it’s purpose. So I’ve migrated Supermarket
to use the far less verbose Agent
abstraction. However there still have two processes involved.
To counter any confusion, place spawned functions in separate modules. I’ll introduce a SupermarketServer
module to demonstrate, as follows:
Clarify intent by always pattern match function return values
You may have noticed I’ve adopted a strange practise throughout this post. I’ve pattern matched every value a function returns, even it’s not used elsewhere. For example _msg = Kernel.send(Mod, {:put, cus_id, name, price, Kernel.self})
. In functional programming not matching a return value suggests a side-effect only function. I prefer to be explicit with my code, any practise that adds greater clarity to the intent of my code is a good thing. If you’re a Dialyzer user you may want to consider turning on the Wunmatched_returns
warning.
That’s all for now folks
I’ve thoroughly enjoyed my journey with Elixir so far. During short periods of JavaScript development I’ve found the lack of immutable functions a jarring experience. This is something I hadn’t truly appreciated prior to learning Elixir.
I hope you’ve found this post useful. If so, consider following me on Twitter.
See you later!