Practical Monads: Dealing with Futures of Options
Using monad transformers to simplify futures of options
The use of futures is prevalent in a lot of asynchronous Scala code. Oftentimes, a future can hold a value that may or may not exist and we wrap that in an Option, which leads to methods that look like this:
def getCount(): Future[Option[Int]]
This is fine, but in order to map over it we need two calls to map. One to map over the future and a second to map over the Option like so:
getCount().map(option => option.map(result => result + 1))
Which is a little verbose, but not terrible. But what if we have multiple Futures of Options that we need to map over? Ideally we’d like to do something like this as we would with a plain old Option:
But since we’re inside a future, the above won’t compile. In order to fix this, we need to map on the future first. We can do this with a nested for comprehension or a series of flat maps and maps:
This is pretty ugly for just adding two numbers together. However, there’s a way we can clean it up and make it look like the simple for comprehension we want. To do this we’re going to create an object which hides the extra map and flat map on the future.
This is known as a monad transformer. It’s quite simply a monad that wraps another monad. Scalaz and Cats both provide implementations of these transformers. Here, we’ll take a look at how to implement it ourselves to see just how easy it is without pulling in a dependency.
Creating an Option Transformer
Unfortunately since Future has no monad trait we’ll need to define one ourselves. We’ll need to have the map, flat map and pure methods on our trait. We’ll define these methods as operating on a value of type T[_] (this is where we will pass in our Future) and taking functions as flat map and map normally do:
We’ll then need to create an implementation of this for Futures. Once we have this we’ll be able to pass it in to our transformer so we can call map, flat map and pure on our futures directly.
Creating this is trivial — all we do is delegate directly to the methods on the Future we pass in. The only exception is pure, which just wraps a value in a Future:
Now that we have this we can define an Option transformer. We’ll take two values in our constructor, an Option value wrapped in a type T and a monad instance for that type T. Then to get our for comprehension to work we’ll need to implement map and flat map:
Our map method is pretty easy to follow. We just call map twice, once on the monad instance and then once on the option with the function we’re passed in.
Flat map is a little trickier because the function we’re mapping with returns an OptionTransformer. So we map over the option as before, but when we call our function f we need to extract the value of the resulting OptionTransformer. This gives us a result with the following type signature:
a.map(b => f(b).value) == Option[T[Option[B]]]
So we have our correct value wrapped in an Option. Now we just need to extract that value or return None. To do this we call getOrElse on the Option. In the case our map produced a Some, we just get the value, otherwise we need to wrap None in a Future, hence the call to pure.
Now that we have this we can simply wrap our Future[Option[_]] in an OptionTransformer and use a for comprehension as if we were working directly with Options:
Now we’ve turned the nested comprehensions into something much simpler than what we had to do earlier. It is worth noting that we can apply this to any type that wraps an Option, provided we write a proper monad instance.
In many cases, monad transformers give us a simpler and cleaner way to work with nested structures and are worth considering when you find yourself dealing with them.
The above code with unit tests is available on GitHub at the link below.