簡體   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