我想使用Scalaz进行验证,并希望能够在不同的上下文中重用验证函数。我是Scalaz btw的新手。
假设我有这些简单的检查:
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 {
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专家的想法吗?
答案 0 :(得分: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箭头。
您可以使用A => M[B]
运算符组合两个Kleisli箭头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
,减去不必要的本地绑定。
答案 1 :(得分:13)
您可能希望查看使用以下内容描述验证合成的Tale of Three Nightclubs:
flatMap
)|@|
和traverse
)基本上规则相当于:通过monads组合快速失败。也就是说,此时您的计算将会短路并解析为Failure(e)
。使用applicative functors意味着您可以累积失败(可能用于Web表单验证) - 您使用collection
(Semigroup
)作为失败类型 - 可以使用{{ 1}}。
NonEmptyList
还有其他有用的东西:
Validation
在你的具体例子中,为什么你认为“必须有更好的方法”而不是通过理解来做到这一点?不过可以稍微改进一下:
val1 <+> val2 //Acts like an `orElse`
val1 >>*<< val2 //Accumulates both successes and failures
在这种情况下,它并不值得一个完整的方法:
def checkDefined(xs: Option[String]) = xs.toSuccess("empty :-(")
答案 2 :(得分: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将包含它们
答案 3 :(得分:0)
我最近编写了一个简单的框架&#34;用于可组合的声明性验证。我最初的实施基于@jissfaktor的答案,但是,除了他提出的问题之外,我还使用Shapeless's Generic
添加了DSL来处理要验证的任意大小输入的元组,它们被输入到匹配arity的函数中。
其用法如下:
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")
}
答案 4 :(得分:0)
除了missfaktor的回答之外,还可以注意到scalaz 7由于Monad
实例的行为不匹配而导致Validation
没有Apply
。因此,可以为Bind
定义Validation
,并为方便起见定义转换器:
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)