The Two Most Useful Elixir Functions You May Not Know: tap() & then()
The pipe operator in the wonderful Elixir language is one of the most underrated features for developer experience. Two simple, recently added functions can greatly enhance how the pipe operator is used, enabling longer and more readable computational pipelines.
by Dave Thomas
⭐️⭐️⭐️⭐️
In this post, we'll explore these two new functions, tap()
and then()
. We'll learn what they are, how they're used and see how they can be used with the pipe operator to write cleaner, simpler code in Elixir.
The Pipe operator in Elixir
If you've written any code in Elixir, you're probably familiar with the pipe operator. The pipe operator in Elixir is a powerful tool that allows developers to write more readable and expressive code. It allows developers to chain together function calls, "piping" the output of one expression into the first argument of the next:
# A nested expression without the pipeline operator
function_d(function_c(function_b(function_a(value))))
# The same expression using the pipeline operator
value
|> function_a
|> function_b
|> function_c
|> function_d
The pipe operator helps to solve the "parenthesis hell" found in many languages, in which a deeply nested series of function calls leads to a mess of opening and closing parenthesis which can be difficult to read or to spot missing characters in.
Additionally, pipelines are easy to read because they present computation as a simple linear flow without variable bindings or inline flow control
Pipeline Side Effects with tap()
The first function to enhance the power of pipelines is tap()
. The tap()
function lets you add side effects to a pipeline. In a functional programming context, side effects refers to actions which have effects other than manipulating the function's input value. In practical terms this might include logging, saving data, analytics calls, etc.
If you use IO.inspect()
or dbg()
in your Elixir code, you're probably already familiar with the idea of inserting them into the middle of a pipeline. This is especially common during development to provide additional logging to an application. In the following example, we show a simple pipeline with an IO.inspect()
call in the middle:
get_user_id()
|> MyApp.get_user()
|> IO.inspect()
|> render_user()
The tap()
function is similar in spirit, but allows for other kinds of side effects than simple logging. The function takes two arguments: an input value v
, and a function f
. It executes the function f with the input, f(v)
, then returns the same input v
unmodified. Consider a scenario in which we need to perform some analytics logging when looking up and rendering a user record:
user = get_user_id()
|> MyApp.get_user()
Analytics.track(:user_viewed, user.id)
render_user(user)
Using the tap()
function, we can insert the analytics call into our pipeline and continue with the computation:
get_user_id()
|> MyApp.get_user()
|> tap(fn user -> Analytics.track(:user_viewed, user.id) end)
|> render_user()
This lets us write longer pipelines without having to store intermediate values, and improves readability.
Transforming Pipeline Data with then()
Another challenge with pipelines is that the data we receive as the output from one function is not quite the input we need to another. A common example of this might be when we need a particular field from a struct to perform a computation.
For example, imagine an authorization use-case in which we need to look up a user, fetch a permission field off of the user struct, then perform some sort of computation to determine whether the user has permission to take an action. We might write this code as follows:
# Look up a user, get their permissions, see if they can edit the page
user = get_user_id()
|> MyApp.get_user()
can_edit = user.permissions
|> can_edit_page(page_id)
Using the then()
function, we can improve on this and simplify our pipeline:
can_edit = get_user_id()
|> MyApp.get_user()
|> then(fn u -> u.permissions end)
|> can_edit_page(page_id)
Or, using the function capture operator (&):
can_edit = get_user_id()
|> MyApp.get_user()
|> then(& &1.permissions)
|> can_edit_page(page_id)
In effect, then()
allows us to pipe computations into anonymous functions without breaking the pipeline. This simple change lets us build longer pipelines without intermediate bindings or calls to functions defined elsewhere.
Recap: Build Better Pipelines with tap()
and then()
Pipelines help improve code readability by implementing simple, linear flows of information, removing (or at the very least abstracting out) flow control and variable bindings. While deceptively simple, these two new Elixir functions, tap()
and then()
can enable longer and more readable pipelines, and are part of what makes the Elixir language such a joy to use.