Scalaz’s \/
(“Disjunction” or “Either”) and Scala’s Future are great examples of how the monad
abstraction can be used to simplify logic. In this post we will be looking at why we need to compose
monads, how to do it (hint: with Monad Transformers), and how they can significnatly simplify our code.
The post is targetted at Scala beginners like myself who has just started to dabble with the functional
side of Scala. I will be focusing more on practical examples to show how to use monad transformers.
I will be using Scalaz’s \/
and Scala’s Future
(scala.concurrent.Future
)
Note that Scala’s standard library has an Either
type as well but if you see mentions of “Either”,
I am talking about Scalaz’s \/
Backstory - Parcel Mega Corp
Parcel Mega Corp is a company that makes parcels. Each Parcel
needs to have a name
,
address
and mobileNumber
field, for obvious reasons.
case class User(userId: String, name: String)
case class Parcel(name: String, address: String, mobileNumber: String)
Scalaz Disjunction (\/
)
With \/
, you can model shortcircuiting when an error happens during your chain of operations, without the need for explicit error checks.
def makeParcel(userId: String): \/[SendParcelError, Parcel] = {
for {
user <- getUser(userId)
address <- getAddress(user)
mobileNumber <- getMobileNumber(user)
} yield Parcel(user.name, address, mobileNumber)
}
In the above example, getUser
, getAddress
and getMobileNumber
have the type \/[SendParcelError, X]
, but the code doesn’t need to explicitly
check the result of each call because the monad bind (flatmap) for \/
handles them for us.
If getUser
fails with a UserNotFound
error, the whole result of makeParcel
function will be -\/(UserNotFound)
Scala Future
With Future
, you can write your asynchronous code in a synchronous manner, though the code
actually runs in a non-blocking, asynchronous manner.
Let’s pretend for a bit that our previous code example is fetching user/address/mobile number information from remote servers, and it cannot fail (barring unexpected exceptions which is handled upstream). You can have some code that looks almost the same. (I have put some type annotations in so you won’t be confused)
def makeParcel(userId: String): Future[Parcel] = {
for {
user <- getUser(userId): Future[User]
address <- getAddress(user): Future[String]
mobileNumber <- getMobileNumber(user): Future[String]
} yield Parcel(user.name, address, mobileNumber)
}
In words, the above logic is
- Fetch the
User
from a remote server - When the result comes back, look up the address of the
User
on another remote server - When we obtain the address from the remote server, look up the
mobileNumber
of theUser
in another remote server
Future actually has a failiure mode as well (Future.failiure(cause: Throwable)
) which causes the
whole for comprehension to shortcircuit, but the type Throwable
should indicate to you that we should
be using this for exceptional cases (e.g. Unhandled exceptions).
The two code snippets from using Future
and \/
shows you how monads are a great way to model
chaining contexts.
Combining Future and \/
That’s cool and all, but what if I need to query a remote server and the user may or may not exist? The return type of my functions will look like Future[\/[SendParcelError, X]].
Let’s see what that looks like!
def makeParcelMegaCorp(userId: String): Future[\/[SendParcelError, Parcel]] = {
import ParcelMakerMegaCorp._
for {
userOrError: \/[SendParcelError, User] <- remote_getUser(userId)
addressOrError: \/[SendParcelError, String] <- userOrError match {
case \/-(user) => remote_getAddress(user)
case -\/(error) => Future.successful(error.left)
}
mobileNumberOrError: \/[SendParcelError, String] <- userOrError match {
case \/-(user) => remote_getMobileNumber(user)
case -\/(error) => Future.successful(error.left)
}
} yield for {
user <- userOrError
address <- addressOrError
mobileNumber <- mobileNumberOrError
} yield Parcel(user.name, address, mobileNumber)
}
Ouch! That’s pretty convoluted! Let’s look at the our logic
- Fetch
User
from the remote server asynchronously - Fetch
address
from the remote server asynchronously IF theUser
exists - Fetch
mobileNumber
from the remote server asynchronously IF the both theUser
and theiraddress
is found
In our code example above, we need to manually implement the shortcircuit logic with something like
case -\/(error) => Future.successful(error.left)
. This is because we couldn’t really compose the Future
and \/
monad together by nesting them – We are still only working with the Future
monad.
The EitherT Monad
Let me introduce a little magic type called EitherT
and wrap every Future[\/[SendParcelError, Parcel]]
we have with
EitherT.apply
:
def makeParcelMegaCorp_improved(userId: String): Future[\/[SendParcelError, Parcel]] = {
import ParcelMakerMegaCorp._
val logicChain = for {
user: User <- EitherT(remote_getUser(userId))
address: String <- EitherT(remote_getAddress(user))
mobileNumber: String <- EitherT(remote_getAddress(user))
} yield Parcel(user.name, address, mobileNumber)
logicChain.run
}
It compiles and the code looks much cleaner! EitherT
successfully composed our Future
and \/
into so now the result we “pull out” of
our composed monad is just the right(\/-
) value from an asynchoronous operation.
Let’s look at the behaviour of our combined monad more carefully:
- When the
Future
completes, check that it has not failed (i.e. no exception happened) - Check that the
\/
is the\/-
(“right”) side
Here’s an ASCII diagram showing this logic
Future \/
-------- -----------
\/-(result) ----> result (continue with the for comprehension with this value)
/
/
Success ---
/ \
/ \
- -\/(error) ----> shortcircuits with a Future.successful(-\/(error))
\
\
Failure ----> shortcircuits with a Future.failed(Throwable)
Cool, but how does this actually work?!
the logicChain
variable from our code snippet above is of the type EitherT[Future, SendParcelError, Parcel]
.
Weird.. Let’s dig deeper!
The definition of EitherT tells us that it is just a newtype wrapper around its underlying combined monads:
final case class EitherT[F[_], A, B](run: F[A \/ B])
// Substituting in Future as F, and renaming "run" into
// "underlyingMonads" for clarity
final case class EitherT[Future, A, B](underlyingMonads: Future[A \/ B])
To make it more clear what EitherT
is doing, let’s look at the map
method:
def map[C](f: B => C)(implicit F: Functor[F]): EitherT[F, A, C] =
EitherT(F.map(run)(_.map(f)))
/* Substitute Future in, keeping in mind that run == underlyingMonads == Future[A \/ B] */
def map[C](f: B => C): EitherT[Future, A, C] =
EitherT(
underlyingMonads.map { either: \/[A, B] =>
either.map(f)
}
)
So essentially what map
method of EitherT
does is first calls map
on the future to get the \/
result,
then calling map
on it again and transform the right side of the \/
using function f
. Finally it wraps the
future back into the EitherT
newtype wrapper.
It does similar things for methods like flatMap
, filter
, withFilter
, which is what allows it to be
used in Scala’s for comprehension.
The reason why EitherT (T stands for Transformers) is so useful is that it can transform any monad F
:
EitherT[Option, A, B] // underlying monad is Option[A \/ B]
EitherT[List, A, B] // underlying monad is List[A \/ B]
There are other transformers like OptionT and ListT too!
ListT[Option, A] // underlying monad is Option[List[A]]
More importantly, since a Monad Transformer is also a monad itself, you can stack up monad transformers.
Although doing this in Scala is a bit more painful than Haskell when you have monads like \/
which has
more than one type parameter.
Extra Tips
Often you will find yourself having some operations that does not have the same type as your transformer. In this case, you will need to find a way to “lift” your value into your transformer type.
In our example, checking that the User
has an address may not require a remote server call if the User
class
contains their address as one of the fields. In this case the function that does the checking will have a signature
like this:
def checkUserHasAddress(user: User): \/[SendParcelError, Unit]
In this case, EitherT.fromEither
will ‘inject’ your pure \/
into a Future
for {
// ...
user <- // ...
_ <- EitherT.fromEither(checkUserHasAddress(user))
// ...
}
For cases where you have a function that returns Future[A]
, then you can use EitherT.right
or EitherT.left
to lift it to your monad transformer stack.
for {
// ...
address <- // ...
// getPostCode has the type Future[A]
postCode <- EitherT.right(getPostCode(address))
// EitherT.right takes a Future[A] and turns it into a EitherT[Future, Nothing, A]
}
It is highly beneficial to read through Scalaz’s documentation (better, the source code) and see what helper
functions are available. In general, whatever you can do with a \/
, there is an equivalent for EitherT
.
For other transformers such as OptionT
and ListT
there will be various helper functions too.
Final Words
We looked at how monads transformers can be useful for composing two monads together.
At work I find myself using a lot of EitherT[Future, A, B]
to model asynchronous operations that can fail,
and chaining them together is a breeze, abstracting over the fact that each operation is asynchronous and
can fail.
Also at this point in time I am looking to gradually migrate from Scala’s built-in Future
to Scalaz’s Task
,
which from my various research seems to yield better performance and safety. (It is very similar to Haskell’s
IO
monad, but the API is more focused on dealing with multithreading and asynchronicity)
The source code for this example can be found Here (Using Scalaz version 7.2. If your project is using an earlier version
you may need to add scalaz-contrib
to your dependencies to get the Monad
instance of Scala’s Future
with
import scalaz.contrib.std.scalaFuture._
)
If you find any errors or think I can improve my post in certain ways, please send me an email! I would love to keep this up to date.