Search code examples
scalamonadscompositionmonad-transformersscala-cats

Compose ReaderT with Either through for-comprehension and applicative pure


Here are functions that return ReaderT and Either as a return type:

  import cats.data.{ReaderT}
  type FailFast[A] = Either[List[String], A]

  def getValue(name: String):ReaderT[FailFast, Map[String, String], String] =
    ReaderT((map) => map.get(name)
      .toRight(List(s"$name field not specified")))

  def nonNullable(name: String)(data: String): FailFast[String] =
    Right(data).ensure(List(s"$name cannot be nullable"))(_ != null)

  def nonBlank(name: String)(data: String): FailFast[String] =
    Right(data).ensure(List(s"$name cannot be blank"))(_.nonEmpty)

Here is a composition of these functions that works fine:

  def readNotEmptyValue(name: String): ReaderT[FailFast, Map[String, String], String] =
    for {
      value <- getValue(name)
      _ <- ReaderT((_:Map[String, String]) => nonNullable(name)(value))
      res <- ReaderT((_:Map[String, String]) => nonBlank(name)(value))
    } yield res

I want to get rid of this ReaderT.apply invocation, and write something via applicative pure:

  type Q[A] = ReaderT[FailFast, Map[String, String], A]
  import cats.syntax.applicative._
  import cats.instances.either._
  def readNotEmptyValue(name: String): ReaderT[FailFast, Map[String, String], String] =
    for {
      value <- getValue(name)
      _ <- nonBlank(name)(value).pure[Q]
      res <- nonBlank(name)(value).pure[Q]
    } yield res.right.get

Unfortunately the last solution does not work with negative cases. I can for sure use match to check, weather it is Right or Left.

But is there a way to compose it with pure and minimize manual effort. How to do it correctly?


Solution

  • Instead of Applicative.pure, you can use EitherOps.liftTo to remove the verboseness of ReaderT.apply:

    def readNotEmptyValue(name: String): ReaderT[FailFast, Map[String, String], String] = 
      for {
        value <- getValue(name)
        _ <- nonBlank(name)(value).liftTo[Q]
        res <- nonBlank(name)(value).liftTo[Q]
      } yield res
    

    Otherwise, you're still dealing with an instance of FailFast[String] and not String, and there's no way around it if you want to attempt and extract the value out of FailFast.