Breaking Down Elixir's `with` Expression
Tags: elixir, thisweekilearned, learning
Published: 2020-05-04 18:29:09

This week I bit the bullet and conquered my fear of Elixir's [`with`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#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: 

{% gist https://gist.github.com/noelworden/11dec929ac9dce325bcdf8a1f6d3c711 %}

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:

{% gist https://gist.github.com/noelworden/45be359e88b075cc05df18be69a60d99 %}

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:

{% gist https://gist.github.com/noelworden/811c824eed25c36fe2c4d1aa0c15e0ec %}

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.

{% gist https://gist.github.com/noelworden/c5d649f523ee6bfd89099b916d853ec2 %}

As per the [docs](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with), a variable bound inside a `with` expression won't leak, so you can create assignments within the expression to use later on.

{% gist https://gist.github.com/noelworden/cf1cfd58a113a8a7f7d4c3b6d309c8d9 %}

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.

{% gist https://gist.github.com/noelworden/cd672374e8dc7f359041ff220e4afe93 %}

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: 

{% gist https://gist.github.com/noelworden/e210173d4ab8b5c49f0d584f0b2f65bd %}

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:

{% gist https://gist.github.com/noelworden/b20761a057d666d1bc959dcfa6013f63 %}

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:


{% gist https://gist.github.com/noelworden/5ee6834d7718d5a9d4ce75a70417938e %}

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. 

{% gist https://gist.github.com/noelworden/8ca370c2ba1f23ed3ca979fb36a59521 %}

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:

{% gist https://gist.github.com/noelworden/637b5c53d4428a0e10a1c97b353d33c3 %}

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:

{% gist https://gist.github.com/noelworden/02f9f2ba73c6477da5a261a71cf18e08 %}

And if the `last_name` key was not present:

{% gist https://gist.github.com/noelworden/3f80859c7e6ef3af0f20b95ac15692a7 %}

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](https://dev.to/noelworden/beginning-of-a-blog-series-5aj3). I welcome any critique, feedback, or suggestions in the comments.