Using tuple-wrapping to improve `with` expressions in Elixir
Wrap your return values in tuples so you can clearly match them in the else
block
The with
expression allows us to declare clauses and their expected results. The clauses are executed in order until the end of the list, or until one clause doesn’t match with its expected result. Here is the example from the Elixir documentation:
In this example, we are calculating the area of a rectangle. If everything works out we will return {:ok, area}
, if not, we will fall through to the else
block and return the error tuple {:error, :wrong_data}
.
But what happens if we want to send back a better error message? Right now we fall through to the else
block with an :error
atom that is returned from Map.fetch/2
, but we don’t now which clause it came from (was the :width
missing? or was the :height
missing?).
To know which clause is failing, we can wrap each side in a tuple that describes the clause, and then match in the else
block:
This “tuple-wrapping” technique borders on the functional programming concept of “Monads”, but is not something I am going to be discussing here.
Lets look at a slightly more complicated example. Here we have a function that is called when a user wants to join some sort of online game. There are few checks we need to do:
Like the previous example, we have ambiguity in the else
block because the Game.is_full?/1
clause and the Game.is_started?/1
clause both return the same value. In addition, we have another boolean value coming from the User.has_permission?/2
clause, which makes the entire else
block hard to understand.
Lets clean this up using the tuple-wrapping. Observe how it makes the else
block easier to understand, and how it gives the ability to return fine-grain error tuples:
In the past I thought this tuple-wrapping was ugly. So I tried to tuck it away inside the functions that I used in the clauses. This ended up being bad. The problems were:
- If the function I was using was from an Ecto repo, I couldn’t change the function’s return value, so I had to add another layer of function wrappers around the Ecto ones. This led to way too much extra code.
- If a function was used inside a
with
expression and now returns a tuple with its “name”, all of the other places in the code that call that function now have to deal with a tuple that they don’t care about. For example:
For posterity, here is an example of problem 1. Some people may prefer doing it this way for all of their clauses, but I found this led to too much extra code (every with
expression now means defining new private functions). Additionally, having the tuple wrapping in the with
expression makes it easier to tell where the error cases in the else
block originate from.
Here is an example anyways:
There may be cases where you don’t care about sending back specific error tuples, and simply want to know if a whole set of clauses succeeded or not. In this case, using the tuple-wrapping would unnecessary:
Wrapping up, the takeaway is that if you want fine-grained error values from with
expressions with ambiguous clauses, then you should apply tuple-wrapping inside of the with
expression.