Map and reduce/fold over HList of scalaz.Validatio

2020-06-04 04:18发布

问题:

I started out with something like this:

def nonEmpty[A] = (msg: String) => (a: Option[A]) => a.toSuccess(msg)

val postal: Option[String] = request.param("postal")
val country: Option[String] = request.param("country")

val params =
  (postal  |> nonEmpty[String]("no postal" )).toValidationNel |@|
  (country |> nonEmpty[String]("no country")).toValidationNel

params { (postal, country) => ... }

Now I thought it would be nice to reduce the boilerplate for better readability and for not having to explain to more junior team members what .toValidateNel and |@| mean. The first thought was List but then the last line would stop working and I'd have to give up some static safety. So I looked towards Shapeless:

import shapeless._; import poly._; import syntax.std.tuple._

val params = (
  postal  |> nonEmpty[String]("no postal"),
  country |> nonEmpty[String]("no country")
)

params.map(_.toValidatioNel).reduce(_ |@| _)

however, I can't even seem to get past the .map(...) bit. I've tried as per a suggestion on #scalaz:

type Va[+A] = Validation[String, A]
type VaNel[+A] = ValidationNel[String, A]

params.map(new (Va ~> VaNel) { def apply[T](x: Va[T]) = x.toValidationNel })

...to no avail.

I've asked for help on #scalaz but it doesn't seem something people just have an out of the box answer to. However, I'm really keen on learning how to solve this both for practical as well as learning purposes.

P.S. in reality my validations are wrapped inside Kleisli[Va, A, B] so that I could compose individual validation steps using >=> but that seems to be orthogonal to the issue as by the time that .map(...) is reached, all Kleislis will have been "reduced" to Validation[String, A].

回答1:

Here's what this would look like with shapeless-contrib's traverse:

import scalaz._, Scalaz._
import shapeless._, contrib.scalaz._, syntax.std.tuple._

def nonEmpty[A] = (msg: String) => (a: Option[A]) => a.toSuccess(msg)

val postal: Option[String] = Some("00000")
val country: Option[String] = Some("us")

val params = (
  postal  |> nonEmpty[String]("no postal"),
  country |> nonEmpty[String]("no country")
)

And then:

object ToVNS extends Poly1 {
  implicit def validation[T] = at[Validation[String, T]](_.toValidationNel)
}

val result = traverse(params.productElements)(ToVNS).map(_.tupled)

Now result is a ValidationNel[String, (String, String)], and you can do anything with it that you could do with the awful ApplicativeBuilder thing you'd get from reducing with |@|.