2012-02-24 7 views
14

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

Скажем, у меня есть эти простые проверки:

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

насчет "(x1 | @ | х2) {(x1, x2) => ...}" Я не уверен точный синтаксис, хотя ... См. http://www.casualmiracles.com/2012/01/16/a-small-example-of-applicative-functors-with-scalaz/ – Jan

ответ

13

Вы можете взглянуть на Tale of Three Nightclubs, который описывает композицию проверки с помощью:

  1. монады (т.е. flatMap)
  2. Аппликативные функторы двумя способами (с использованием |@| и 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) 
+0

На самом деле у меня есть своя сущность на открытой вкладке прямо сейчас. Это очень хороший пример. То, с чем я борюсь, - это то, что я хочу, чтобы мои чеки сочинялись. То есть выход из одной проверки должен быть входным для следующего. Как и в вашем примере, когда у вас есть свои функции проверки в списке, все они выполняют проверку того же экземпляра человека, который отличается от того, что я пытаюсь сделать. – chrsan

+0

Вы описываете 'flatMap'; т. е. вы уже знали ответ! –

+0

Спасибо большое! Нет, в этом случае ему не нужен целый метод, но мне нужно проверить одно и то же в разных контекстах. То есть идентификатор сам по себе и объект, который имеет идентификатор и некоторые другие поля. «Лучшим способом» я имел в виду прохладно выглядящие короткие методы, подобные тем, которые вы описываете выше, что не так очевидно для нас с императивным фоном, желающим быть более функциональным и т. Д. – chrsan

17

В дополнение к решениям, предложенным @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 называется стрелка Клейсли.

Вы можете составить две Клейсли стрелки 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

О, для вывода с использованием частично прикладного типа! –

+0

@oxbow_lakes, это одна из самых необходимых вещей в Scala. К сожалению, это не похоже на их краткий список. – missingfaktor

+1

, как отметил @oxbow_lakes, существует подход с коротким замыканием, а также подход с накоплением. Этот пример - подход с коротким замыканием. Как это можно сделать, если вы хотите накопить неудачи? – OleTraveler

0

Expression

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

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

(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 будет содержать оба

+1

Er, нет, он не может. Успешный результат для первой проверки требуется в качестве ввода ко второму, поэтому аппликативные функторы не могут помочь вам здесь –

0

Я недавно закодировал простую «структуру» для декларативных валидаций, которые являются составными. Сначала я основывал свою реализацию на ответе @ missingfaktor, но в дополнение к тому, что он придумал, я добавил DSL, используя Shapeless's 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.

0

В дополнение к ответу missingfaktor, можно заметить, что скалаз 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)