Issue 04

10 tips for new Elixir developers

Welcome to the world of magic

Jul 2, 2022 ยท 9 minute read

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 tollerance, 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 it's hidden power comes from it's 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:

# prints timing information for the N slowest tests
mix test --slowest N  

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 sound.

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 becase 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 litered 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 assited 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.

Twitter

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:

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 below for similar content and follow me on Twitter for more Elixir (and general programming) tips.

fin

Sign up to my substack to be emailed about new posts