组合Scalaz验证

14

我希望使用Scalaz进行验证,并希望能够在不同的上下文中重用验证函数。顺便说一句,我对Scalaz完全不熟悉。

假设我有以下简单的检查:

def checkDefined(xs: Option[String]): Validation[String, String] =
  xs.map(_.success).getOrElse("empty".fail)

def nonEmpty(str: String): Validation[String, String] =
  if (str.nonEmpty) str.success else "empty".fail

def int(str: String): Validation[String, Int] = ...

我希望能够将一个验证的输出作为另一个验证的输入。我可以使用flatMap或for循环来完成,但感觉应该有更好的方法。

for {
  v1 <- checkDefined(map.get("foo"))
  v2 <- nonEmpty(v1)
  v3 <- int(v2)
  v4 <- ...
} yield SomeCaseClass(v3, v4)
或者
val x1 = checkDefined(map get "foo").flatMap(nonEmpty).flatMap(int)
val x2 = check(...)

// How to combine x1 and x2?

Scalaz专家们有什么想法吗?


1
“(x1 |@| x2){(x1,x2) => ... }”是什么意思呢?不过我对其确切的语法并不太确定...请参考http://www.casualmiracles.com/2012/01/16/a-small-example-of-applicative-functors-with-scalaz/。 - Jan
5个回答

17

除了@oxbow_lakes提出的解决方案外,您还可以使用Kleisli组合。

scala> import scalaz._, Scalaz._
import scalaz._
import Scalaz._

scala> def f: Int => Validation[String, Int] = i => if(i % 2 == 0) Success(i * 2) else    Failure("Odd!")
f: Int => scalaz.Validation[String,Int]

scala> def g: Int => Validation[String, Int] = i => if(i > 0) Success(i + 1) else Failure("Not positive!")
g: Int => scalaz.Validation[String,Int]

scala> type Va[+A] = Validation[String, A]
defined type alias Va

scala> import Validation.Monad._
import Validation.Monad._

scala> kleisli[Va, Int, Int](f) >=> kleisli[Va, Int, Int](g)
res0: scalaz.Kleisli[Va,Int,Int] = scalaz.Kleislis$$anon$1@4fae3fa6

scala> res0(11)
res1: Va[Int] = Failure(Odd!)

scala> res0(-4)
res2: Va[Int] = Failure(Not positive!)

scala> res0(4)
res3: Va[Int] = Success(9)

一个类型为A => M[B]的函数,其中M:Monad被称为Kleisli箭头。

您可以使用>=>运算符将两个Kleisli箭头A => M[B]B => M[C]组合成一个箭头A => M[C]。这被称为Kleisli组合。

表达式kleisli(f) >=> kleisli(g) >=> kleisli(h)等同于x => for(a <- f(x); b <- g(a); c <- h(b)) yield c,去掉了不必要的局部绑定。


