Very often we deal with Future[A], List[A] and Either[A, B] types but, when it comes to composing them, things can get messy.
I find it easier to understand with a real-world-ish example.
Consider the following model for a Commit, an Author and their product CommitAndAuthor:
And let's assume we have the following methods already in place:
getCommits: get all commits from a repositorygetAuthor: get the Author of a specific commitNow that we can retrieve all the commits and the author of a specific commit, let's try to provide all the commits with their authors:
So we just need to retrieve all the commits and, for each one, retrieve its author and then combine both commit and author.
We can start with the easiest part, the innermost author retrieval from the list of commits:
So now we have a List[Future[Either[A, B]]] but we need it to be a Future[Either[A, List[B]]] after we combine it with the result of search().
First of all we'll use Future's sequence traversal to reduce many Futures to a single Future:
From the current Future[List[Either[A, B]]] we need to convert it to a Future[Either[A, List[B]] and we'll do so by mapping over the Future and then folding the List into a single Either containing a List[CommitAndAuthor]:
And finally, we have to compose the result of calling search() with this previous code:
which, if we expand the listCommitsAndAuthor function, would look like:
Ok then, we got it but, even though it works as expected, it's kind of verbose and there's more boilerplate code than actual logic.
Most code from the previous approach focuses on mapping over Future and Either to manipulate the underlaying data structures, and here's where monad transformers come into play.
We'll use the datatypes from Cats, a Typelevel project providing a lightweight, modular and extensible library for functional programming.
We can take advantage of EitherT, a monad transformer for Either:
EitherT's flatMap will apply both Future and Either's flatMap functions so we don't have to, and it will preserve their short-circuiting nature either (pun intended) if Future fails or Either is Left.
To extract the value from an EitherT instance we just need to call the value function, which will transform it back to a Future[Either[A, B]] instance.
Now we can rewrite listCommitsAndAuthor, the innermost function, as:
Since we're composing Future and Either we need a couple of imports from Cats and an ExecutionContext.
We are left with a List[Either[A, B]]. Then, using the sequenceU method we can traverse the List and transform it into an Either[List[A, B]]:
Notice that since we are now also composing Lists, we need a couple more imports from Cats.
Finally we can compose the call to search(), wrapped in an EitherT as well, with the previous code:
Now we have a much cleaner implementation which let's you focus on the relevant logic and gently hides all the boilerplate code related to the composition of the monads.
The full code, including all required imports, is available in this gist.
Would you like to leave a comment? Since this blog is hosted on GitHub Pages there's no straightforward way to do so.
Instead, you can add a comment in this GitHub issue. If you'd like to see it here, refresh this page after posting the comment.