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?
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
.