在Scalaz.Validation中,对HList进行Map和Reduce/Fold操作

7

我最开始的内容大致是这样的:

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) => ... }

现在我想减少样板代码以提高可读性,同时也不必向更初级的团队成员解释.toValidateNel|@|的含义。最初的想法是使用List,但这样最后一行将停止工作,我必须放弃一些静态安全性。因此,我转向了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(_ |@| _)

然而,我甚至无法通过.map(...)这一步。 我试过了#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 })

...毫无效果。

我在 #scalaz 上寻求帮助,但似乎没有人能够给出一个现成的答案。然而,我非常渴望学习如何解决这个问题,这对于实际和学习目的都非常有益。

P.S. 实际上,我的验证被封装在 Kleisli[Va, A, B] 中,以便我可以使用 >=> 组合单独的验证步骤,但是当到达 .map(...) 时,所有的 Kleisli 都将被“缩减”为 Validation[String, A],这似乎与该问题不相关。


1
你肯定需要将Poly1定义为一个对象(由于与稳定标识符有关的各种奇怪的Scala相关原因)。这看起来很像一个遍历,而shapeless-contrib的traverse可以让你在一步中执行toValidationNel和(道义上等价于)reduce(_ |@| _) - Travis Brown
1
请参阅我在此处发布的相关博客文章链接 - Travis Brown
1个回答

3
这是使用shapeless-contribtraverse所呈现的效果:
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")
)

然后:

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

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

现在,result 是一个 ValidationNel[String, (String, String)],你可以像使用可怕的 ApplicativeBuilder 一样使用它,并且做任何你想做的事情,这是通过缩小使用 |@| 得到的。

你能详细解释一下为什么和如何traverse与普通的map不同,为什么它能工作而map不能,并且为什么它(尚)不是Shapeless的一部分吗? - Erik Kaplun
2
@TravisBrown 为什么我们需要 .productElements.map(_.tupled)?难道 shapeless 的 Generic 对于元组的支持不允许我们直接遍历元组吗?@ErikAllik traverse 类似于一个累加 Applicative 效果的 map。如果我们使用普通的 map,我们会得到 (ValidationNel[String, String],ValidationNel[String, String])traverse 还会"序列化" ValidationNel 效果,这样我们就可以针对整个元组获得单个 ValidationNeltraverse 依赖于 scalaz,因此它不能成为主要 Shapeless 的一部分,因为他们不想引入这种依赖关系。 - lmm
Travis:如果有任何指针可以解释为什么ApplicativeBuilder很糟糕(例如与普通的ValidationNel相比),那将不胜感激! - Erik Kaplun
1
@Erik:ApplicativeBuilder的问题在于它只是一种语法技巧,没有实际意义。它本质上是一个参数列表,等待一个可以提升到applicative functor并应用于自身的函数。 - Travis Brown
那么,这是因为语法黑客总体上都不好,还是只是这个黑客特别不好?或者只是不必要的? - Erik Kaplun
我猜“可怕”这个词有点过了,但与 Haskell 相比,你只需编写 foo <$> a <*> b <*> c(函数和参数按通常顺序排列,没有中间的 Builder 等),就可以完成相同的操作。 - Travis Brown

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接