1

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
      }
    }
Alexandr
  • 9,213
  • 12
  • 62
  • 102

2 Answers2

1

Maybe the code is self-explanatory here:

/** Edited for the new question. */
import cats.data.Validated
import cats.instances.list._ // for Semigroup
import cats.syntax.apply._ // for tupled
import cats.syntax.either._ // for toValidated

type FailFast[A] = Either[List[String], A]
type FailSlow[A] = Validated[List[String], A]
type Date = ???
type SimpleDateFormat = ???

def readDate(date: String)
            (map: Map[String, String])
            (format: SimpleDateFormat): FailFast[Date] = ???

def oneDateAfterAnotherFailSlow(dateBefore: String, dateAfter: String)
                       (map: Map[String, String])
                       (format: SimpleDateFormat): FailSlow[(Date, Date)] =
  (
    readDate(dateBefore)(map)(format).toValidated,
    readDate(dateAfter)(map)(format).toValidated
  ).tupled.ensure(List(s"$dateAfter date cannot be before $dateBefore"))(t => t._1.before(t._2))

The thing with Applicatives is that you should not (and if working with the abastraction can not) use flatMap since that will have sequential semantics (In this case FailFast behavior).
Thus, you need to use the abstractions that they provide, usually mapN to call a function with all the arguments if all of they are valid or tupled to create a tuple.

Edit

As the documentation states andThen should be used where you want your Validated to work as a Monad without being one.
It is there just by convenience, but you should not used it if you want the FailSlow semantics.

"This function is similar to flatMap on Either. It's not called flatMap, because by Cats convention, flatMap is a monadic bind that is consistent with ap. This method is not consistent with ap (or other Apply-based methods), because it has "fail-fast" behavior as opposed to accumulating validation failures".

  • oneDateAfterAnother is very simple. It does not validate dates. In your code you actually validate them. But it gets 2 dates, already parsed and checks that one date comes after another. It is also a primitive function. Now I need to combine the results of 2 readDate with oneDateAfterAnother using function composition, without assignment. tupled does not allow to use results, I can combine only functions like readDate. mapN rquires a function, that return the result without context. I will update my initial question to be more clear. – Alexandr Mar 11 '19 at 15:30
1

I could compose it finally with the following code:

  import cats.syntax.either._
  import cats.instances.list._ // for Semigroupal
  def oneDateAfterAnotherFailSlow(dateBefore:String, dateAfter:String)
                                 (map: Map[String, String])(format: SimpleDateFormat)
  : FailFast[Tuple2[Date, Date]] =
    for {
      t <-Semigroupal[FailSlow].product(
          readDate(dateBefore)(map)(format).toValidated,
          readDate(dateAfter)(map)(format).toValidated
        ).toEither
      r <- oneAfterAnother(t._1, t._2)
    } yield r

The idea, is that first validations for strings are applied, to make sure dates are correct. They are accumulated with Validated(FailSlow). Then fail-fast is used, cause if any of the dates is wrong and cannot be parsed, it makes no sense to continue and to compare them as dates.

It passed through my test cases.

If you can offer another, more elegant solution, always welcome!

Alexandr
  • 9,213
  • 12
  • 62
  • 102