2

I am learning Scala and today came across the Fail Slow mechanism using Scalaz ValidationNel however it is really difficult to understand how to use it. I am reading these blogs: Blog1 , I am reading this StackOverflow post too: StackOverflow but it is really difficult to understand for non functional programmer. Can somebody provide a simple example on how to accumulate errors in ValidationNel in Scala? It will be really helpful to have a description about the example too.

Explorer
  • 1,491
  • 4
  • 26
  • 67

2 Answers2

1

Using the example from the blog you've linked

val sumV: ValidationNEL[String, Int] = for {
  a <- 42.successNel[String]
  b <- "Boo".failNel[Int]
  c <- "Wah wah".failNel[Int] // by defn of flatMap, can't get here
} yield a + b + c

What this is doing is using a flatMap to chain together various operations. 42.successNel[String], for example, creates a Success, and "Boo".failNel[Int] creates a failure. The way flatMap works here is to continue on to the next operations only on a success. So this is a "fail fast" operation - it gathers the first failure into your error case and stops.

If you want to "fail slow" - ie. gather all possible failures, you need to use a different method. This is where Applicative comes in.

val yes = 3.14.successNel[String]
val doh = "Error".failNel[Double]

def addTwo(x: Double, y: Double) = x + y

(yes |@| yes)(addTwo) // Success(6.28)
(doh |@| doh)(addTwo) // Failure(NonEmptyList(Error, Error))

(a |@| b)(someFunctionOfTwoArgsHere) - What this is saying is "Perform the 'a' operation, and perform the 'b' operation, and if both are successful, perform someFunctionOfTwoArgsHere(a,b). Otherwise, take any failures and combine them. So if a fails, but b succeeds, you get a Validation failure with the result of a failing. If a AND b fails, you get a Validation failure with the results of both a and b failing.

Ren
  • 3,395
  • 2
  • 27
  • 46
  • thanks for your answer, what exactly the statement `val yes = 3.14.successNel[String]` means? I am from a OOPs background and I am trying to corelated your example with `if(condition)` statement. – Explorer Oct 04 '17 at 02:17
  • `val yes = 3.14.successNel[String]` is creating an instance of a ValidationNEL[String, Int] object. The type parameters (String and Int) refer to the failure and success cases respectively. This kind of object either contains a non empty list of the type on the lift (String, in this case) or a single instance of a the type on the right (Int). This type of object has the `|@|` behaviour encoded into it, and that behaviour is - take another ValidationNel, and check what the actual value of the objects are - are they both Successes? – Ren Oct 04 '17 at 02:31
  • In that case, take the right arguments and run it through the function, and put the result in a Success object. Or are they both Failure, or one of them is a Failure? In that case, take the Failures and combine them in a list, and put that list in a Failure object, – Ren Oct 04 '17 at 02:33
0

Previous answer is good but I understand you come from OOP paradigm so let me put another example comparing both paradigms.

Common code:

val a = "1"
val b = "aaa"
val c = "bbb"

def isAllDigits(x: String) = x forall Character.isDigit
def appendError(x: String, errors: mutable.Buffer[String]) = errors += s"$x is not number"

type Errors = NonEmptyList[String]

// disjunction \/ for fail fast
def toDigitFailFast(x: String): Errors \/ Int = {
  if (isAllDigits(x)) {
    x.toInt.right
  } else {
    s"$x is not number".wrapNel.left
  }
}

// validation nel (non empty list) for fail slow
def toDigitFS(x: String): ValidationNel[String, Int] = {
  if (x forall Character.isDigit) {
    x.toInt.successNel
  } else {
    s"$x is not number".failureNel
  }
}

Code for fail fast imperative:

// fail fast imperative programming
println("---\nFail Fast imperative")
val failFastErrors = mutable.Buffer.empty[String]

if(isAllDigits(a)) {
  if(isAllDigits(b)) {
    if(isAllDigits(c)) {
      val total = a.toInt + b.toInt + c.toInt
      println(s"Total = ${total}!!")
    } else {
      appendError(c, failFastErrors)
    }
  } else {
    appendError(b, failFastErrors)
  }
} else {
  appendError(a, failFastErrors)
}

if(failFastErrors.nonEmpty) {
  println("Errors:")
  for(error <- failFastErrors) {
    println(error)
  }
}

Code for fail fast functional (with disjunction /):

val resultFunc = for {
  x <- toDigitFailFast(a)
  y <- toDigitFailFast(b)
  z <- toDigitFailFast(c)
} yield (x + y + z)

resultFunc match {
  case \/-(total) => println(s"Total = $total")
  case -\/(errors) =>
    println("Errors:")
    errors.foreach(println)
}

Output on fail fast (only tells you first error):

Fail Fast imperative
Errors:
aaa is not number
Fail Fast functional
Errors:
aaa is not number

Now the fail slow code for imperative:

// fail slow imperative programming
println("---\nFail Slow imperative")
val failSlowErrors = mutable.Buffer.empty[String]

if(!isAllDigits(a)) {
  appendError(a, failSlowErrors)
}
if(!isAllDigits(b)) {
  appendError(b, failSlowErrors)
}
if(!isAllDigits(c)) {
  appendError(c, failSlowErrors)
}

if(failSlowErrors.isEmpty) {
  val total = a.toInt + b.toInt + c.toInt
  println(s"Total = ${total}!!")
} else {
  println("Errors:")
  for(error <- failSlowErrors) {
    println(error)
  }
}

And the functional version (fail slow):

// fail slow functional programming
println("---\nFail Slow functional")


val resultFuncSlow = 
  (toDigitFS(a) |@| toDigitFS(b) |@| toDigitFS(c)) { _ + _ + _ }

resultFuncSlow match {
  case Success(result) => println(result)
  case Failure(errors) =>
    println("Errors:")
    errors.foreach(println)
}

And the output with both errors:

Fail Slow imperative
Errors:
aaa is not number
bbb is not number
---
Fail Slow functional
Errors:
aaa is not number
bbb is not number
Carlos Verdes
  • 3,037
  • 22
  • 20