loading...

Breaking Down Elixir's `with` Expression

noelworden profile image Noel Worden Updated on ・5 min read

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.

Posted on by:

noelworden profile

Noel Worden

@noelworden

Software Engineer in Boulder, CO - Writing code and getting strategically lost in the mountains

Discussion

pic
Editor guide
 

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.

 
 

Really clear and useful post, thanks!♥️

 

I'm glad you found it helpful!