Stacking monads Writer and OptionT

294 Views Asked by At

I have the following code:

override def getStandsByUser(email: String): Try[Seq[Stand]] =
  (for {
    user <- OptionT(userService.findOneByEmail(email)): Try[Option[User]]
    stands <- OptionT.liftF(standService.list()):[Try[List[Stand]]]
    filtered = stands.filter(stand => user.stands.contains(stand.id))
  } yield filtered).getOrElse(Seq())
}

I want to add logging on each stage of the processing - so I need to introduce writer monad and stack it with monad transformer OptionT. Could you please suggest how to do that?

2

There are 2 best solutions below

0
On BEST ANSWER

The best way to do this is to convert your service calls into using cats-mtl.

For representing Try or Option you can use MonadError and for logging you can use FunctorTell. Now I don't know what exactly you're doing inside your userService or standService, but I wrote some code to demonstrate what the result might look like:

type Log = List[String]

//inside UserService
def findOneByEmail[F[_]](email: String)
  (implicit F: MonadError[F, Error], W: FunctorTell[F, Log]): F[User] = ???

//inside StandService
def list[F[_]]()
  (implicit F: MonadError[F, Error], W: FunctorTell[F, Log]): F[List[Stand]] = ???

def getStandsByUser[F[_]](email: String)
(implicit F: MonadError[F, Error], W: FunctorTell[F, Log]): F[List[Stand]] =
  for {
    user <- userService.findOneByEmail(email)
    stands <- standService.list()
  } yield stands.filter(stand => user.stands.contains(stand.id))


//here we actually run the function
val result =
  getStandsByUser[WriterT[OptionT[Try, ?], Log, ?] // yields WriterT[OptionT[Try, ?], Log, List[Stand]]
    .run  // yields OptionT[Try, (Log, List[Stand])]
    .value // yields Try[Option[(Log, List[Stand])]]

This way we can avoid all of the calls to liftF and easily compose our different services even if they will use different monad transformers at runtime.

0
On

If you take a look at the definition of cats.data.Writer you will see that it is an alias to cats.data.WriterT with the effect fixed to Id.

What you want to do is use WriterT directly and instead of Id use OptionT[Try, YourType].

Here is a small code example of how that can be achieved:

object Example {

  import cats.data._
  import cats.implicits._

  type MyType[A] = OptionT[Try, A]

  def myFunction: MyType[Int] = OptionT(Try(Option(1)))

  def main(args: Array[String]): Unit = {
    val tmp: WriterT[MyType, List[String], Int] = for {
      _ <- WriterT.tell[MyType, List[String]](List("Before first invocation"))
      i <- WriterT.liftF[MyType, List[String], Int](myFunction)
      _ <- WriterT.tell[MyType, List[String]](List("After second invocation"))
      j <- WriterT.liftF[MyType, List[String], Int](myFunction)
      _ <- WriterT.tell[MyType, List[String]](List(s"Result is ${i + j}"))
    } yield i + j

    val result: Try[Option[(List[String], Int)]] = tmp.run.value
    println(result)
    // Success(Some((List(Before first invocation, After second invocation, Result is 2),2)))
  }

}

The type annotations make this a bit ugly, but depending on your use case you might be able to get rid of them. As you can see myFunction returns a result of type OptionT[Try, Int] and WriterT.lift will push that into a writer object that also has a List[String] for your logs.