以下是the Scala with Cats book中的一个示例:
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)
一些注意事项:
每个原始验证函数:nonBlank
,nonNegative
,getValue
返回所谓的FailFast类型,它是单子函数,而不是适用的。
有2个函数readName
和readAge
,它们使用以前的函数组成,本质上也是FailFast。
readUser
相反,失败较慢。为此,将readName
和readAge
的结果转换为Validated并通过所谓的“语法”组成
让我们假设我还有另一个验证功能,该功能接受名称和年龄,并由readName
和readAge
验证。举例来说:
//fake implementation:
def validBoth(name:String, age:Int):FailSlow[User] =
Validated.valid[List[String], User](User(name,age))
如何用validBoth
和readAge组成readName
?快速失败非常简单,因为我使用了for-comrehension
并可以访问readName
和readAge
的结果:
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。但是,猫确实 提供一个名为andThen的flatMap替代品。的类型签名 并且然后与flatMap相同,但是名称不同 因为这不是针对monad的合法实施 法律:
32.valid.andThen { a =>
10.valid.map { b =>
a + b
}
}
好吧,我尝试基于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
}
}
答案 0 :(得分:1)
也许代码在这里是不言自明的:
/** 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 而不是一个的地方。
它只是为了方便而存在,但是如果您想要FailSlow
语义,则不要使用它。
“此函数与Either上的flatMap类似。它不称为flatMap,因为按照Cats约定,flatMap是与ap一致的单子绑定。此方法与ap(或其他基于Apply的方法)不一致,因为它具有“快速失败”的行为,而不是累积验证失败。”
答案 1 :(得分:1)
我最终可以用以下代码编写它:
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)一起累积。然后使用快速失败,因为如果任何日期错误并且无法解析,则继续并将它们作为日期进行比较是没有意义的。
它通过了我的测试用例。
如果您可以提供其他更优雅的解决方案,请随时欢迎!