Как составлять функции с аппликативными эффектами для валидации в Cats в Scala - PullRequest
1 голос
/ 11 марта 2019

Вот пример из книги Scala с кошками :

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 с readName и readAge?С fail fast это довольно просто, потому что я использую for-comrehension и имею доступ к результатам readName и readAge:

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

, но как получить тот же результат для failslow?

РЕДАКТИРОВАТЬ вероятно, это не достаточно ясно, с этими функциями.Вот реальный пример использования.Существует функция, похожая на readName / readAge, которая аналогичным образом проверяет дату.Я хочу создать функцию проверки, которая принимает 2 даты, чтобы убедиться, что одна дата приходит за другой.Дата происходит от строки.Вот пример того, как это будет выглядеть для 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

Моя цель - накапливать возможные ошибки с датами аппликативным способом.В книге сказано, с.157:

Мы не можем flatMap, потому что Validated не является монадой.Тем не менее, Cats обеспечивает замену flatMap под названием andThen.Сигнатура типа andThen идентична сигнатуре flatMap, но у нее другое имя, поскольку она не является законной реализацией в отношении законов монады:

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
      }
    }

Ответы [ 2 ]

1 голос
/ 11 марта 2019

Наконец, я мог бы составить его с помощью следующего кода:

  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).Затем используется fast-fast, потому что если какая-либо из дат неверна и не может быть проанализирована, нет смысла продолжать и сравнивать их как даты.

Это прошло через мои тесты.

Если вы можете предложить другое, более элегантное решение, всегда добро пожаловать!

1 голос
/ 11 марта 2019

Может быть, код не требует пояснений:

/** 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, поскольку это будет иметь последовательную семантику (В этом case FailFast поведение) .
Таким образом, вам нужно использовать предоставленные ими абстракции, обычно mapN для вызова функции со всеми аргументами, если все они допустимы, или tupled для создания кортежа.

Редактировать

Как указано в документации, andThen следует использовать там, где вы хотите, чтобы Validated работал как Монада , не будучи единым целым.
Это просто для удобства, но вы не должны использовать его, если хотите семантику FailSlow.

"Эта функция аналогична flatMap для Either. Она не называется flatMap, потому что по соглашению Cats flatMap - это монадное связывание, которое соответствует ap. Этот метод не согласуется с ap (или другими методами, основанными на Apply), потому что он ведет себя как "отказоустойчивый", а не накапливает ошибки проверки ".

...