繁体   English   中英

如何在Scala中的猫中组合具有实用效果的功能以进行验证

[英]How to compose functions with applicative effects for Validation in the Cats in Scala

这是Scala with Cats书中的示例:

object Ex {

  import cats.data.Validated

  type FormData = Map[String, String]
  type FailFast[A] = Either[List[String], A]

  def getValue(name: String)(data: FormData): FailFast[String] =
    data.get(name).toRight(List(s"$name field not specified"))
  type NumFmtExn = NumberFormatException

  import cats.syntax.either._ // for catchOnly
  def parseInt(name: String)(data: String): FailFast[Int] =
    Either.catchOnly[NumFmtExn](data.toInt).leftMap(_ => List(s"$name must be an integer"))

  def nonBlank(name: String)(data: String): FailFast[String] =
    Right(data).ensure(List(s"$name cannot be blank"))(_.nonEmpty)

  def nonNegative(name: String)(data: Int): FailFast[Int] =
    Right(data).ensure(List(s"$name must be non-negative"))(_ >= 0)


  def readName(data: FormData): FailFast[String] =
    getValue("name")(data).
      flatMap(nonBlank("name"))

  def readAge(data: FormData): FailFast[Int] =
    getValue("age")(data).
      flatMap(nonBlank("age")).
      flatMap(parseInt("age")).
      flatMap(nonNegative("age"))

  case class User(name: String, age: Int)

  type FailSlow[A] = Validated[List[String], A]
  import cats.instances.list._ // for Semigroupal
  import cats.syntax.apply._ // for mapN
  def readUser(data: FormData): FailSlow[User] =
    (
      readName(data).toValidated,
      readAge(data).toValidated
    ).mapN(User.apply)

一些注意事项:每个原始验证函数: nonBlanknonNegativegetValue返回所谓的FailFast类型,它是单子函数,不适用。

有两个函数readNamereadAge ,它们使用以前的函数组成,并且本质上也是FailFast。

相反, readUser失败较慢。 为此,将readNamereadAge结果转换为Validated并通过所谓的“语法”组成

假设我还有另一个用于验证的函数,该函数接受名称和年龄,并由readNamereadAge验证。 对于实例:

  //fake implementation:
  def validBoth(name:String, age:Int):FailSlow[User] =
    Validated.valid[List[String], User](User(name,age))

如何撰写validBothreadName和readAge? 快速失败非常简单,因为我使用for-comrehension readName并且可以访问readNamereadAge的结果:

for {
  n <- readName...
  i <-  readAge...
  t <- validBoth(n,i)
} yield t

但是如何为failslow获得相同的结果?

用这些功能编辑可能还不够清楚。 这是一个真实的用例。 有一个类似于readName / readAge的函数,它以类似的方式验证日期。 我想创建一个接受两个日期的验证功能,以确保一个日期紧随另一个日期。 日期来自字符串。 这是一个示例,显示FailFast的样子,在这种情况下这不是最佳选择:

def oneAfterAnother(dateBefore:Date, dateAfter:Date): FailFast[Tuple2[Date,Date]] = 
  Right((dateBefore, dateAfter))
    .ensure(List(s"$dateAfter date cannot be before $dateBefore"))(t => t._1.before(t._2))

for {
  dateBefore <- readDate...
  dateAfter <-  readDate...
  t <- oneDateAfterAnother(dateBefore,dateAfter)
} yield t

我的目的是以适用方式累积日期可能出现的错误。 在书中说,p。 157:

我们无法进行FlatMap,因为Validated不是monad。 但是,Cats确实提供了名为andThen的flatMap替代品。 andThen的类型签名与flatMap的类型签名相同,但是其名称有所不同,因为它不是就monad法则而言的合法实现:

32.valid.andThen { a =>
  10.valid.map { b =>
    a + b
  }
}

好的,我尝试基于andThen重用此解决方案,但结果产生了andThen的效果,但没有应用效果:

  def oneDateAfterAnotherFailSlow(dateBefore:String, dateAfter:String)
                                 (map: Map[String, String])(format: SimpleDateFormat)
  : FailSlow[Tuple2[Date, Date]] =
    readDate(dateBefore)(map)(format).toValidated.andThen { before =>
      readDate(dateAfter)(map)(format).toValidated.andThen { after =>
        oneAfterAnother(before,after).toValidated
      }
    }

也许代码在这里是不言自明的:

/** Edited for the new question. */
import cats.data.Validated
import cats.instances.list._ // for Semigroup
import cats.syntax.apply._ // for tupled
import cats.syntax.either._ // for toValidated

type FailFast[A] = Either[List[String], A]
type FailSlow[A] = Validated[List[String], A]
type Date = ???
type SimpleDateFormat = ???

def readDate(date: String)
            (map: Map[String, String])
            (format: SimpleDateFormat): FailFast[Date] = ???

def oneDateAfterAnotherFailSlow(dateBefore: String, dateAfter: String)
                       (map: Map[String, String])
                       (format: SimpleDateFormat): FailSlow[(Date, Date)] =
  (
    readDate(dateBefore)(map)(format).toValidated,
    readDate(dateAfter)(map)(format).toValidated
  ).tupled.ensure(List(s"$dateAfter date cannot be before $dateBefore"))(t => t._1.before(t._2))

带有Applicatives的事情是,您不应该(如果不能使用抽象技术,则不能)使用flatMap因为它将具有顺序语义(在这种情况下为FailFast行为)
因此,你需要使用抽象,他们提供的,通常mapN调用一个函数的所有参数,如果所有这些都是有效的或tupled创建一个元组。

编辑

如文档所述, andThen在您希望Validated作为Monad而不是Monad的地方使用。
它只是为了方便而存在,但是如果您需要FailSlow语义,则不要使用它。

“此函数类似于Either上的flatMap。它不称为flatMap,因为按照Cats约定,flatMap是与ap一致的单子绑定。此方法与ap(或其他基于Apply的方法)不一致,因为它具有与累积验证失败相反的“快速失败”行为”。

我最终可以用以下代码编写它:

  import cats.syntax.either._
  import cats.instances.list._ // for Semigroupal
  def oneDateAfterAnotherFailSlow(dateBefore:String, dateAfter:String)
                                 (map: Map[String, String])(format: SimpleDateFormat)
  : FailFast[Tuple2[Date, Date]] =
    for {
      t <-Semigroupal[FailSlow].product(
          readDate(dateBefore)(map)(format).toValidated,
          readDate(dateAfter)(map)(format).toValidated
        ).toEither
      r <- oneAfterAnother(t._1, t._2)
    } yield r

这个想法是,首先对字符串进行验证,以确保日期正确。 它们与Validated(FailSlow)一起累积。 然后使用快速失败,因为如果任何日期错误并且无法解析,则继续并将它们作为日期进行比较是没有意义的。

它通过了我的测试用例。

如果您可以提供其他更优雅的解决方案,请随时欢迎!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM