Hands on function composition with monad transformers
When using functional programming languages like Scala, developers spend a lot of their time composing functions and effects. One of the most common ways to express composability is to use monads. However, composing functions that return different monads can become quite messy and, without the right tools, quickly turn into a massive headache. That’s where Monad Transformers, which are the main focus of this post, come in handy!
The focus of this post is not to explain monads and their ability to be transformed as there are already dozens of posts that do that (I will reference some of them in the end of this post). The main goal is to show a concrete example of how transformers can help you in situations where the composability of your code becomes entangled. For the sake of keeping the scope of this text sane, you can think of monads as a design pattern that helps function composability. Furthermore, the only two monads I’ll approach in this post are the Future
and the Option
monads which are commonly used if you’re a Scala developer.
Composing a simple API
Let’s suppose we’re using a simple API to query a database in a synchronous way:
sealed trait Employee {
val id: String
}
final case class EmployeeWithoutDetails(id: String) extends Employee
final case class EmployeeWithDetails(id: String, name: String, city: String, age: Int) extends Employee
case class Company(companyName: String, employees: List[EmployeeWithoutDetails])
trait SyncDBOps {
protected def getDetails(employeeId: String): Option[EmployeeWithDetails]
protected def getCompany(companyName: String): Option[Company]
}
From this simple design, the following conclusions are derived:
- An
Employee
is either anEmployeeWithDetails
or anEmployeeWithoutDetails
- A
Company
has a name and a list of employees without their details - A
SyncDBOps
can fetch aCompany
by its name and anEmployeeWithDetails
by its id.
Suppose we want to create a new software layer on top of this API and expose a single function with the following specification:
- Receives two strings -
companyName ; employeeId
- Gets a company using
companyName
- Verifies if that company has an employee with id equal to
employeeId
- Retrieves the employee’s age using the
employeeId
A simple way to do this would be to use for-comprehensions
and compose the two API calls:
def getEmployeeAge(employeeId: String, companyName: String): Option[Int] = {
for {
company <- getCompany(companyName)
if company.employees map(_.id) contains employeeId
details <- getDetails(employeeId)
} yield details.age
}
If any of the two functions we’re calling - getCompany
and getDetails
- returns a None
, the getEmployeeAge
function will immediately terminate and return None
as well. This code is quite simple and allows us to compose two function calls that return the Option
monad in a readable way.
Raising the bar for function composition
Let’s imagine that in order to try and raise the throughput of our service, we decided to switch our API layer into an asynchronous API that returns Futures
. The data API would then change into the following code:
trait AsyncDBOps {
protected def getDetails(employeeId: String): Future[Option[EmployeeWithDetails]]
protected def getCompany(companyName: String): Future[Option[Company]]
}
We will now try to compose our two API calls and get our employee’s age using Future
of Option
:
def getEmployeeAge(employeeId: String, companyName: String): Future[Option[Int]] = {
for {
companyOpt: Option[Company] <- getCompany(companyName)
company: Company = companyOpt.getOrElse(Company("error", List()))
if company.employees map(_.id) contains employeeId
detailsOpt: Option[EmployeeWithDetails] <- getDetails(employeeId)
} yield detailsOpt map (_.age)
}
There are a lot of issues in this code snippet - First of all, we had to add error-case code for the case where the getCompany
function returns Future.successful(None)
. Secondly, we introduced a dummy Company
in case the first function returns None
. This dummy is introduced so that our if-guard
isn’t only computed when the first Option
is Some
. This would be a dramatic change in the semantics of our application as we would be getting an employee’s age even if he didn’t exist in the company (the company was returned as None
). By adding the dummy company, we now force the if-guard
to return false. However, we are forcing the Future
monad to fail when it didn’t! Software that uses this function will now have a hard time distinguishing cases where the companyName
didn’t exist from cases where the Future
really failed.
What a nightmare!
Monad Transformers
It turns out that this a classical problem in functional programming when composing monads. The answer to it is a design construct called Monad Transformer. Summing it up - as there are also multiple posts that cover monad transformers in depth - it allows you to compose functions that return two or more monads. Unfortunately, Scala doesn’t come with monad transformers in its standard library. However, there are two functional programming libraries that provide them in Scala: scalaz and cats. I will be using cats in this post as I found their documentation more detailed than the one provided by scalaz. Now we can change the previous code and start using monad transformers, in particular OptionT
, to refactor the code:
import cats.data.OptionT
import cats.std.future._
def getEmployeeAge(employeeId: String, companyName: String): Future[Option[Int]] = {
(for {
company <- OptionT(getCompany(companyName))
if company.employees map(_.id) contains employeeId
details <- OptionT(getDetails(employeeId))
} yield details.age).value
}
As you can see, this snippet is nearly equal to the one provided in the first example! The only code introduced here is that we’re now calling the apply function from the OptionT
monad. The “magic” being done here is that after applying OptionT(getCompany(companyName))
we now get a Company
. Not a Future
, not an Option
, a Company
, just like the first example! Scala’s for-comprehension
automatically calls the flatMap
function for the OptionT
monad, which is applying the Future
monad followed by the Option
monad flatMap
functions.
With OptionT
, if the Future
returns Failure
or the Option
returns None
, the function will immediatelly return with that value. Other than the OptionT
apply function, only the .value
function is called. This function transforms the OptionT
monad back into a Future[Option[Company]]
. By using OptionT
we have removed our biggest issue in the previous example where we were changing the semantics of the application when composing functions that used two monads. You can get more details about the implementation of OptionT
and other monad transformers by going to the cats documentation provided at the end of this post.
More use cases for monad transformation
Monad transformers aren’t only used when composing functions that return two monads in the form M[F[T]]
where M
and F
are distinct monads like in our previous example of Future[Option[Company]]
. Suppose the API was changed into the following two functions:
trait HybridDBOps {
protected def getDetails(employeeId: String): Future[EmployeeWithDetails]
protected def getCompany(companyName: String): Option[Company]
}
This is also a common use case for monad transformers - composing functions that return different monads. Let’s implement the getEmployeeAge
function using OptionT
:
def getEmployeeAge(employeeId: String, companyName: String): Future[Option[Int]] = {
(for {
company <- OptionT.fromOption(getCompany(companyName))
if company.employees map(_.id) contains employeeId
details <- OptionT.liftF(getDetails(employeeId))
} yield details.age).value
}
The only changes here when compared to the previous example is that we’re now using the OptionT.fromOption
function in the first case, and the OptionT.liftF
function in the second one. The fromOption
function creates an OptionT
from an Option
monad. It is internally wrapping the return of the getCompany
function in a Future.successful
call. The liftF
function lifts any monad F
into an OptionT
. Internally, it is calling the map
function from the Future
monad and wrapping the returned EmployeeWithDetails
in a Some
. It is important to note that this is just an example and there are more monad transformers like EitherT
, ListT
, etc, in both cats and scalaz.
Conclusion
I hope you enjoyed this post and feel like getting started with function composition using multiple monads. The code used for this post is available at E.Near’s Monad Transformers.
Detailed explanations about monads and monad transformation can be found at: