Elixir Tips and Tricks

27 Oct 2017

1. Struct Update

The special syntax for updating a map, %{var | field: value} is particularly useful with structs.

With a plain map, the following gives a runtime erorr:

saiyan = %{name: "goku"}
saiyan = %{goku | pwer: 9001}

because %{var | ...} can only be used to update an existing field. However, with a struct, you'll get a compile-time error:

defmodule Saiyan do
  defstruct [:name, :power]

goku = %Saiyan{name: "goku"}
goku = %Saiyan{goku | pwer: 9001}

** (CompileError) code.ex:17: unknown key :pwer for struct Saiyan

2 alias, import and use

Starting off, it can be a overwhelming to realize that Elixir has at least three ways of interacting with other modules. While there's a bit of overlap, they mostly serve distinct purposes.

2.a alias

alias is the simplest to understand and the one you'll use the most. It lets you access a module via a shortened name rather than its full name:

MyApp.Service.Auth.user(email, password)

# vs
alias Myapp.Service.Auth
Auth.user(email, password)

2.b import

import takes this a step further: once imported, you can access functions directly as though they're defined in the current module:

import Myapp.Service.Auth
user(email, password)

Personally, I think you should avoid importing. It makes things obscure: where is user defined? Also, because you lose the context provided by the [hopefully] meaningful module name, for the sake of readability, you'll often have you re-add that context to the function name, such as auth_user.

Why bother? First, you have to import a module if you want to use its macros. As a beginner-safe-rule, you should only import when you need to use a module's macros. You can enforce this:

import MyApp.Service.Auth, only: :macros

You can also import only functions or specific functions or macros.

I confess that we do occasionally import functions. Most notably in our tests, where a number of helpers are repeatedly needed. For example, all of our integration tests have access to an imported truncate function which is used to wipe a database clean.

2.c use

use isn't like alias or import but it's so powerful that it can be, and often is, used to inject an alias and/or import. When you use a module, that module's __using__/1 macro is executed. That macro can do anything, which means the behavious of use changes from module to module.

defmodule MyApp.Controller.Base do
  # automatically called when you: use MyApp.Controller.Base
  defmacro __using__(_opts) do
    quote do
      # Gets injected into the calling module
      import MyApp.Controller.Base

      # ... probably does more things

Above is a somewhat common pattern where a use'd module imports itself. If you're just starting to learn elixir, use when a library / documentation tells you to, and take the opportunity to look at that module's __using__/1 macro to learn what it's doing.

3. alias __MODULE__, as...

You can minimize the presence of __MODULE__ by aliasing it:

defmodule DBZ.Saiyan do
  # Expands to alias DBZ.Saiyan, which means we can now use
  # Saiyan instead of __MODULE__
  alias __MODULE__

  def new(name) do
    %Saiyan{name: name}

We often do this in our GenServers and give it the name of State (unless something more specific makes sense). This works well when combined with the special update syntax described in tip #1:

defmodule Dune.Harvesters do
  use GenServer
  alias __MODULE__, as: State

  defstruct [:lookup, :dispatched]

  # ...

  def handle_cast({:destroyed, id}, state) do
    state = %State{state | lookup: Map.delete(state.lookup, id)}
    {:noreply, state}

4. With's default else behaviour

Elixir's with is useful for dealing with more complex flows, but did you know that you can omit the else clause? The default behaviour is to return whatever broke the flow (and would have triggered the else).

In other words, any time you write:

with {:ok, decoded} <- Poison.decode(data),
     {:ok, scores} <- extract_scores(decoded)
  # do something with scores
  err -> err

You can omit the else block:

with {:ok, decoded} <- Poison.decode(data),
     {:ok, scores} <- extract_scores(decoded)
  # do something with scores

5. Atom Memory

Atom's aren't garbage collected. You should be weary of using String.to_atom/1. Doing this on user input, for example, is a good way to run out of memory.

One option is to use String.to_existing_atom/1 which raises if the atom isn't already defined.

Another option is to leverage matching:

def planets("caladan"), do: :caladan
def planets("ix"), do: :ix
def planets("arrakis"), do: :arrakis
def planets(_), do: nil

Or less tedious and less readable:

for {value, input} <- [caladan: "caladan", ix: "ix", ...] do
  def planets(unquote(input)), do: unquote(value)
def planets(_), do: nil

6. IO.inspect Label

IO.inspect takes an optional label value which is prepends to the output:

IO.inspect("#{min} - #{max}", label: "debug")
> debug: "10 - 100"

7. IO.inspect return

