This week I bit the bullet and conquered my fear of Elixir's with
expression. I had managed to write a few small ones in the past, but it was not intuitive and I really struggled. But now I had a deeply nested case
statement, and it need to be refactored. It took the fourth or fifth blog post until it clicked for me. I can't quite put my finger on why it took a hot second for me to wrap my head around, but it just wasn't happening.
I did successfully rewrite the case
statement as a with
expression, and although that work is still a PR, I feel good about what I wrote. Coming fresh off that experience, I thought it would be good practice to write my own explanation of how a with
expression is executed, both to solidify my knowledge and write it out in a way that helps others curious in it's utility.
At the core of a with
expression, it is checking that what's to the left of the arrow matches what is on the right. For the sake of wrapping your head around it, the arrow could be replaced with ==
. That's it, really; does left ==
right? The other thing to know is that the expressions are tested in order, and on the first match failure it kicks out of the expression and returns an error. Those two basic aspects are the core of the with
expression. Now lets look at some examples, working from simple to more elaborate.
Using the IEx shell is a great way to sketch out these matches, the first example will be testing if a value is a string:
Below is a pretty basic with
expression. The left side of both arrows is the boolean true
, and the right is testing if the inputs are strings. If you were to execute the right side in an IEx shell, it would output true
, and thats all the statement is looking for. So in this case both of the statements match up, and the do
block is executed. In this expression, the output is a string:
If one of the statements did not match, as is the case below, the do
block will not execute, and the resulting error is output:
You can also work with maps and variables, for a more dynamic with
expression. Because the Map.fetch/2
function returns a tuple, the left side of the arrow also needs to be a tuple. But, that doesn't mean you have to return a tuple in the do
block. The return can be anything you'd like, in this case it's a string.
As per the docs, a variable bound inside a with
expression won't leak, so you can create assignments within the expression to use later on.
There's a good bit of logic happening with that last expression. There's two comparisons, a variable assignment, and a third comparison. So far it's all been happy path scenarios, with no explicit handling of errors. This last expression, just like the first, would simply output the error received, if one occurred.
This next expression does handle errors. Errors are handled in the else
block, and they have to match left to right, just as the statements did before the else
. In this case there are two error options, the :error
, and the underscore. If Map.fetch/2
cannot find the given key, it returns :error
, so that would be a match in the else
block and the output would be "There was an error"
. The underscore acts as a catch-all, and when used in the else
block it basically means that any error that is not an :error
will output "This is a catch-all error"
. As the person
map is written out below, it hits the happy path, does not error, and returns the string from the do
block.
This person
map omits the last_name
key, but the expression is still looking for it. So here the logic will kick out, match with :error
in the else
block, and output that string:
Now the value of first_name
is replaced with an integer in the map. The second line of the expression will fail, drop into the else
block, get picked up by the underscore, and output that string:
Now that the basics of error handling have been covered, let's step it up a notch. If basic error handling exists, theres always the potential to make it more verbose. One way to accomplish that is by writing out the left and right comparisons as tuples. By creating tuples, you can give each a custom a custom key, and in turn match that key as an error in the else
block and display a specific error. Just like the first example, the left side of the arrow is whatever the output would be if that query was run in the IEx shell. For example:
By putting the Map.fetch/2
as the value of the tuple, it becomes a sort of tuple in a tuple, and the tuple keys in the else
block can be utilized if an error occurs. The example below is a happy path, but notice how every line in the with
condition has a unique tuple key. That will come in handy soon.
In this example, the age
field of the map has been turned into a string, so the last match will fail. Because all the matches are setup as tuples, it will hit the {:age_integer, false}
match in the else
block, and will print a string with a specific message:
Now, there's a chance that the last expression was a bit overkill with the error handling. So, if not-quite-so-specific errors are what you're after, you could utilize underscores. As I have it written below, the errors are now only looking for the type of error (:error
or false
) and not the specific tuple keys. Let's skip the happy path and jump right to an error:
And if the last_name
key was not present:
The underscore is a catch-all for the tuple key, and as long as the value :error
matches, it will output that string. Same with the boolean check, if that matched any of tuple values, regardless of the key, it would output it's respective string.
That is the core of a with
expression. Matching left to right, executing a do
block if it succeeds, and and else
block if it does not. I was a bit stubborn to accept it at first, but it is much cleaner logic than a deeply nested case
statement. I hope this helps clear up some of the potential murkiness around this approach for conditionals. If you'd like to explore some more, here are some links I found while researching the topic:
https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with
https://www.openmymind.net/Elixirs-With-Statement/
https://relistan.com/elixir-thoughts-on-the-with-statement
https://blog.agentrisk.com/elixirs-with-statement-is-fantastic-1431bcbcde3
This post is part of an ongoing This Week I Learned series. I welcome any critique, feedback, or suggestions in the comments.
Top comments (5)
I seem to remember somebody cautioning me about
with
statements creating problems with dialyzer. It might have been you at Elixir Daze a few years ago.Im flattered, but I can't take credit for that. My first elixir-based conference will be the upcoming ElixirConf.
Right on!
Really clear and useful post, thanks!♥️
I'm glad you found it helpful!