Monad transformers

Let's hold on for a second and recap what we just did.

We made a small sacrifice and increased the complexity of the return type of our original functions to some "common denominator" type. This sacrifice is rather small because in our example, as well as in real life, this is usually done by just lifting the original functions into their proper context.

The signatures we came up with look a little awkward, but this is partly because we started to develop them as concrete implementations. In fact, the user-facing API of our fishing component should be similar to the following snippet straight from the beginning, if implemented in a more abstract way:

abstract class FishingApi[F[_]: Monad] {

val buyBait: String => F[Bait]
val castLine: Bait => F[Line]
val hookFish: Line => F[Fish]

def goFishing(bestBaitForFish: F[String]): F[Fish] = for {
name <- bestBaitForFish
bait <- buyBait(name)
line <- castLine(bait)
fish <- hookFish(line)
} yield fish
}

This approach abstracts over the type of effect, giving more flexibility to us as library authors and more structure to the user of our API.

This API can be used with any effect with a monad. This is an example of how it can be implemented utilizing functions we currently have—returning mixed Future and Optional results:

import Transformers.OptionTMonad
import ch09.Monad.futureMonad
import scala.concurrent.ExecutionContext.Implicits.global

// we need to fix the types first to be able to implement concrete fucntions
object Ch10 {
type Bait = String
type Line = String
type Fish = String
}

object Ch10FutureFishing extends FishingApi[OptionT[Future, ?]] with App {

val buyBaitImpl: String => Future[Bait] = Future.successful
val castLineImpl: Bait => Option[Line] = Option.apply
val hookFishImpl: Line => Future[Fish] = Future.successful

override val buyBait: String => OptionT[Future, Bait] =
(name: String) => buyBaitImpl(name).map(Option.apply)
override val castLine: Bait => OptionT[Future, Line] =
castLineImpl.andThen(Future.successful(_))
override val hookFish: Line => OptionT[Future, Fish] =
(line: Line) => hookFishImpl(line).map(Option.apply)

goFishing(Transformers.optionTunit[Future, String]("Crankbait"))
}

 Exactly as before, we implemented facades for our original functions, doing nothing more than routine lifting of them into the appropriate effect. And the goFishing method can be used as is—the compiler takes only a monad for the OptoinT[Future] available to make it happen.

For instance, at some point the implementor of the underlying functions can decide that they should return Try instead of the future now. This is OK because requirements change and we can incorporate this change in our logic quite easily:

import scala.util._
object
Ch10OptionTTryFishing extends FishingApi[OptionT[Try, ?]] with App {

val buyBaitImpl: String => Try[Bait] = Success.apply
val castLineImpl: Bait => Option[Line] = Option.apply
val hookFishImpl: Line => Try[Fish] = Success.apply

override val buyBait: String => OptionT[Try, Bait] =
(name: String) => buyBaitImpl(name).map(Option.apply)
override val castLine: Bait => OptionT[Try, Line] =
castLineImpl.andThen(Try.apply(_))
override val hookFish: Line => OptionT[Try, Fish] =
(line: Line) => hookFishImpl(line).map(Option.apply)

goFishingM(Transformers.optionTunit[Try, String]("Crankbait"))

}

Assuming that the change in the library is given, the only things we need to alter on our side are:

  • The lifting approach for the castLine function; it changes from Future.success to Try.apply
  • The type parameter we're passing over for the wrapper for the initial argument of the goFishing function

And we're done. We don't need to touch our fishing "business" logic at all!

The monad transformer in a sense "flattens" both monads, such that it is possible to cut through all layers at once when calling the map and flatMap methods—and thus also in for-comprehension.

Currently, it is not possible to change the type of the "inner" effect though— we only have an OptionT monad transformer available. But this is just a matter of implementing another transformer once, entirely like we did with monads. To be more specific, let's see the effect of altering the return type of the basic functions to Either instead of Option. Supposing it is expected that the new version uses String as a description of the unhappy case; we would have the following code:

object Ch10EitherTFutureFishing extends FishingApi[EitherT[Future, String, ?]] with App {

val buyBaitImpl: String => Future[Bait] = Future.successful
val castLineImpl: Bait => Either[String, Line] = Right.apply
val hookFishImpl: Line => Future[Fish] = Future.successful

override val buyBait: String => EitherT[Future, String, Bait] =
(name: String) => buyBaitImpl(name).map(l => Right(l): Either[String, Bait])
override val castLine: Bait => EitherT[Future, String, Line] =
castLineImpl.andThen(Future.successful(_))
override val hookFish: Line => EitherT[Future, String, Fish] =
(line: Line) => hookFishImpl(line).map(l => Right(l): Either[String, Fish])

goFishing(Transformers.eitherTunit[Future, String, String]("Crankbait")).value

}

The return type of castLineImpl is now Either[String, Line] as new requirements dictate. The lifting we are doing is slightly convoluted, just because we need to convey the types of both the left and right side of Either to the compiler. The rest of the implementation is the same as before.

And it relies on the fact that we have an instance of EitherT and a corresponding monad available. We already know how to implement monad transformers and can come up with the code in no time. First, the EitherT class, which resembles an OptionT almost identically, with respect to the need to carry the type of the left side of Either around as follows:

implicit class EitherT[F[_]: Monad, L, A](val value: F[Either[L, A]]) {
def compose[B](f: A => EitherT[F, L, B]): EitherT[F, L, B] = {
val result: F[Either[L, B]] = value.flatMap {
case Left(l) => Monad[F].unit(Left[L, B](l))
case Right(a) => f(a).value
}
new EitherT(result)
}
def isRight: F[Boolean] = Monad[F].map(value)(_.isRight)
}

Instead of pattern matching on None and Some, we pattern-match on the Left and Right sides of Either. We also replace the helper method isEmpty with the more suitable isRight.

The lifting function and the implementation of the monad are also considerably similar—just boilerplate, if you will:

def eitherTunit[F[_]: Monad, L, A](a: => A) = new EitherT[F, L, A](Monad[F].unit(Right(a)))

implicit def EitherTMonad[F[_] : Monad, L]: Monad[EitherT[F, L, ?]] =
new Monad[EitherT[F, L, ?]] {
override def unit[A](a: => A): EitherT[F, L, A] =
Monad[F].unit(ch09.Monad.eitherMonad[L].unit(a))
override def flatMap[A, B](a: EitherT[F, L, A])(f: A => EitherT[F, L, B]): EitherT[F, L, B] =
a.compose(f)
}

Incredible! We now have two monad transformers in our toolbox and the previously broken definition of Ch10EitherTFutureFishing has started to compile and run!

Eager to implement TryT to cement this newly gained knowledge? We're happy to leave this as an exercise for you.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.71.28