In Scala Cats Validated, how can combine ordered validations

3.1k views Asked by At

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#andThenoperation. 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.

2

There are 2 answers

3
Oleg Pyzhcov On BEST ANSWER

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)
0
Peter Neyens On

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)))