简体   繁体   中英

In Scala Cats Validated, how can combine ordered validations

In https://gist.github.com/satyagraha/897e427bfb5ed203e9d3054ac6705704 I have posted a Scala Cats validation scenario which seems reasonable, but I haven't found a very neat solution.

Essentially, there is a two-stage validation, where individual fields are validated, then a class constructor is called which may throw due to internal checks (in general this may not be under my control to change, hence the exception handling code). We wish to not to call the constructor if any field validation fails, but also combine any constructor failure into the final result. "Fail-fast" is definitely right here for the two-phase check.

This is a kind of flatMap problem, which the cats.data.Validated framework appears to handle via the cats.data.Validated#andThen operation. However I couldn't find a particularly neat solution to the problem as you can see in the code. There are quite a limited number of operations available on a cats.syntax.CartesianBuilder and is wasn't clear to me how to link it with the andThen operation.

Any ideas welcome! Note there is a Cats issue https://github.com/typelevel/cats/issues/1343 which possibly is related, not sure.

For fail fast, chained validation it is easier to use Either than Validated . You can easily switch from Either to Validated or vice versa depending if you want error accumulation.

A possible solution to your problem would be to create a smart constructor for User which returns an Either[Message, User] and use this with Validated[Message, (Name, Date)] .

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

def user(name: Name, date: Date): Either[Message, User] = 
  Either.catchNonFatal(User(name, date)).leftMap(Message.toMessage)

// error accumulation -> Validated
val valids: Validated[Message, (Name, Date)] = 
  (validateName(nameRepr) |@| validateDate(dateDepr)).tupled

// error short circuiting -> either
val userOrMessage: Either[Message, User] =
  valids.toEither.flatMap((user _).tupled)

// Either[Message,User] = Right(User(Name(joe),Date(now)))

I would make a helper second-order function to wrap the exception-throwing ones:

def attempt[A, B](f: A => B): A => Validated[Message, B] = a => tryNonFatal(f(a))

Also, default companions of case classes extend the FunctionN trait, so there's no need to do (User.apply _).tupled , it can be shortened to User.tupled (on custom companions, you need to write extends ((...) => ...)) but apply override will be autogenerated)

So we end up with that using andThen :

val valids = validateName(nameRepr) |@| validateDate(dateDepr)
val res: Validated[Message, User] = valids.tupled andThen attempt(User.tupled)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM