[英]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)
一些注意事項:每個原始驗證函數: nonBlank
, nonNegative
, getValue
返回所謂的FailFast類型,它是單子函數,不適用。
有兩個函數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
與readName
和readAge? 快速失敗非常簡單,因為我使用for-comrehension
readName
並且可以訪問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。 但是,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.