Here is an example from the Scala with Cats book:
object Ex {
import cats.data.Validated
type FormData = Map[String, String]
type FailFast[A] = Either[List[String], A]
def getValue(name: String)(data: FormData): FailFast[String] =
data.get(name).toRight(List(s"$name field not specified"))
type NumFmtExn = NumberFormatException
import cats.syntax.either._ // for catchOnly
def parseInt(name: String)(data: String): FailFast[Int] =
Either.catchOnly[NumFmtExn](data.toInt).leftMap(_ => List(s"$name must be an integer"))
def nonBlank(name: String)(data: String): FailFast[String] =
Right(data).ensure(List(s"$name cannot be blank"))(_.nonEmpty)
def nonNegative(name: String)(data: Int): FailFast[Int] =
Right(data).ensure(List(s"$name must be non-negative"))(_ >= 0)
def readName(data: FormData): FailFast[String] =
getValue("name")(data).
flatMap(nonBlank("name"))
def readAge(data: FormData): FailFast[Int] =
getValue("age")(data).
flatMap(nonBlank("age")).
flatMap(parseInt("age")).
flatMap(nonNegative("age"))
case class User(name: String, age: Int)
type FailSlow[A] = Validated[List[String], A]
import cats.instances.list._ // for Semigroupal
import cats.syntax.apply._ // for mapN
def readUser(data: FormData): FailSlow[User] =
(
readName(data).toValidated,
readAge(data).toValidated
).mapN(User.apply)
Some notes:
each primitive validation function: nonBlank
, nonNegative
, getValue
returns so called FailFast type, which is monadic, not applicative.
There are 2 functions readName
and readAge
, which use a composition of the previous ones, and also are FailFast by the nature.
The readUser
is on the contrary, fail slow. To achieve it results of readName
and readAge
are converted to Validated and composed through so called "Syntax"
Let's assume I have another function for validation, that accepts name and age, validated by readName
and readAge
. For intstance:
//fake implementation:
def validBoth(name:String, age:Int):FailSlow[User] =
Validated.valid[List[String], User](User(name,age))
How to compose validBoth
with readName
and readAge? With fail fast it is quite simple, cause I use for-comrehension
and have access to the results of readName
and readAge
:
for {
n <- readName...
i <- readAge...
t <- validBoth(n,i)
} yield t
but how to get the same result for failslow?
EDIT probably it is not clear enough, with these function. Here is a real use case. There is a function, similar to readName/readAge that validates date in the similar way. I want to create a validation fucntion, that accepts 2 dates, to make sure that one date comes after another. Date comes from String. Here is an example, how it will look like for FailFast, which is not the best option in this context:
def oneAfterAnother(dateBefore:Date, dateAfter:Date): FailFast[Tuple2[Date,Date]] =
Right((dateBefore, dateAfter))
.ensure(List(s"$dateAfter date cannot be before $dateBefore"))(t => t._1.before(t._2))
for {
dateBefore <- readDate...
dateAfter <- readDate...
t <- oneDateAfterAnother(dateBefore,dateAfter)
} yield t
My purpose is to accumulate possible errors with dates in applicative way. In the book it is said, p. 157:
We can’t flatMap because Validated isn’t a monad. However, Cats does provide a stand-in for flatMap called andThen . The type signature of andThen is identical to that of flatMap, but it has a different name because it is not a lawful implementation with respect to the monad laws:
32.valid.andThen { a =>
10.valid.map { b =>
a + b
}
}
Ok, I tried to reuse this solution, based on andThen
, but the result had monadic, but not applicative effect:
def oneDateAfterAnotherFailSlow(dateBefore:String, dateAfter:String)
(map: Map[String, String])(format: SimpleDateFormat)
: FailSlow[Tuple2[Date, Date]] =
readDate(dateBefore)(map)(format).toValidated.andThen { before =>
readDate(dateAfter)(map)(format).toValidated.andThen { after =>
oneAfterAnother(before,after).toValidated
}
}