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 Future
s 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 List
s, 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.