Elixirs popularity has exploded in the last year - and for good reason!
Stack Overflows 2022 Developer Survey lists Phoenix as the most most loved Web framework and Elixir is now the sixth highest paid language π - it might be time to have a look if you haven't already.
What is Elixir?
It's a dynamic, functional programming language with an emphasis on fault tolerance, scalability, and developer experience. It's gorgeous.
I'm fortunate enough to write Elixir daily (when I'm not in meetings) as part of my job as the core tech stack and have done so for over 3 years. As such I've encountered some of the gotchas you're likely to experience if you're new to the language and amassed a bank of helpful tips to ensure you're really getting the most out of the language.
1. Get used to pattern matching
In Elixir, the =
operator is actually the match operator. You can use it to assign variables just as you might expect but its hidden power comes from its ability to destructure complex data types.
A quick example from the official language guide:
iex> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> a
:hello
iex> b
"world"
Now this is all well and good but some of the real fun comes from pattern matching on function heads to:
- Replace pesky
if
statements
defp greet(%{first_name: first_name} = user), do: "Greetings, #{first_name}!"
defp greet(_), do: "Greetings, stranger!"
- Handle different outcomes in your code declaritively
defp split_traffic(%{age: users_age}) when is_even(users_age), do: # redirect to path A
defp split_traffic(%{age: users_age}) when is_odd(users_age), do: # redirect to path B
2. Append to a list the correct way
Lists in Elixir are effectively linked lists - they are internally represented in pairs containing the head and the tail of a list.
Meaning both of these approaches are valid:
list = [1 | [2 | [3 | []]]]
# [1, 2, 3]
list = [1,2,3]
# [1, 2, 3]
Because of their linked list nature, prepending to a list is always faster (as it's constant time vs having to internally traverse each list til the end and append - linear time).
You can add to them by concatenating lists together using Kernal.++/2
or you can use |
to do the same thing faster.
Have a look at the example below (which is using the erlang timer module to time the function call).
iex(19)> :timer.tc(fn -> [1,2,3 | 4] end)
{6, [1, 2, 3 | 4]}
iex(20)> :timer.tc(fn -> [1,2,3 | 4] end)
{6, [1, 2, 3 | 4]}
iex(21)> :timer.tc(fn -> [1,2,3 | 4] end)
{6, [1, 2, 3 | 4]}
iex(22)> :timer.tc(fn -> [1,2,3] ++ [4] end)
{13, [1, 2, 3, 4]}
iex(23)> :timer.tc(fn -> [1,2,3] ++ [4] end)
{14, [1, 2, 3, 4]}
iex(24)> :timer.tc(fn -> [1,2,3] ++ [4] end)
{12, [1, 2, 3, 4]}
3. Finding N slowest tests in mix application
You'll be writing tests in your Elixir code (right? ππΌ), and once your application grows to a large size you may find your tests take a long time to execute (not too long, Elixir's pretty speedy).
You can narrow the culprits down with the following:
mix test --slowest N # prints timing information for the N slowest tests
4. Read the docs (especially Enum)
This might be an obvious one to many of you but you'd be surprised how often people don't RTFM. I actually discovered that last tip by reading the official docs π€― - Elixir's documentation and standard libraries are stellar.
I can guarentee that you'll be using the Enum
, Map
, List
, and Kernal
modules often - get used to reading their documentation.
You'll find examples like:
-
Enum.chunk_while/2
for chunking lists when certain conditions are met -
Enum.into/2
for converting enumerables into collectables (lists into maps) -
Map.put_new_lazy/3
for putting the result of a function into a key if it's not already there
Any time you think there's a more functional way to do something - there probably is, get reading, chances are the standard librarys got you.
This leads me on to my next point quite nicely...
5. Embrace Doctests
It's impossible to talk about how good Elixirs documentation is without talking about the magic of doctests. I've spoken about these before in my Elixirs Hidden Potions post (which you should also read) but I have to re-iterate just how powerful this can be.
Doctests let you annotate your functions with example usage (including expected output) and have them run as part of your test suite.
Yes, they're as amazing as they sounds.
Taken from the official getting started guide:
defmodule KVServer.Command do
@doc ~S"""
Parses the given `line` into a command.
## Examples
iex> KVServer.Command.parse("CREATE shopping\r\n")
{:ok, {:create, "shopping"}}
"""
def parse(_line) do
:not_implemented
end
end
# Running the tests...
1) test doc at KVServer.Command.parse/1 (1) (KVServer.CommandTest)
test/kv_server/command_test.exs:3
Doctest failed
code: KVServer.Command.parse "CREATE shopping\r\n" === {:ok, {:create, "shopping"}}
lhs: :not_implemented
stacktrace:
lib/kv_server/command.ex:7: KVServer.Command (module)
6. Atoms are not garbage collected
Be careful about converting user supplied parameters and converting them into atoms. Atoms are meant to be used as named constants - do not take parameters from your users and convert them into atoms.
Atoms are not garbage collected.
The reason for this is because Atoms are represented as integers internally - when Beam (the Erlang VM) sees an atom for the first time, it inserts it into the atom table.
Don't worry - when your code is littered with :ok
atoms from tuples they're all referencing the same atom (the one referenced from your atom table).
If you really, for some reason, need to convert user generated data into atoms - reach for String.to_existing_atom/1
.
Fun fact: the default maximum number of atoms is 1048576
.
7. Embrace the small, but mighty community
Elixir isn't the biggest language (yet π) but it has one of the most helpful communities I've ever stumbled across.
Slack
I'm consistently being assisted by the creators of platforms, libraries, the language itself all on the Elixir Slack channel. Get involved and do your part answering questions you can help with as they pop up.
Some people hate Twitter - I maintain that it's a treasure trove of great information if you're willing to curate your feed and block out the noise.
Some of my favourite Elixir twitter accounts to follow include:
- Jose Valim - the creator of Elixir
- Chris McCord - the creator of Phoenix & LiveView
- Uku TΓ€ht - Founder of Plausible Analytics
- Chris Gregori π - Founder of niceice.io & 6words.xyz
- Johanna Larson
- Evadne W
If I've left you out send me a message on Twitter and I'll add you to the list!
8. Add labels to your IO.inspect/2 calls
Really you should be using Pry for debugging your applications (see more in my other article here), but sometimes you just need a quick and dirty way to print out and that's where IO.inspect
comes in.
The thing is you may get lost in the sea of terminal print out when you're trying to debug lots of data that looks very similar.
Consider the following pseudo-code:
customer_orders
|> Enum.map(&validate_purchases/1)
|> IO.inspect()
|> Enum.map(&exclude_late_orders/1)
|> IO.inspect()
|> Enum.map(&email_customers/1)
If your customer_orders
represented a complicated schema with lots of nested data (and many of them) you'd struggle to decipher which are which in your printed logs after transformation - a simple option being passed to our IO.inspect/2 calls makes this inspection a hell of a lot more legible:
customer_orders
|> Enum.map(&validate_purchases/1)
|> IO.inspect(label: "Validated purchases")
|> Enum.map(&exclude_late_orders/1)
|> IO.inspect(label: "On-time orders")
|> Enum.map(&email_customers/1)
9. Try and keep your functions small and composable
This is a tip that applies to most software disciplines also.
When I first stumbled across functional programming, one thing that took some getting used to was creating lots of little functions. I used to write a lot of React - and thus a fair amount of boilerplate - which meant that I was used to large render functions.
In Elixir, and all functional programming, try and create lots of small function calls to describe your code. There are plenty of benefits:
- It's more declarative so it's easier to read and reason about
- It's easier to refactor later on down the line
- It's easier to test
- It helps enforces the DRY principle
- If you structure your code nicely you'll find that you'll re-use plenty of functions often which inadvertently leads to better code structure. Better code structure also leads less circular dependency referencing which leads to faster compile times (but this is a topic for another blog post - let me know if you'd like to hear more!)
10. Learn about Macros but be cautious
Macros are powerful - with great power comes great ~responsibility~ chance of doing something invisible and horrendous to your codebase. Even the official language docs tell you to use them responsibly.
You can use macros to extend module behaviour by adding functionality through wrappers - think along the lines of always logging when you call a function without having to declare it - or dynamically generating routes in your Phoenix application to warn a developer when they've not accounted for something they should've like localisation.
It's good to be aware of Macros and what they can do for you - but seriously, triple check yourself every time you think about adding one. If you want a custom guard clause for your function heads pattern matching, look at defguard
and defguardp
instead.
There we have it.
10 tips to help you embark upon your Elixir journey. It's an incredible language, give it a chance and you'll slowly realise why I've been hooked for the past 3 years.
Subscribe to my Substack for similar content and follow me on Twitter for more Elixir (and general programming) tips.
Top comments (0)