Составьте проверки Скалаза - PullRequest
14 голосов
/ 24 февраля 2012

Я хотел бы использовать Scalaz для валидации и хотел бы иметь возможность повторно использовать функции валидации в разных контекстах.Кстати, я новичок в Scalaz.

Допустим, у меня есть следующие простые проверки:

def checkDefined(xs: Option[String]): Validation[String, String] =
  xs.map(_.success).getOrElse("empty".fail)

def nonEmpty(str: String): Validation[String, String] =
  if (str.nonEmpty) str.success else "empty".fail

def int(str: String): Validation[String, Int] = ...

Мне нравится иметь возможность составлять проверки, когда выходные данные одного поступают в другой.Я мог бы легко сделать это с помощью flatMap или через для понимания, но кажется, что должен быть лучший способ, чем это.

for {
  v1 <- checkDefined(map.get("foo"))
  v2 <- nonEmpty(v1)
  v3 <- int(v2)
  v4 <- ...
} yield SomeCaseClass(v3, v4)

или

val x1 = checkDefined(map get "foo").flatMap(nonEmpty).flatMap(int)
val x2 = check(...)

// How to combine x1 and x2?

Любые мысли из Скалазаэксперты там?

Ответы [ 5 ]

17 голосов
/ 24 февраля 2012

В дополнение к решениям, предложенным @oxbow_lakes, вы также можете использовать композицию Клейсли.

scala> import scalaz._, Scalaz._
import scalaz._
import Scalaz._

scala> def f: Int => Validation[String, Int] = i => if(i % 2 == 0) Success(i * 2) else    Failure("Odd!")
f: Int => scalaz.Validation[String,Int]

scala> def g: Int => Validation[String, Int] = i => if(i > 0) Success(i + 1) else Failure("Not positive!")
g: Int => scalaz.Validation[String,Int]

scala> type Va[+A] = Validation[String, A]
defined type alias Va

scala> import Validation.Monad._
import Validation.Monad._

scala> kleisli[Va, Int, Int](f) >=> kleisli[Va, Int, Int](g)
res0: scalaz.Kleisli[Va,Int,Int] = scalaz.Kleislis$$anon$1@4fae3fa6

scala> res0(11)
res1: Va[Int] = Failure(Odd!)

scala> res0(-4)
res2: Va[Int] = Failure(Not positive!)

scala> res0(4)
res3: Va[Int] = Success(9)

Функция типа A => M[B], где M : Monad называется стрелкой Клейсли.

Вы можете составить две стрелки Клейсли A => M[B] и B => M[C], чтобы получить стрелку A => M[C], используя оператор >=>. Это известно как композиция Клейсли.

Выражение kleisli(f) >=> kleisli(g) >=> kleisli(h) эквивалентно x => for(a <- f(x); b <- g(a); c <- h(b)) yield c, минус ненужные локальные привязки.

13 голосов
/ 24 февраля 2012

Возможно, вы захотите взглянуть на Сказку о трех ночных клубах , в которой описана композиция для проверки с использованием:

  1. Монады (т.е. flatMap)
  2. Аппликативные функторы двумя способами (с использованием |@| и traverse)

В основном правила сводятся к следующему: составление через монады fail-fast . То есть ваши вычисления в этой точке будут короткими и преобразуются в Failure(e). Использование аппликативных функторов означает, что вы можете накапливать сбои (возможно, для проверки веб-формы) - что вы делаете, используя collection (то есть Semigroup) в качестве типа сбоев - канонические примеры используют NonEmptyList.

В Validation есть и другие полезные вещи:

val1 <+> val2    //Acts like an `orElse`
val1 >>*<< val2  //Accumulates both successes and failures

В вашем конкретном примере, почему вы думаете, что "должен быть лучший способ", чем делать это через понимание? Это может быть немного улучшено, хотя:

def checkDefined(xs: Option[String]) = xs.toSuccess("empty :-(")

В этом случае он не заслуживает целого метода:

for {
  v1 <- map get "foo" toSuccess "Empty :-("
  v2 <- some(v1) filterNot (_.isEmpty) toSuccess "Empty :-("
  v3 <- (v2.parseInt.fail map (_.getMessage)).validation 
  v4 <- ...
} yield SomeCaseClass(v3, v4)
0 голосов
/ 28 апреля 2017

В дополнение к ответу об отсутствующем факторе можно заметить, что у скалаза 7 нет Monad для Validation из-за несоответствия его поведения с экземпляром Apply.Таким образом, можно определить Bind для Validation вместе с конвертерами для удобства:

import scalaz.{Bind, Kleisli, Validation, Success, Failure}

implicit def toKleisli[E, A, B](f: A => Validation[E, B]): Kleisli[Validation[E, ?], A, B] =
  Kleisli[Validation[E, ?], A, B](f)

implicit def fromKleisli[E, A, B](f: Kleisli[Validation[E, ?], A, B]): A => Validation[E, B] = f.run

implicit def validationBind[E] = new Bind[Validation[E, ?]] {

  def bind[A, B](fa: Validation[E, A])(f: (A) => Validation[E, B]): Validation[E, B] = {
    import Validation.FlatMap._
    fa.flatMap(f)
  }

  def map[A, B](fa: Validation[E, A])(f: (A) => B): Validation[E, B] = fa.map(f)
}

val parse: Option[String] => Validation[String, Int] = checkDefined _ >=> nonEmpty _ >=> int _

println(parse(None)) // Failure(empty)
println(parse(Some(""))) // Failure(empty)
println(parse(Some("abc"))) // Failure(java.lang.NumberFormatException: For input string: "abc")
println(parse(Some("42"))) // Success(42)
0 голосов
/ 24 октября 2014

Я недавно кодировал простую "структуру" для декларативных проверок, которые являются составными. Первоначально я основал свою реализацию на ответе @ missingfaktor, однако, помимо того, что он придумал, я добавил DSL, используя Shapeless's Generic для работы с кортежами произвольного размера входов в быть проверены, которые передаются в функции соответствия арности.

Его использование выглядит следующим образом:

def nonEmpty[A] = (msg: String) => Vali { a: Option[A] =>
  a.toSuccess(msg)
}

def validIso2CountryCode = (msg: String) => Vali { x: String =>
  IsoCountryCodes2to3.get(x).toSuccess(msg)
}

val postal = "12345".some
val country = "GB".some

val params = (
  postal
     |> nonEmpty[String]("postal required"),
  country
     |> nonEmpty[String]("country required")
    >=> validIso2CountryCode("country must be valid")
)

// parameter type inference doesn't work here due to the generic type level nature of the implementation; any improvements are welcome!
validate(params) { (postal: String, country: String) =>
  println(s"postal: $postal, country: $country")
}

Реализация может быть найдена в https://gist.github.com/eallik/eea6b21f8e5154e0c97e.

0 голосов
/ 24 февраля 2012

Выражение

for {
  v1 <- checkDefined(map.get("foo"))
  v2 <- nonEmpty(v1)
  v3 <- int(v2)
  v4 <- someComputation()
} yield SomeCaseClass(v3, v4)

можно заменить таким образом

(checkDefined(map.get("foo")).liftFailNel |@| nonEmpty(v1)) {(v1, v2) =
    SomeCaseClass(int(v2), someComputation)
}

и результат будет

 Validtion[NonEmptyList[String], SomeCaseClass] which is equal to ValidationNEL[String, SomeCaseClass]

если обе проверки не пройдены, NonEmptyList будет содержать их обоих

...