Elixir’s with statement is fantastic

How to tame one of Elixir’s most powerful constructs and write concise code that’s easy to read, maintain, and debug.

--

I gotta be honest. It was not exactly love at first sight when I first encountered Elixir’s with statement. But just like I learned to appreciate (and love) the whole non-defensive/let-it-crash approach, the same happened here as well.

The result: I was able to rewrite a bunch of complicated functions with very hard-to-read nested case and/or cond statements in a very clean, and, more importantly, maintainable and extensible way using with.

Basic usage

The basic premise of with is that it lets you chain together pattern matches and arriving at the positive result, in a very concise statement. Error handling (if any — remember, this is still let-it-crash land) is done in an optional else clause, after we’re done with the positive codepath.

Let’s look at a basic example. Suppose you’re working with the Elixir library for Stripe and you want to implement a function that creates a new subscription for a customer. Here’s what this may have looked if we hadn’t used the with statement:

Function implementation using nested case statements

What’s wrong with the code above? Here’s a hint:

Spaghetti: better eaten than debugged. Photo by Mae Mu on Unsplash.

Let’s try to write the same function using the with statement:

Function implementation using with statement, without nested case statements

That’s a lot cleaner and easier to read, no?

Alas, it’s not all rosy

But there’s a problem. In the second implementation, it’s really hard to infer which of the steps of the function calls failed the pattern matching, so that we can return a more useful error to the caller. It also makes it harder to debug.

So, did we trade readability for usability?

We can do better!

Fear not, reader! A really simple solution that we’ve been leveraging heavily in our codebase in AgentRisk is to leverage pattern matching. Here’s how:

Each chained function call returns a tuple with an atom that helps us know exactly where things went wrong in the chain.

If we rewrite the previous code example like this, this is what we get:

Function implementation using with statement, with improved error handling

In the example above we are swallowing the error return values, but this was in favor of simplicity. The core idea here is that we tag each step with a unique tuple pattern, so that we can know exactly where the error occurred.

A final note

Hopefully, this post clarified a few of the nuances of Elixir’s with statement. We have not found a better way to implement complex functions in our codebase, for readability, maintainability, and ease of debugging.

I want to thank Carlos Brito Lage for introducing me to this pattern as well as editing an early version of this post.

At AgentRisk, we build investment products and tools for individuals as well as financial advisors, leveraging state-of-the-art machine learning algorithms and time-tested investment theories.

--

--