Speaking of IO.inspect, it returns the parameter that you pass into it. This makes it injectable without having to change your code (say, by having to introduce a temporary variable or breaking a pipe chain):

case IO.inspect(parse_input(input)) do

# or
result = String.split(input, ",", parts: 3)
|> Enum.map(&String.to_integer/1)
|> IO.inspect()
|> ...

8. Running a Specific Test

Add the path to the mix test command to run that specific file and optionally include :LINE to run a specific test:

mix test test/users_test.exs
mix test test/users_test.exs:24

9. Dependencies

List outdated dependencies by running mix hex.outdated; clean unused dependencies with mix deps.clean --unlock --unused

10. Phoenix custom action parameters

If you find yourself using the same variable from conn.assigns, consider having it automatically injected into your actions:

# turn
def show(conn, params) do
  context = conn.assigns[:context]

# into
def show(conn, params, context) do

This can be achieved by overriding action/2 within your controller, (as described in the documentation):

def action(conn, _) do
  args = [conn, conn.params, conn.assigns[:context]]
  apply(__MODULE__, action_name(conn), args)

11. Default parameters and function heads

If you're new to elixir, you might see a function parameter which includes two backslashes: opts \\ []. This is how elixir defines a default value for a parameter.

Default values are related to another strange thing you might spot: functions with no bodies (called function heads). Consider the implementation of Enum.all/2:

def all?(enumerable, fun \\ fn(x) -> x end) # No body?!

def all?(enumerable, fun) when is_list(enumerable) do

def all?(enumerable, fun) do

That first line is required by the compiler whenever you use default values and have multiple versions of the function. It removes any ambiguity that might arise from having default values and multiple functions.

(Function heads are also useful in more advanced cases where documenting actual implementation(s) is messy or impractical, usually related to macros).

12. Pattern matching anonymous functions

Just like normal functions, anonymous functions can also do pattern matching:

