Scalaz中如何对单个对象执行多验证且仅累积错误?
解决Scalaz Validation仅累积错误的需求
这个问题我之前也遇到过,核心就是要只累积错误,忽略成功结果的合并——毕竟你的场景里成功结果都是原对象A,不需要把多个Success(A)合并成什么,只要所有验证都通过就返回原A,有错误就把所有错误攒起来对吧?
思路拆解
你现在有List[A => ValidationNel[Err, A]],要把每个函数应用到同一个A上,最终得到ValidationNel[Err, A]。关键在于:
- 错误部分:需要把所有
Failure里的Nel[Err]合并成一个大的Nel[Err] - 成功部分:只要所有验证都成功,直接返回原A就行,不需要处理多个
Success(A)的合并
实现方案
这里有两种比较优雅的实现方式,都利用了Scalaz的现有工具:
方案1:映射成功结果为Unit后合并
我们可以把每个验证的成功结果转换成Unit(因为成功时我们只关心“没有错误”这个事实),然后利用ValidationNel的错误Semigroup来累积错误,最后再把成功映射回原A:
import scalaz._ import Scalaz._ type Err = String type ValidationResult[A] = ValidationNel[Err, A] def validateAll[A](a: A, validators: List[A => ValidationResult[A]]): ValidationResult[A] = { // 把每个验证的成功结果转成Unit,只保留错误信息 val errorOnlyValidations = validators.map(f => f(a).map(_ => ())) // 合并所有错误:suml会自动累积Failure里的Nel,成功则合并为Success(()) val combinedErrors = errorOnlyValidations.suml combinedErrors match { case Success(_) => a.successNel // 全成功,返回原A case Failure(errs) => errs.failureNel[A] // 有错误,返回累积的错误 } }
方案2:直接折叠错误列表
另一种思路是把每个验证的错误提取出来,合并成一个列表,最后判断是否有错误:
def validateAll[A](a: A, validators: List[A => ValidationResult[A]]): ValidationResult[A] = { // 折叠所有验证的错误,把错误收集成一个List[Err] val allErrors = validators.foldLeft(List.empty[Err]) { (acc, validator) => validator(a) match { case Failure(errs) => acc ++ errs.list case Success(_) => acc } } // 根据是否有错误返回对应的ValidationNel allErrors.toNel match { case Some(errs) => errs.failureNel[A] case None => a.successNel } }
为什么不用+++?
你提到的+++方法会同时合并成功和错误结果,它要求成功类型有对应的Semigroup——比如如果你的成功类型是List[T],+++会把多个成功的列表合并成一个。但你的场景里成功类型是单个T,除非用Id的Semigroup(也就是T自己的Semigroup),但这会把多个Success(A)合并成A的组合结果,这显然不是你想要的——你只需要原A,不需要合并多个成功结果。
示例演示
假设我们有一个User类,要验证用户名非空、年龄大于18:
case class User(name: String, age: Int) // 验证用户名非空 val validateName: User => ValidationResult[User] = u => if (u.name.nonEmpty) u.successNel else "用户名不能为空".failureNel[User] // 验证年龄大于18 val validateAge: User => ValidationResult[User] = u => if (u.age > 18) u.successNel else "年龄必须大于18岁".failureNel[User] // 测试验证通过的情况 val validUser = User("Alice", 20) validateAll(validUser, List(validateName, validateAge)) // 输出:Success(User(Alice,20)) // 测试验证失败的情况 val invalidUser = User("", 17) validateAll(invalidUser, List(validateName, validateAge)) // 输出:Failure(NonEmptyList(用户名不能为空, 年龄必须大于18岁))
内容的提问来源于stack exchange,提问作者allidoiswin
相关产品推荐
相关产品推荐

