在上一章节中我们介绍了Either的实现,在使用Either来校验输入的例子中我们提到了Either的一个缺陷,那就是Either只能收集一次错误信息,收集完就返回错误信息了, 这里存在的问题就是当我们需要多个输入项进行校验的时候,每次都只能返回一个错误信息。想想一个表单的校验,假如用户有许多的字段都是错误的,没有提交只能返回一个错误信息,那用户需要不断的提交试错,这样是非常不友好的。参照Either,我们来实现一种新的数据结构:
package org.fp.scala.datastruct
trait Validated[+E, +A] {
def map[B](f: A => B): Validated[E, B] = this match {
case Invalid(v) => Invalid(v)
case Valid(a) => Valid(f(a))
}
def flatMap[EE >: E, B](f: A => Validated[EE, B]): Validated[EE, B] = this match {
case Invalid(v) => Invalid(v)
case Valid(a) => f(a)
}
def orElse[EE >: E, B >: A](default: Validated[EE, B]): Validated[EE, B] = this match {
case Invalid(_) => default
case _ => this
}
def map2[EE >: E, B, C](vb: Validated[EE, B])(f: (A, B) => C): Validated[EE, C] =
(this, vb) match {
case (Invalid(v), Valid(_)) => Invalid(v)
case (Valid(_), Invalid(v)) => Invalid(v)
case (Invalid(v1), Invalid(v2)) => Invalid(v1 ++ v2)
case (Valid(a), Valid(b)) => Valid(f(a, b))
}
}
case class Invalid[+E](value: List[E]) extends Validated[E, Nothing]
case class Valid[+A](value: A) extends Validated[Nothing, A]
可以看到Validated的结构和Either结构基本是类似的,只是表示错误的时候用的是Invalid[+E](value: List[E]),他表明Invalid可以收集和传递无限个错误。再看一看其中基本方法的实现,其中其他的基本方法的实现都是类似的,只有map2方法不一样,新的map2方法再遇到两个Validated都是Invalid类型的时候,可以将Invalid中的错误信息组合起来。既然实现了map2我们完全可以使用map2来实现map3, 下面来看下代码:
def map3[EE >: E, B, C, D](vb: Validated[EE, B])(vc: Validated[EE, C])(f: (A, B, C) => D): Validated[EE, D] =
map2(vb)((_, _)).map2(vc) {
case ((a, b), c) => f(a, b, c)
}
我们再来看下traverse和sequence的实现:
object Validated {
def traverse[E, A, B](li: List[A])(f: A => Validated[E, B]): Validated[E, List[B]] =
li match {
case Nil => Valid(List())
case Cons(h, t) => f(h).map2(traverse(t)(f))(Cons(_, _))
}
def traverse1[E, A, B](li: List[A])(f: A => Validated[E, B]): Validated[E, List[B]] =
li.foldRight(Valid(List()): Validated[E, List[B]]) ((a, vlb) => f(a).map2(vlb)(Cons(_, _)))
def sequence[E, A](li: List[Validated[E, A]]): Validated[E, List[A]] =
traverse(li)(x => x)
}
可以看到Validated类型中traverse和sequence的实现是类似的,但是由于map2的实现不同,他产生的效果是完全不一样的, 我们再来看下之前关于校验的例子:
case class Student(firstName: Name, lastName: Name, age: Age)
case class Name(value: String)
case class Age(value: Int)
def parseName(name: String): Validated[String, Name] = {
if (name == null || name.isEmpty) Invalid(List("name is null or empty"))
else Valid(Name(name))
}
def parseAge(age: Int): Validated[String, Age] = {
if (age < 0 || age > 50) Invalid(List("age is not between [0, 50]"))
else Valid(Age(age))
}
def parseStudent(firstName:String, secondName: String, age: Int): Validated[String, Student] =
parseName(firstName).map3(parseName(secondName))(parseAge(age))(Student)
这一次parseStudent方法的实现,我们利用了map3方法, 现在来测试一下:
def mkString(li: List[String], s: String): String =
li.foldRight("")(_ + s + _)
parseStudent(null, "", -1) match {
case Invalid(v) => println(mkString(v, "\n"))
case Valid(s) => println("student: " + s)
}
scala>name is null or empty
scala>name is null or empty
scala>age is not between [0, 50]
可以看到使用了新的数据类型Validated, parseStudent方法可以校验出所有的错误类型