The only thing I've missed while working in Elixir the last few years is an early return.
In Javascript, you can do this:
let contrivedFunction = arg => {
if(!arg) {
return 'falsy';
}
if([] !== []) {
return 'wtf';
};
// This will never be evaluated because JS is
// a sad language where [] does not equal []
return 'truthy';
};
At any point, you can stop the execution of the current function and return a value, and nothing after that point will run.
Usually, in Elixir, you can get by just fine without that ability. You can use the with
statement, pattern matching, or control flow like case
and cond
.
But sometimes, I really miss having an early return. Usually when I'm working in a really messy domain with a lethal combo of tech debt, third-party APIs, and a long chain of processing where things can go wrong in a lot of places.
I recently ran into a situation like that, and decided to do some digging. I knew that a function stopped execution as soon as an error was raise
d, so I thought I could maybe write a macro for that functionality. But then in the Elixir guides for try
, catch
, and rescue
, I realized that the solution was hiding in plain sight:
def contrived_function(arg) do
if !arg do
throw("falsy")
end
if [] != [] do
throw("wtf")
end
# Elixir is sane and [] does equal [], so this is possible
"truthy"
catch
value -> value
end
It turns out, when you throw
within a function, execution stops and you can catch
(and return) the value! No macros or extra abstractions needed.
I don't anticipate that I'll use this pattern very much - in most cases the standard methods of control flow are easier to reason about and follow.
But, I hope that like me you appreciate having an extra tool in hand for those situations when it feels like the best way to express a bit of logic.
Top comments (5)
I usually use pattern matching as a guard / return statement:
I use this approach as well :)
I subscribe to the common belief that exceptions should be reserved for exceptional situations: actual failures.
You should most certainly profile this code vs. a more traditional approach. I would be surprised if you didn't see a huge performance gap ... throwing an exception is most languages is very costly, and disruptive to the kinds of optimizations that a compiler might make.
I also subscribe to that belief, and it's how I've operated so far in my career in Elixir.
The interesting thing to me about
throw
/catch
in strictly the Elixir context is that I've never actually seen it used for exceptions (or really anything else honestly). Typically the only thing you do with exceptions isrescue
them, which isn't common but does come up occasionally. The one example I can think of is that the Bamboo email library until recent versions would raise instead of returning an:error
tuple if the third-party email provider like SendGrid didn't send the email successfully, so you were forced torescue
if you wanted to handle an email failure gracefully.I created a quick Benchee script on your recommendation, and these were the results on my 2013 MBP:
Using a
throw
does interfere with tail call optimization, which is one of the tradeoffs you mentioned might be present. It looks like the code with thethrow
is about half as fast as the one with the return, so there is somewhat of a performance penalty.I really am not recommending that anyone takes this as a pattern and uses it extensively instead of the usual control flow structures in Elixir. It's just an interesting quirk of the language that a rarely-used BIF plus the elegant syntax for function level
catch
/rescue
handling turns out to implement an early return with surprisingly little ceremony.EDIT: the original results I posted showed that the
throw
version was faster, but I realized there was a bug in my script and I was calling thethrow
version in both cases. I updated it, and thethrow
version is about twice as slow.I usually reach for a
cond
if I want an early return-like function.