在上一章節(jié)中我們介紹了Either的實現(xiàn)巴柿,在使用Either來校驗輸入的例子中我們提到了Either的一個缺陷,那就是Either只能收集一次錯誤信息死遭,收集完就返回錯誤信息了广恢, 這里存在的問題就是當(dāng)我們需要多個輸入項進行校驗的時候,每次都只能返回一個錯誤信息呀潭。想想一個表單的校驗钉迷,假如用戶有許多的字段都是錯誤的,沒有提交只能返回一個錯誤信息钠署,那用戶需要不斷的提交試錯糠聪,這樣是非常不友好的。參照Either谐鼎,我們來實現(xiàn)一種新的數(shù)據(jù)結(jié)構(gòu):
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的結(jié)構(gòu)和Either結(jié)構(gòu)基本是類似的舰蟆,只是表示錯誤的時候用的是Invalid[+E](value: List[E]),他表明Invalid可以收集和傳遞無限個錯誤。再看一看其中基本方法的實現(xiàn)身害,其中其他的基本方法的實現(xiàn)都是類似的味悄,只有map2方法不一樣,新的map2方法再遇到兩個Validated都是Invalid類型的時候塌鸯,可以將Invalid中的錯誤信息組合起來傍菇。既然實現(xiàn)了map2我們完全可以使用map2來實現(xiàn)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的實現(xiàn):
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的實現(xiàn)是類似的,但是由于map2的實現(xiàn)不同界赔,他產(chǎn)生的效果是完全不一樣的丢习, 我們再來看下之前關(guān)于校驗的例子:
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方法的實現(xiàn),我們利用了map3方法淮悼, 現(xiàn)在來測試一下:
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]
可以看到使用了新的數(shù)據(jù)類型Validated咐低, parseStudent方法可以校驗出所有的錯誤類型