2
哦,部分应用类型构造函数推断! - oxbow_lakes
1
正如@oxbow_lakes所指出的,有一种短路方法和一种累加方法。这个例子是短路方法。如果你想累加失败怎么做呢? - Travis Stevens
认真点。既然你能这么好地回答失败快速的情况,难道你就不能读懂问题吗? - ms-ati
1
@missingfaktor,你的示例在scalaz 7中不再适用。Validation.Monad对象已经不存在了。有什么办法可以让kleisli箭头组合与scalaz7的Validations一起工作吗? - Grega Kešpret
@Grega,自从我上次使用Scalaz以来,它已经发生了很多变化。我一直没有跟上时代的步伐。恐怕你只能自己找答案了。 :-( - missingfaktor
显示剩余2条评论

14

你可能想看一下三家夜总会的故事,它描述了使用以下内容进行验证组合:

  1. 单子(即flatMap
  2. 两种方法的应用函子(使用|@|traverse

基本上规则如下:通过单子进行组合是快速失败的。也就是说,您的计算将在此处短路并解析为Failure(e)。使用应用函子意味着您可以累积失败(例如用于Web表单验证)- 通过使用一个collection(作为失败类型的Semigroup) - 规范示例使用NonEmptyList

还有其他有用的Validation内容:

val1 <+> val2    //Acts like an `orElse`
val1 >>*<< val2  //Accumulates both successes and failures
在你所举的例子中,为什么你认为“一定有更好的方法”而不是使用for-comprehension呢?虽然可以稍微改进一下:
def checkDefined(xs: Option[String]) = xs.toSuccess("empty :-(")

在这种情况下,它并不真的值得一个整个的方法:

for {
  v1 <- map get "foo" toSuccess "Empty :-("
  v2 <- some(v1) filterNot (_.isEmpty) toSuccess "Empty :-("
  v3 <- (v2.parseInt.fail map (_.getMessage)).validation 
  v4 <- ...
} yield SomeCaseClass(v3, v4)

实际上,我现在有一个打开的选项卡中保存了您的要点。这是一个非常好的例子。我正在努力解决的问题是我希望我的检查能够组合。也就是说,一个检查的输出应该成为下一个检查的输入。就像您的示例中,当您将检查函数放在列表中时,它们都会对相同的人员实例进行检查,这与我尝试做的不同。 - chrsan
你正在描述flatMap; 也就是说,你已经知道答案了! - oxbow_lakes
非常感谢!在这种情况下,它不需要整个方法,但我需要在不同的上下文中验证相同的事情。即一个id本身和一个具有id和其他一些字段的实体。通过“更好的方式”,我指的是像您上面描述的那些酷炫的短方法,对于我们想要更加功能化的命令式背景来说并不那么明显。 - chrsan
哈哈,我也有命令式编程背景。我现在已经使用“验证”约18个月了,但是只有几个月前我才“发现”了<+>。 - oxbow_lakes
当我与您通话时...最好的方法是什么,可以在现有验证的基础上进行附加并只更改失败。像上面的示例中所做的那样,这是正确的方法吗 checkLong(x).map(Some(_)).fail.map(_ => "Ooops").validation - chrsan
我认为另一个选择是将Validation视为Bifunctor并使用f <-: v - oxbow_lakes

0

表达式

for {
  v1 <- checkDefined(map.get("foo"))
  v2 <- nonEmpty(v1)
  v3 <- int(v2)
  v4 <- someComputation()
} yield SomeCaseClass(v3, v4)

可以这样替换

(checkDefined(map.get("foo")).liftFailNel |@| nonEmpty(v1)) {(v1, v2) =
    SomeCaseClass(int(v2), someComputation)
}

结果将会是:

 Validtion[NonEmptyList[String], SomeCaseClass] which is equal to ValidationNEL[String, SomeCaseClass]

如果两个验证都失败,NonEmptyList 将包含它们两个。

1
额,它做不到。第一个验证成功的结果需要作为第二个的输入,因此,applicative functors 对你来说并没有帮助。 - oxbow_lakes

0
我最近编写了一个简单的“框架”,用于声明性验证,这些验证是可组合的。我最初基于@missingfaktor的答案实现了它,但在他的基础上,我添加了一个DSL,使用Shapeless的Generic来处理要验证的任意大小的输入元组,并将其馈入匹配度函数中。
它的使用方法如下:
def nonEmpty[A] = (msg: String) => Vali { a: Option[A] =>
  a.toSuccess(msg)
}

def validIso2CountryCode = (msg: String) => Vali { x: String =>
  IsoCountryCodes2to3.get(x).toSuccess(msg)
}

val postal = "12345".some
val country = "GB".some

val params = (
  postal
     |> nonEmpty[String]("postal required"),
  country
     |> nonEmpty[String]("country required")
    >=> validIso2CountryCode("country must be valid")
)

// parameter type inference doesn't work here due to the generic type level nature of the implementation; any improvements are welcome!
validate(params) { (postal: String, country: String) =>
  println(s"postal: $postal, country: $country")
}

实现可以在https://gist.github.com/eallik/eea6b21f8e5154e0c97e找到。


0
除了missingfaktor的答案之外,还可以注意到scalaz 7没有为Validation提供Monad,因为其行为与Apply实例不匹配。因此,可以定义Validation的Bind,并提供方便的转换器:
import scalaz.{Bind, Kleisli, Validation, Success, Failure}

implicit def toKleisli[E, A, B](f: A => Validation[E, B]): Kleisli[Validation[E, ?], A, B] =
  Kleisli[Validation[E, ?], A, B](f)

implicit def fromKleisli[E, A, B](f: Kleisli[Validation[E, ?], A, B]): A => Validation[E, B] = f.run

implicit def validationBind[E] = new Bind[Validation[E, ?]] {

  def bind[A, B](fa: Validation[E, A])(f: (A) => Validation[E, B]): Validation[E, B] = {
    import Validation.FlatMap._
    fa.flatMap(f)
  }

  def map[A, B](fa: Validation[E, A])(f: (A) => B): Validation[E, B] = fa.map(f)
}

val parse: Option[String] => Validation[String, Int] = checkDefined _ >=> nonEmpty _ >=> int _

println(parse(None)) // Failure(empty)
println(parse(Some(""))) // Failure(empty)
println(parse(Some("abc"))) // Failure(java.lang.NumberFormatException: For input string: "abc")
println(parse(Some("42"))) // Success(42)

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