def extract_errors(results) do
  Enum.reduce(results, [], fn
    :ok, errors -> errors  # don't do anything
    {:error, err}, errors -> [err | errors]
    other, errors ->  -> [other | errors]

13. Enum.reduce/3

Any function in the Enum module can be implemented using Enum.reduce/3. For example, Enum.map/2 is implemented as a reduce + reverse (since it preserves ordering).

You should always consider using the more readable versions, but if you're just getting started, it's a good mental exercise to consider how you'd implement each function using only reduce/3. Also, if performance matters and you have specific needs (like wanting to map but not caring about order), doing things in reduce/3 might be faster.

14. Don't overlook Enum.reduce_while/3

Enum.reduce_while/3 is a powered-up version of reduce/3. It behaves almost the same, including taking the same parameters, but you control when it should stop enumerating the input. This makes it a more efficient solution for implementing Enum.find/2, Enum.take_while/2 and any custom partial enumeration behaviour you need.

In the normal reduce/3 the supplied function returns the accumulator, which is passed to the next iteration. With reduce_while/3 the function returns a tuple, either: {:cont, acc} or {:halt, acc}. The values :cont and :halt control whether iteration should continue or halt. These values are stripped from the final return.

For example, say we're dealing with user input. We're getting an array of strings. We want to convert them into integers and limit the array to 10 items:

{_count, ids} = Enum.reduce_while(param["ids"], {0, []}, fn
  id, {11, ids} -> {:halt, {10, ids}} # or counter is > 10, stop processing
  id, {count, ids} ->
    case Integer.parse(id) do
      {n, ""} -> {:cont, {count + 1, [n | ids]}}
      _ -> {:cont, {count, ids}}

15. Process Mailboxes

The most important thing to understand about processes is how they interact with their mailbox. Every process has a queue of pending messages called a mailbox. When you send a message to a process (say via send/2, GenServer.call/2 or GenServer.cast/2), that message is added at the back of the target process' queue.

When you receive you dequeue the oldest message from the mailbox. For a GenServer receiving happens automatically; but the important point is that one message is processed at a time. It's not until you return from your handle_call/3, handle_cast/2 or handle_info/2 function that the next pending message will be processed. This is why your processes state is always consistent, it's impossible to have two concurrent threads of execution within the same process overwriting each other's changes to your state.

Whether you're using GenServer.cast/2 or call/2 doesn't change anything from the target process' point of view. The difference between the two only impacts the callers behaviour. If you cast and then call, you're guaranteed that the handle_cast will fully execute before the handle_call and thus the handle_call will see any state changes made by the handle_cast

16. GenServer's init

A failure in your GenServre's init/1 will take down your entire app. The behaviour might surprise you at first, but it isn't without benefits: it provides a strong guarantee about the state of your app. This is particularly true given that supervisors and their workers are started synchronously. In other words, if you have a supervisor tree that places WorkerB after WorkerA, then WorkerB can be sure that WorkerA has been fully initialized (and thus can call it).

If you absolutely need a database connection for your app to work, establish it in init. However, if you're able to gracefully handle an unavailble database, you should establish it outside of your init function. A common pattern is to send yourself a message:

def init(_) do
  send(self(), :connect)
  {:ok, nil}

def handle_info(:connect, state) do
  {:noreply, new_state}

Because processes only handle one message at a time, you could simply block in the above handle_info/2 until a connection can be established. Any call/2 or cast/2 into this process will only be processed once the above function returns. Of course, by default, call/2 will timeout after 5 seconds (which is probably what you want: the app will startup, but trying to use the features that rely on this process will error).

17. nil

There are a couple properties of nil that you should be aware or. First, because nil, like true and false, is actually an atom, it participates in term ordering rules:

nil > 1
> true

nil > "1"
> false

nil > :dune
> true

nil > :spice
> false

Secondly the Access behaviour (var[:key]) ignores nils. So while many languages would throw an exception on the following code, Elixir doesn't:

user = nil
> nil

> nil

18. Sigils

Strings that embed quotes can be messy to write and hard to read. Sigils are special functions that begin with ~ aimed at helping developers deal with special text.

The most common is the ~s sigil which doesn't require quotes to be escaped:

"he said \"it's over 9000!!\""

# vs

~s(he said "it's over 9000")

The difference between the lowercase s and uppercase S sigils is that the lowercase one allows escape characters and interpolation:

~s(he said:\n\t"it's over #{power}")
> he said:
  "it's over 9000"

# vs

~S(he said:\n\t"it's over #{power}")
> he said:\n\t\"it's over \#{power}\"

The other popular sigil is ~r to create a regular expression:

Regex.scan(~r/over\s(\d+)/, "over 9000")
> [["over 9000", "9000"]]

Finally, you can always create your own sigils.

19. [Linked] Lists

Lists in Elixir are implemented as linked lists. In the name of performance, this is something you should always be mindful of. Many operations that are O(1) in other languages, are O(N) in Elixir. The three that you'll need to be most vigilant about are: getting the count/length, getting a value at a specific index and appending a value at the end of the list. For example, if you want to know if a list is empty, use Enum.empty?/1 rather than Enum.count/1 == 0 (or match against []code>).

While appending a value is O(N), prepending is O(1) - the opposite of what you'd see in languages with dynamic arrays. For this reason, when dealing with multiple values, you should favour prepending (+ reversing if order matters).

There are also cases where Erlang's double-ended queue might prove useful. It has O(1) operation to get and add values at both ends of the queue. Sadly, it still doesn't have an O(1) length operation, but you could easily create your own wrapper.

20. iolists

Many language us a buffered string when dealing building a string dynamically. While that isn't an option given immutable data structure, we do have a suitable alternative: iolists. An iolist is a list made of binaries (strings) or nested iolists. Here's a simple example, but keep in mind that iolists are often deeply nested:

sql = ["select ", ["id", " ,name", " ,power"], " from saiyans"]

Much of the standard library and many third party libraries can work with iolists directly:

IO.puts(["it's", " ", ["over ", ["9000", "!!!"]]])
> it's over 9000!!!

File.write!("spice", ["the", [" ", "spice", [" must", [" flow"]]]])
> "the spice must flow"

In some cases functions that receive an iolist will first convert it to a normal binary then process it (which is what Poison.decode/1 does if you pass it an iolist). However, in other cases, processing happens on the iolist directly.

Taking a step back, let's say we have a list of words:

colors = ["red", "blue", "green", "orange", "yellow"]

To append "grey" via the ++ operator, we need to create a new list and copy all the values into the new list. That's why we say it's O(N). Now consider what needs to happen if we do this as an iolist. The result would look like:

colors = [["red", "blue", "green", "orange", "yellow"], "grey"]

The original list doesn't change and so its values aren't copied. We do create a new list (the outer brackets). That new list references the original list at its head, and the new value at its tail. The cost of an iolist is no longer tied to the number of elements in the lists.

21. Piping

Don't do single statement piping. It's the bad sort of clever. This:

params = params |> Map.delete(:id)

Is less readable than this:

params = Map.delete(params, :id)

It's never called for.