Composing Future, List and Either

Posted on June 13, 2017 · 3 min read

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.

The context

Consider the following model for a Commit, an Author and their product CommitAndAuthor:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-model-scala

And let's assume we have the following methods already in place:

  • getCommits: get all commits from a repository
  • getAuthor: get the Author of a specific commit
https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-githubapi-scala

The task

Now 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:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-compose-scala

Approach #1 - Using vanilla Scala

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:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-firstapproachinner-scala

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:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-firstapproachsequence-scala

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]:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-firstapproachfold-scala

And finally, we have to compose the result of calling search() with this previous code:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-firstapproach-scala

which, if we expand the listCommitsAndAuthor function, would look like:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-firstapproachexpanded-scala

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.

Approach #2 - Using monad transformers

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:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-eithert-scala

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.

Refactoring

Now we can rewrite listCommitsAndAuthor, the innermost function, as:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-secondapproachinner-scala

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]]:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-secondapproachtraverse-scala

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:

https://gist.github.com/pbassiner/6bb41c132da822cce2020827d6163cc9#file-secondapproach-scala

In conclusion

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.

References


Comments

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.