Early returns can be useful in statement-based languages like Java or C# to restructure code and eliminate nesting.
When using F# for the first time, one of the things I struggled with was the lack of early returns. The concept of an early return doesn’t make sense in an expression-based language, where we’re composing values together to produce new values rather than sequencing statements. How then do we deal with nesting in F#?
Classic Solution: Bind and Computation Expressions
Here’s some code that looks two things up, then combines those things if they both have a value:
One level of nesting is not awful, but you can see how this can get out of hand quickly.
This pattern, where we short-circuit on failure or keep going on success, is common enough that we can use
Option.bind to encapsulate it:
This is perhaps a little better, since it removed the tedious mapping
None -> None, but it’s still nested.
We can use computation expressions to eliminate the nesting:
Astute readers will notice that we can also use
Option.map2 here to eliminate the nesting without resorting to a computation expression. The point is, there’s lots of existing literature on how to handle code like this where you short-circuit on failure and keep going on success.
Off the Beaten Path
Sometimes, though, you may want to short-circuit on success and keep going on failure. Imagine, for example, that you’re authorizing a user to perform an action on a blog post. You may first check if they’re a super user, and if not, you may check if they’re the author of the post, or at least a contributor, before ultimately returning that they’re not authorized.
This works, but it’d be nice to eliminate some of the nesting. Related to that, it’d be nice to make the checks as independent and composable as possible to make the code easier to maintain. We can start by writing each check in its own function like this:
Great. Now let’s sequence the checks together:
Now we just need a way to combine these results. In this case, we want to take the first successful result, and if we don’t find any, then return
NotAuthorized. Let’s write a combine function that expresses this:
Now we can pipe our sequence into
combineResults to get our overall result:
The cool thing about doing this with a
seq is that it will only produce values as needed, potentially saving you some database look-ups or expensive computations if a previous check succeeds.
If all else fails, Embrace the Nesting
Finally, a word of caution. Eliminating nesting is not an end in itself. You should use techniques like those listed above when it helps with readability and maintenance of the code. A few nested pattern matches are fine, and may be the most straightforward and friendly to newcomers.
Expressions and static typing are your friends here: the compiler will ensure that all your types line up.
This post is part of the F# Advent Calendar 2018