链接Scalaz验证函数:Function1 [A,验证[E,B]]

时间:2012-12-19 21:57:50

标签: scala scalaz

我正在尝试编写一些代码,以便轻松链接返回Scalaz Validation类型的函数。我试图编写的一种方法类似于Validation.flatMap(验证短路),我称之为andPipe。另一个类似于|@|上的ApplicativeBuilder(累积错误),除了它只返回最终Success类型,我将称之为andPass

假设我有功能:

def allDigits: (String) => ValidationNEL[String, String]
def maxSizeOfTen: (String) => ValidationNEL[String, String] 
def toInt: (String) => ValidationNEL[String, Int]

作为一个例子,我想首先将输入String传递给allDigits和maxSizeOf10。如果存在故障,则应通过不调用toInt函数来短路,并返回发生的一个或两个故障。如果成功,我想将Success值传递给toInt函数。从那里,它将成功输出值为Int,或者它将无法仅从toInt返回验证失败。

def intInput: (String) => ValidationNEL[String,Int] = (allDigits andPass maxSizeOfTen) andPipe toInt 

如果没有我的附加实现,有没有办法做到这一点?

这是我的实施:

  trait ValidationFuncPimp[E,A,B] {
    val f: (A) => Validation[E, B]

    /** If this validation passes, pass to f2, otherwise fail without accumulating. */
    def andPipe[C](f2: (B) => Validation[E,C]): (A) => Validation[E,C] = (a: A) => {
      f(a) match {
        case Success(x) => f2(x)
        case Failure(x) => Failure(x)
      }
    }

    /** Run this validation and the other validation, Success only if both are successful.  Fail accumulating errors. */
    def andPass[D](f2: (A) => Validation[E,D])(implicit S: Semigroup[E]): (A) => Validation[E,D] = (a:A) => {
      (f(a), f2(a)) match {
        case (Success(x), Success(y)) => Success(y)
        case (Failure(x), Success(y)) => Failure(x)
        case (Success(x), Failure(y)) => Failure(y)
        case (Failure(x), Failure(y)) => Failure(S.append(x, y))
      }
    }
  }
  implicit def toValidationFuncPimp[E,A,B](valFunc : (A) => Validation[E,B]): ValidationFuncPimp[E,A,B] = {
    new ValidationFuncPimp[E,A,B] {
      val f = valFunc
    }
  }

3 个答案:

答案 0 :(得分:5)

我并不是说这个答案肯定比drstevens's更好,但它采取的方法略有不同,不适合那里的评论。

首先是我们的验证方法(注意我已经改变了toInt的类型,原因我将在下面解释):

import scalaz._, Scalaz._

def allDigits: (String) => ValidationNEL[String, String] =
  s => if (s.forall(_.isDigit)) s.successNel else "Not all digits".failNel

def maxSizeOfTen: (String) => ValidationNEL[String, String] =
  s => if (s.size <= 10) s.successNel else "Too big".failNel

def toInt(s: String) = try(s.toInt.right) catch {
  case _: NumberFormatException => NonEmptyList("Still not an integer").left
}

为方便起见,我将定义一个类型别名:

type ErrorsOr[+A] = NonEmptyList[String] \/ A

现在我们刚刚得到了几个Kleisli箭头:

val validator = Kleisli[ErrorsOr, String, String](
  allDigits.flatMap(x => maxSizeOfTen.map(x *> _)) andThen (_.disjunction)
)

val integerizer = Kleisli[ErrorsOr, String, Int](toInt)

我们可以撰写:

val together = validator >>> integerizer

并像这样使用:

scala> together("aaa")
res0: ErrorsOr[Int] = -\/(NonEmptyList(Not all digits))

scala> together("12345678900")
res1: ErrorsOr[Int] = -\/(NonEmptyList(Too big))

scala> together("12345678900a")
res2: ErrorsOr[Int] = -\/(NonEmptyList(Not all digits, Too big))

scala> together("123456789")
res3: ErrorsOr[Int] = \/-(123456789)

对非monadic的东西使用flatMap会让我有点不舒服,并将我们的两个ValidationNEL方法合并到\/ monad中的Kleisli箭头中 - 这也是一个我们的字符串到整数转换的适当模型对我来说感觉更清洁。

答案 1 :(得分:3)

这是相对简洁的,几乎没有“添加代码”。它仍然有点不稳定,因为它忽略了应用allDigits的成功结果。

scala> val validated = for {
     |   x <- allDigits
     |   y <- maxSizeOfTen
     | } yield x *> y
validated: String => scalaz.Validation[scalaz.NonEmptyList[String],String] = <function1>

scala> val validatedToInt = (str: String) => validated(str) flatMap(toInt)
validatedToInt: String => scalaz.Validation[scalaz.NonEmptyList[String],Int] = <function1>

scala> validatedToInt("10")
res25: scalaz.Validation[scalaz.NonEmptyList[String],Int] = Success(10)

或者,您可以同时保留allDigitsmaxSizeOfTen的两个输出。

val validated2 = for {
  x <- allDigits
  y <- maxSizeOfTen
} yield x <|*|> y

我很好奇其他人是否能想出更好的方法来结合这些。这不是真的构图......

val validatedToInt = (str: String) => validated2(str) flatMap(_ => toInt(str))

validatedvalidated2都会累积失败,如下所示:

scala> def allDigits: (String) => ValidationNEL[String, String] = _ => failure(NonEmptyList("All Digits Fail"))
allDigits: String => scalaz.Scalaz.ValidationNEL[String,String]

scala> def maxSizeOfTen: (String) => ValidationNEL[String, String] = _ => failure(NonEmptyList("max > 10"))
maxSizeOfTen: String => scalaz.Scalaz.ValidationNEL[String,String]

scala> val validated = for {
     |   x <- allDigits
     |   y <- maxSizeOfTen
     | } yield x *> y
validated: String => scalaz.Validation[scalaz.NonEmptyList[String],String] = <function1>

scala> val validated2 = for {
     |   x <- allDigits
     |   y <- maxSizeOfTen
     | } yield x <|*|> y
validated2: String => scalaz.Validation[scalaz.NonEmptyList[String],(String, String)] = <function1>

scala> validated("ten")
res1: scalaz.Validation[scalaz.NonEmptyList[String],String] = Failure(NonEmptyList(All Digits Fail, max > 10))

scala> validated2("ten")
res3: scalaz.Validation[scalaz.NonEmptyList[String],(String, String)] = Failure(NonEmptyList(All Digits Fail, max > 10))

答案 2 :(得分:1)

将ApplicativeBuilder与前两个一起使用,以便累积错误, 然后是flatMap toInt,所以只有前两个成功才会调用toInt

val validInt: String => ValidationNEL[String, Int] = 
  for {
    validStr <- (allDigits |@| maxSizeOfTen)((x,_) => x); 
    i <- toInt
  } yield(i)