Составить проверки Scalaz
Я хотел бы использовать 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?
Любые мысли от экспертов Scalaz там?
Ответы
Ответ 1
В дополнение к решениям, предложенным @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] = [email protected]
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
называется стрелкой Клейсли.
Вы можете создать две стрелки Kleisli 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
, минус ненужные локальные привязки.
Ответ 2
Возможно, вам стоит взглянуть на Рассказ о трех ночных клубах, в котором описывается состав валидации, используя:
- Монады (т.е.
flatMap
)
- Аппликативные функторы двумя способами (используя
|@|
и traverse
)
В основном правила сводятся к следующему: состав с помощью монад происходит с ошибкой. То есть, ваш расчет будет короткозамкнутым в этой точке и будет разрешен к 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)
Ответ 3
Выражение
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 будет содержать оба из них
Ответ 4
Недавно я закодировал простую "структуру" для декларативных валидаций, которые являются составными. Первоначально я основывал свою реализацию на ответе @missingfaktor, однако, помимо того, что он придумал, я добавил DSL, используя Shapeless Generic
для работы с кортежами произвольного размера входов, подлежащих проверке, которые подаются в функции согласования arity.
Его использование выглядит следующим образом:
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.
Ответ 5
В дополнение к отсутствующему ответу, можно заметить, что у scalaz 7 нет Monad
для Validation
из-за несоответствия его поведения экземпляру Apply
. Таким образом, для Validation
можно определить Bind
вместе с конвертерами:
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)