Monad transformers stacks

In the meantime, we'll entertain ourselves with the following ideas: 

  • Monad transformers require an instance of a monad for the outer layer
  • A monad transformer itself has a monad
  • Will something bad happen if we use a monad transformer as an instance of a monad for another monad transformer?

Let's try it out. We've already implemented two monad transformers so let's bring them together. To start, we'll define the type of stack. It will be EitherT wrapped in OptionT. This will give us an unwrapped type of the following code:

Future[Either[String, Option[Fish]]]

This can be interpreted as an operation which takes time and might return an error in the case of nontechnical failure and needs to have an explanation (technical failures are denoted by failed Futures). An Option represents an operation which can return no result in a natural way that requires no further explanation.

With type aliases, we can represent the type of the inner transformer, fixing String as the type of the left side, as follows:

type Inner[A] = EitherT[Future, String, A]

The outer transformer in the stack is even simpler. In contrast to the inner type, where we fixed the type of effect to be Future, it takes a type constructor for an effect as the type parameter, as follows:

type Outer[F[_], A] = OptionT[F, A]

We can now use these aliases to define the whole stack as follows: 

type Stack[A] = Outer[Inner, A]

To make the situation realistic, we'll just take the last version of our original fishing functions—the one with castLineImpl returns Either[String, Line]. We need to decorate all original functions so that the result type matches the type of the stack we now have. This is where it starts to become unwieldy. The compiler is not allowed to apply two implicit conversions in a row, so therefore we have to apply one of them by hand. For the two functions returning Future[?], we also need to envelop the bottom layer into the Option:

override val buyBait: String => Stack[Bait] =
(name: String) => new EitherT(buyBaitImpl(name).map(l => Right(Option(l)): Either[String, Option[Bait]]))

override val hookFish: Line => Stack[Fish] =
(line: Line) => new EitherT(hookFishImpl(line).map(l => Right(Option(l)): Either[String, Option[Fish]]))

Now the compiler will be able to apply implicit conversion to the OptionT.

Likewise, the function returning Either[String, Line] needs to be converted to EitherT on the outer side as follows:

override val castLine: Bait => Stack[Line] =
(bait: Bait) => new EitherT(Future.successful(castLineImpl(bait).map(Option.apply)))

Internally, we have to map the contents of Either into an Option and apply Future to the whole result.

The compiler can help us to create an input of the proper type by applying implicit conversions as required—we won't see a lot of changes on this side, as follows:

val input = optionTunit[Inner, String]("Crankbait")

A small tweak is needed at the moment as we're calling our business logic with this transformer stack—now we have two layers of transformation, so we need to call value two times to extract the result, as follows:

val outerResult: Inner[Option[Fish]] = goFishing(input).value
val innerResult: Future[Either[String, Option[Fish]]] = outerResult.value

It can become tedious quite quickly to turn to the value method repeatedly on each of the monad transformers that constitute the stack. Why do we need to? Because returning the result with the type of specific transformer can pollute the client's code quite quickly. Hence, there are usually a couple of suggestions related to the monad transformers and monad transformer stacks worth considering, as follows:

  • Stacking monads and especially monad transformers adds performance and garbage collection overhead. It is essential to carefully consider the necessity of adding every additional layer of effects to the existing type.
  • It is also arguable that more layers in the stack add mental overhead and clutter the code. The approach is the same as with the first suggestion—don't do this unless absolutely needed.
  • Clients usually do not operate in terms of monad transformers, therefore they (transformers) should be considered to be an implementation detail. The API should be defined in generic terms. If it needs to be specific, prefer effect types over transformer types. In our example, it is better to return the result of the type Future[Option[?]] compared to OptionT[Future, ?].

Given all these considerations, are monad transformers really useful in real life? Surely they are! Nevertheless, as always there are alternatives, for example the free monad.

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

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