在Scala中免费验证

优锐课带你详细了解如何在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架构思维导图,分享给同样正在认真学习的每位朋友~

原文地址:https://www.cnblogs.com/youruike-/p/12331101.html

时间: 2024-10-11 23:29:19

在Scala中免费验证的相关文章

Scala中Iterator允许执行一次

背景 使用spark执行mapPartitionsWithIndex((index,iterator)=>{....}),在执行体中将iterator进行一次迭代后,再次根据iterator执行迭代,iterator迭代体未执行. 猜想及验证过程 猜测iterator只能执行一次迭代. 测试例子如下: val rdd1 = sc.makeRDD(1 to 10,2) val rdd2 = rdd1.mapPartitionsWithIndex{(index,iterator)=>{ var r

Scala 中 构造函数,重载函数的执行顺序

在调试scala在线开发教程(http://www.imobilebbs.com/wordpress/archives/4911)的过程中看到了以下代码,但是这段代码无论怎么调试都无法成功. 1 abstract class Element{ 2 def contents:Array[String] 3 val height:Int = contents.length 4 val width:Int = if(height==0) 0 else contents(0).length 5 } 6

scala中的伴生对象实现原理

孤立对象是只有一个object关键字修饰的对象. 该对象会编译成两个class文件, 一个是以孤立对象的名字命名的class,  一个是以孤立对象的名字后面加上一个$字符命名的class, 这个class又叫做虚构类. 源码中的孤立对象中的字段和方法, 都被编译成以孤立对象的名字命名的class中的静态方法, 这些静态方法都会访问单例的虚构类对象. 虚构了是传统意义上的单例模式, 并且在类初始化的时候有, 就会创建唯一的对象. 源码中的所有字段和方法都会在虚构类中有相对应的成员. 如果不明白的可

在Scala中函数和方法有什么区别

方法可以作为一个表达式的一部分出现(调用函数并传参),但是方法(带参方法)不能作为最终的表达式, 但是函数可以作为最终的表达式出现: scala> //定义一个方法 scala> def m(x:Int) = 2*x m: (x: Int)Int scala> //定义一个函数 scala> val f = (x:Int) => 2*x f: Int => Int = <function1> scala> //方法不能作为最终表达式出现 scala&g

scala中trait学习笔记

scala中提供的trait(特质)和Java中的Interface有很多相似之处.都可以持有方法的声明和属性,但是trait还有比interface强大的多的其他用法. 1. trait可以带有方法实现: 2. trait与interface一样,可以互相继承.但是trait可以继承自某个类,但是这种特质只能够混入父类的子类中,不能随意混入: 3. trait中可以在运行时动态调用方法. 下面举一个trait使用的例子. 首先定义一个虚类IntQueue和特质Logger abstract c

转载: scala中span和partition区别

scala中的partition span splitAt groupBy 可把Collection分成:满足条件的一组,其他的另一组. partitionspan List(1,9,2,4,5).span(_<3)       // (List(1),List(9, 2, 4, 5)),碰到不符合就结束 List(1,9,2,4,5).partition(_<3) // (List(1, 2),List(9, 4, 5)),扫描所有 splitAt // (List(1, 3),List(5

scala学习手记16 &ndash; scala中的static

前面两节学了scala的对象和伴生对象,这两个在使用的时候很有些java的静态成员的意思. scala中没有静态字段和静态方法.静态成员会破坏scala所支持的完整的面向对象模型.不过可以通过伴生对象实现对scala的类一级的操作. 回过头来再看一遍那个Marker的例子,略做了一些调整: class Marker private(val color: String) { println("Creating " + this) override def toString(): Stri

scala学习手记2 - scala中的循环

先来看一段Java中的循环: for (int i = 1; i < 4; i++) { System.out.print(i + ","); } 毫无疑问,scala可以让这个循环更加简洁.根据上一节中的内容,没有必要显示指定变量i的类型,我们甚至不需要声明这个变量.其次输出的语句也可以更加简洁一些,在scala中可以直接使用println()这个方法输出字符串.最后scala的循环结构也是非常的轻量级.好了,可以看一下代码了: for (i <- 1 to 3) { p

第85讲:Scala中For表达式的强大表现力实战

今日[DT大数据梦工厂视频]<第85讲:Scala中For表达式的强大表现力实战>51CTO视频:http://edu.51cto.com/lesson/id-71503.html(DT大数据梦工厂scala的所有视频.PPT和代码在百度云盘的链接:http://url.cn/fSFPjS)85讲 scala for 表达式的强大表现力高阶函数的行为 指定了对数据 处理 的细节 .case class Person(name:String,isMale:Boolean,children:Per