优锐课带你详细了解如何在Scala中实施免费的monad验证。抽丝剥茧,细说架构那些事!
由于业务数据的复杂性,已经在数据验证上花费了很多精力。在Scala中,提出了使用应用程序进行验证的方法,并被广泛认为是一种有效的方法。受应用验证和免费monad的思想启发,在本文中,我们介绍了一个monadic验证框架,该框架“免费”构建验证。我们将进一步讨论此方法,并通过示例代码演示实现。
在Scala中验证
当检测到错误时,可以以不同的形式找到验证。遇到第一个错误(或异常)时,验证可以立即返回;验证结果可能包含也可能不包含验证错误或异常消息。这种情况称为快速失败验证,其中该验证未验证所有业务规则,并且仅返回零或一个消息,并且在出现第一个错误时应缩短该过程。这种简单的验证形式有时被认为是不充分的,因为没有累积的错误就无法进行完整的验证。实际上,它广泛用于应用程序开发中。 Scala中的Monadic实体(例如Option,Try和Either)非常易于使用。
验证所有业务规则和累积错误与快速失败的验证有很大不同。提出了应用函子,它们有效地解决了累积问题。流行的Scala库,例如scalaz和Cats,都提供了应用程序验证支持。读者可以参考scalaz.Validation API和cats.data.Validated API了解更多详细信息。
但是,由于各种原因,用Scala编写的许多项目都不使用这两个库中的任何一个,并且Scala语言没有对应用性理解的本地支持。因此,一种不依赖任何第三方库的本地验证方法非常有吸引力。
验证问题与免费monad可以解决的问题非常相似——将一系列验证器建模为用于理解的工作流,并在解释器中执行它们。免费的monad提供了一种强大的方法,可以将验证程序隐式地提升到monad中(免费)。因此,我们需要解决的问题是对我们的解释器进行重新建模以在遇到错误或异常时继续执行。解释器应有能力保证每个验证器的执行,并且所有验证消息都应返回到呼叫站点。
验证器和免费Monad
在我们的方法中,验证是验证器执行的单子组合;每个验证器代表对每个业务规则的验证,由理解组成。这是免费的monad可以提供帮助的地方——可以将验证器隐式提升为“免费”的monad。这使我们的验证可以享受免费的monad可以提供的所有好处——堆栈免费和自然转换。
首先,我们讨论快速失败的验证框架及其局限性。
假设我们需要先验证一个人的姓名和年龄,然后再将该人保存到数据库中:
1 case class Person(name: String, age: Int){ 2 def validateName= if (name.isEmpty) None else Some("Success") 3 def validateAge = if (age < 18) None else Some("Success") 4 } 5 def save(person: Person): Boolean = { 6 println(s"save ${person.name} at age ${person.age}") 7 true 8 } 9 val person = Person("Michael", 20) 10 for { 11 _ <- person.validateName 12 _ <- person.validateAge 13 } yield save(person)
上面的编码有两个限制:
1.验证不会将验证错误带到呼叫站点,遇到错误时将返回无
2.验证将在无时停止。为了理解,将上面的代码合并成一个flatMap链,然后是一个map:
1 person.validateName 2 .flatMap((_: String) =>person.validateAge.map((_: String) => save(person)))
如果名称为空,validateName返回None,该过程将短暂停止,并且validateAge将不会执行——这是快速失败的验证。
在我们的方法中,我们对验证器进行建模以使其符合简单特征:
1 sealed trait Validator[A] { 2 def validate: Option[Error] 3 def unbox: A 4 }
每个验证器都实现自己的验证规则:
1 case class NameValidator(name: String) extends Validator[String] { 2 def validate = if (name.isEmpty) Option(NameError) else None 3 def unbox: String = name 4 } 5 case class AgeValidator(age: Int) extends Validator[Int] { 6 def validate = if (age >= 18) None else Some(AgeError) 7 def unbox: Int = age 8 }
请注意此实现中的以下内容:
- 验证器是要验证的数据及其验证规则的容器。提供了“取消装箱”功能,以允许从验证器获取数据,这在稍后将讨论的实现中很重要;
- 我们的验证器不会在错误时返回None,而在错误时返回Some [Error],而没有错误则返回None。这使我们能够将错误消息传送回呼叫站点。
验证程序隐式提升为monad,就像在*monad中一样:
implicit def liftF[F[_], A](fa: F[A]): Free[F, A] = FlatMap(fa, Return.apply)
免费的monad是以下形式的标准:
1 sealed trait Free[F[_], A] { 2 def flatMap[B](f: A => Free[F, B]): Free[F, B] = this match { 3 case Return(a) => f(a) 4 case FlatMap(sub, cont) => FlatMap(sub, cont andThen (_ flatMap f)) 5 } 6 def map[B](f: A => B): Free[F, B] = flatMap(a => Return(f(a))) 7 } 8 final case class Return[F[_], A](a: A) extends Free[F, A] 9 case class FlatMap[F[_], I, A](sub: F[I], cont: I => Free[F, A]) extends Free[F, A]
解释器相应地执行验证器:
1 val interpreter = new Executor[Validator] { 2 override def exec[A](fa: Validator[A]) = fa.validate 3 override def unbox[A](fa: Validator[A]) = fa.unbox 4 } 5 def validate[F[_], A](prg: Free[F, A], interpreter: Executor[F]): List[Error] = { 6 def go(errorList: List[Option[Error]], prg: Free[F, A]): List[Option[Error]]= 7 prg match { 8 case Return(a) => errorList 9 case FlatMap(sub, cont) => go(interpreter.exec(sub) :: errorList, 10 cont(interpreter.unbox(sub))) 11 } 12 go(List.empty[Option[Error]], prg).flatten 13 }
解释器是数据,错误,数据验证器和*单子之间的粘合剂。请注意,此解释器与免费monad中的解释器之间的三个重要的详细区别是:
- 解释器提供拆箱功能;它用于“no-error”情况。返回None类型时,unbox用于以类型安全的方式查找正在验证的数据。为了继续验证过程,反装箱又使用验证器提供的拆箱功能来获取正在验证的数据。
- 与免费monad中的解释器不同,该过程的继续是通过monadic操作完成的,可能会缩短为None:
executor.exec(sub).flatMap(x => validateAndRun(cont(x), executor))
在我们的验证解释器中,通过遵循验证器直到执行最后一个验证器来保证过程的继续,因为顺序执行是从flatMap
中取出的,但是仍然保持了尾递归位置,因此验证过程是堆栈——*。
- 返回一个列表,其中包含来自每个验证器的验证错误消息(如果存在)。
验证组成
正如工作流是在免费monad中建模一样,验证流程也是通过理解建模的。例如,如果名称验证和年龄验证全部通过,我们将调用save(person)。否则,我们将打印出累积的错误:
1 val validation = for { 2 _ <- NameValidator(person.name) 3 _ <- AgeValidator(person.age) 4 } yield () 5 validate(validation, interpreter) match { 6 case Nil => save(person) 7 case errors => errors foreach println 8 }
可以在免费验证中找到该实现。
结论
免费的monad是一种允许你从任何Functor构建monad的构造。 像其他单子一样,它是表示和操纵计算的一种纯方法。
特别是免费的monad提供了一种实用的方法:
- 将状态计算表示为数据,然后运行它们
- 以堆栈安全的方式运行递归计算
- 构建嵌入式DSL(特定于域的语言)
- 使用自然转换将计算重新定位到另一个解释器
(以上是项目类型的重点)
根据Leif Battermann的说法:
“应用程序使我们能够编写独立的操作并评估每个操作。即使中间评估失败。这也使我们能够收集错误消息,而不仅仅是返回发生的第一个错误。”
在本文中,我们介绍了一种为你提供两全其美的方法——一种验证框架,它是不带应用程序的免费monad。希望你喜欢这个简短的演示!
另外近期整理了一套完整的java架构思维导图,分享给同样正在认真学习的每位朋友~