Ten Things I Wish I Knew Before Using Elixir

Someone working on code at a desk

In this blog post, I will share 10 things that I wish I had known about when I first started using Elixir. These are just a few tips and tricks covering lesser-known aspects of the Elixir language and tools. I hope at least a few of them will prove to be useful to you in your Elixir development.

1) Running stale tests in Mix

Despite claims to the contrary, Test-Driven Development is alive and well.

Reports of my death have been greatly exaggerated.

     — Test-Driven Development

If you are practicing TDD, you will be frequently running your tests to verify that your code works as expected. But if you have a large codebase—and hence a large test suite—you don’t want to run your entire test suite for each incremental change that you make.

mix test --stale to the rescue! The first time you use the --stale option, all tests will be run. On subsequent runs, though, only stale tests are run. A test is considered stale if it directly or indirectly references a module that has been changed, or if the test file has itself been changed.

If you’d like to take it one step further and automatically run stale tests whenever a file is changed, then mix test.watch is for you.

2) Using IO.inspect in a pipeline

Elixir pipes are awesome and allow for very concise and expressive code. However, they can sometimes make debugging difficult because it’s hard to tell where in the pipeline that data doesn’t match expectations. If you’d like to peek at data in the middle of a pipeline, you can take advantage of the fact that IO.inspect returns whatever you pass as its first argument, and you can even provide an informative label to help identify where the data is in the pipeline.

[1, 2, 3]
|> IO.inspect(label: "original data")
|> Enum.map(&(&1 * 2))
|> IO.inspect(label: "after doubling")
|> Enum.reduce(fn(n, sum) -> n + sum end)
|> IO.inspect(label: "after summing")

This produces the following output:

original data: [1, 2, 3]
after doubling: [2, 4, 6]
after summing: 12
12

3) The update syntax for maps

The Map module provides several functions for manipulating maps, but what if you want to modify several different values in a map? You could make several calls to Map.put/3 in a pipeline, but it is much more convenient to use Elixir’s built-in “update” syntax for maps:

data = %{
  value: 1,
  created_at: DateTime.utc_now(),
  updated_at: DateTime.utc_now()
}

# Update the value and updated_at values:
%{data |
  value: 2,
  updated_at: DateTime.utc_now()
}

4) Updating deeply nested data

As nice as the aforementioned update syntax for maps is, it is of no use for updating data in deeply nested maps. For that, we’ll need another tool that allows us to reach into a deeply nested data structure such as a map, and update the data therein. For this purpose, Elixir provides us with the update_in/3 function. The update_in/3 function takes a map (or another data type that implements the Access protocol) as its first argument, a list of keys as its second argument, and a function that provides the updated value as its third argument. Here’s a simple example adapted from the Elixir docs:

iex> %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> |> update_in(["john", :age], &(&1 + 1))
%{"john" => %{age: 28}, "meg" => %{age: 23}}

In fact, Elixir provides several related functions and macros for dealing with nested data structures:

Get to know your standard library.

5) List destructuring

We all know and love pattern matching in Elixir, but what about the closely related concept of destructuring? Destructuring can be used to pluck elements out of a list based on position. This sample IEx session shows list destructuring in action, including an example of the little-known Kernel.destructure/2 macro:

iex(1)> list = [1, 2, 3]
[1, 2, 3]
iex(2)> [first|rest] = list
[1, 2, 3]
iex(3)> {first, rest}
{1, [2, 3]}
iex(4)> [first, second|rest] = list
[1, 2, 3]
iex(5)> {first, second, rest}
{1, 2, [3]}
iex(6)>  [first, second, third, fourth|rest] = list
** (MatchError) no match of right hand side value: [1, 2, 3]

iex(6)> destructure([first, second, third, fourth], list)
[1, 2, 3, nil]

6) You can pipe directly into a case expression

Have you ever written code that resembles this?

result =
  value
  |> do_something()
  |> do_something_else()
  |> do_one_more_thing()

case result do
  :ok -> handle_ok_result()
  {:error, reason} -> handle_error_result(reason)
end

That result variable stands out like a sore thumb. It’s only there to be used as the value for the case expression. Thankfully, we don’t actually need the result variable at all because Elixir allows us to pipe a value directly into a case expression:

value
|> do_something()
|> do_something_else()
|> do_one_more_thing()
|> case do
  :ok -> handle_ok_result()
  {:error, reason} -> handle_error_result(reason)
end

7) Shell history in IEx

By default, IEx does not remember history between sessions, so you cannot scroll through history using the arrow keys, or search through history with Ctrl-r. In recent versions of Elixir, however, you can turn shell history on in IEx by enabling shell history in the ERL_AFLAGS environment variable:

export ERL_AFLAGS="-kernel shell_history enabled"

And now IEx will finally behave like every other interactive shell ever created.

8) IEx helpers

IEx comes with a slew of incredibly useful built-in helpers, which you can learn about by firing up iex and typing h. Some of my favorites are:

  • h/1 – prints help for the given module, function or macro
  • i/1 – prints information about the given term
  • v/0 – retrieves the last value from the history
  • v/1 – retrieves the nth value from the history
  • recompile/0 – recompiles the current project

9) The IEx break-trigger

You’re in IEx and typing (or pasting) a lengthy multi-line expression, you hit enter and…nothing. The IEx prompt is just sitting there waiting for more input. Clearly, you’ve missed a closing quotation mark or bracket somewhere, but it’s hard to tell where. At this point, you’d just like to break out of the errant expression and start over, but Ctrl-gdoesn’t work and Ctrl-c completely exits the shell. IEx has a solution for you: the break-trigger. Simply type #iex:break on a line by itself and IEx will break out of any pending expression and return to its normal state:

iex(1)> ["ab
...(1)> c"
...(1)> "
...(1)> ]
...(1)> #iex:break
** (TokenMissingError) iex:1: incomplete expression

10) Dialyzer is right and you are wrong

If you are using Dialyzer on your project (and you should be), then you’ve probably been confounded by the rather cryptic error messages that it produces. You might even be convinced that Dialyzer is wrong in some cases. I hate to break it to you, but Dialyzer is probably right even if you can’t immediately figure out why. The Dialyzer tool uses an optimistic typing model, which means that it will only produce an error when it can definitively prove that there is a type error. If you are confronted with an error message from Dialyzer, it would behoove you to investigate to find the root cause.

If you’d like to learn more about the history and philosphy behind the Dialyzer—as well as how to effectively use it in Elixir projects—you can watch my talk from ElixirConf 2016: Dialyzer: Optimistic Type Checking for Erlang and Elixir.

Conclusion

I hope this blog post has taught you something useful about Elixir. If you’d like more Elixir tips and tricks, you’ll find a wealth of them at Killer Elixir-Tips. What are some of your favorite Elixir tips? Feel free to leave them in the comments below!

Send this to a friend