For-comprehensions in scala allows writing easy to read and maintain code. In practice, it is just a syntactic sugar for composing
filter operations. Any class providing them can be used in for-comprehension statement.
For such list of users to get an email by name the following code
can be rewritten using for-comprehension like this
The return type is always the same as the first expression’s type.
In scala we often use context containers (monads) like
Future[A]- for asynchronous computations,
Option[A]- for optional values,
Try[A]- for exception handling,
Either[A, B]- usually to express an error or a value.
It is possible to use all of them in for-comprehensions.
However, each statement must result in the same type as the first one. It is not possible to mix
Option, you need to convert
Either in order to use it next to
Either resulting statement. Things get even more complicated when you combine monads with each other.
You may have functions that return an async optional value
Future[Option[A]] or an async error or value
Future[Either[Error, A]]. For this, you need to use nested for-comprehensions.
In this case, for comprehensions are not easy to read as when using single monads..
To avoid nested for-comprehensions you can use a common context container (monad) and transform all expressions into it. In the example above, the combined monad is
Future[Either[Error, A]]. If you have a way to transform to result of
getUser methods into this monad, you can make this for-comprehension look simple.
I prepared an example that shows how to use scalaz monad transformers to build neat for-comprehensions for a http4s service with combined monads. The example app provides a REST endpoint that authenticates a user using token and gets the user and his devices. The model looks like this:
There are two methods: authenticate a user using token and get a device by id.
scalaz.concurrent.Future[Throwable \/ A]and is used in http4s a lot. Read more about task here.
A \/ Bis a scalaz way to express
authenticate function returns asynchronous computation of either error or user. DeviceService’s
getByid function return asynchronous computation of optional device.
In order to use both functions in for-comprehension without nesting, we need to use a combined monad.
EitherT[Task, Failure, A] represents something like
Future[Either[Failure, A]]. It is a monad transformer. It allows wrapping
Either into another monad like
Result[A] shares the behaviour of
Either. In simple terms, it provides flatMap and map methods that do the nesting for you.
It is easy to create a
Result[A] of a single value. You just need to do
"email".point[Result]. However, to build it from a
Task[Option[A]] in a convenient way you need helper functions.
Here, are some functions that simplify creation of a
Result[A] and provide a way to express a failure.
|>is the Thrush combinator. In this context it applies the value on the left to a function on the right.
Although, the for-comprehension operates on combined monads the code is not nested. In addition, it handles the cases when
Either == Left or
Option == None. Complete example can be found at github. Run
sbt test to see the spec.
There are many resources that cover the monad transformers topic in more detail. Here, I wanted to show a particular problem that can be solved using them and the benefits. I used scalaz and http4s just as an example. Similar functionality can be achieved in vanilla scala as well, but requires more code.