6

Suppose I would like to code the following logic in Scala

val xdir = System.getProperty("XDir")
if (xdir == null)
   error("No XDir") // log the error and exit

val ydir = System.getProperty("YDir") 
if (ydir == null)
   error("No YDir")

if (!new File(xdir).isDirectory)
   error("XDir is not a directory")

if (!new File(ydir).isDirectory)
   error("YDir is not a directory")

if (!new File(xdir).exists)
   error("XDir does not exis")

if (!new File(ydir).exists)
   error("YDir does not exist")
...
(and so on)

What is the best way to code this chain of validations in Scala?

Michael
  • 10,185
  • 12
  • 59
  • 110

3 Answers3

4

something like:

val batch = for{
  a <- safe(doA, "A failed") either
  b <- safe(doB, "B failed") either
  c <- safe(doC, "C failed") either
} yield(a,b,c)
batch fold( error(_), doSuccess(_) )

Where safe performs a, you guessed it, safe (try/catch) operation that takes a failure (Left outcome) message and returns an Either RightProjection (which allows you to do above batch operation while threading through the point-of-failure error message)

class Catching[T](f: => T) {
  def either(msg: String) = {
    try { Right(f).right } catch { Left(msg).right }
  }
}
def safe[T](f: => T) = new Catching(f)

Can add an option method to Catching class as well, along with logging if you want to log particular error types.

See Jason Zaugg's solution for right biasing Either and this thread from scala-debate on the subject as well. No consensus as yet, but most scala "heavies" seem to be in favor.

One limitation of this approach is that if you attempt to add conditionals (if a = b) to the for{} block, it won't compile (since default Either filter method returns Option). The workaround is to implement filter and withFilter, returning Either, something I have yet to figure out/do (if someone has already done so, please post)

virtualeyes
  • 11,147
  • 6
  • 56
  • 91
4

Here's some useful things:

def sysValue(prop: String) = Option(System.getProperty(prop)) //returns Option[String]

def trySysValue(prop: String) = //returns Either[String, String]
  sysValue(prop) map Right getOrElse Left("Absent property: " + prop)

Then you can use monadic composition of Either through its right-projection

val batch = //batch is Either[String, (File, File)]
  for {
    x  <- trySysValue("XDir")).right
    xf <- dir(x).right
    y  <- trySysValue("YDir").right
    yf <- dir(y).right
  } 
  yield (xf, yf)

Where:

def dir(s: String) = { //returns Either[String, File]
  val f = new File(s)
  if (!f.exists()) Left("Does not exist: " + f)
  else if (!f.isDir()) Left("Is not a directory: " + f)
  else Right(f)
}

The left-hand-side of the Either will be an error message. This monadic composition is fail fast. You can achieve composition which will accumulate all failures (for example, if neither XDir nor YDir exist, you would see both messages) using scalaz Validation. In that case, the code would look like this:

def trySysValue(prop: String) = //returns Validation[String, String]
  sysValue(prop) map Success getOrElse ("Absent property: " + prop).fail

def dir(s: String) = {
  val f = new File(s)
  if (!f.exists())("Does not exist: " + f).fail
  else if (!f.isDir()) ("Is not a directory: " + f).fail
  else f.success
}

val batch = //batch is ValidationNEL[String, (File, File)]
  (trySysValue("XDir")) flatMap dir).liftFailNel <|*|> (trySysValue("YDir")) flatMap dir).liftFailNel
oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
  • Well, you could if you introduce the same concepts via your own library. What you are looking for is a combination of **applicative functors** and **monoids**. However, these are so useful and common, that you might as well just use scalaz. – oxbow_lakes Jun 25 '12 at 17:38
  • I talk about them here: http://skillsmatter.com/podcast/scala/practical-scalaz-2518 – oxbow_lakes Jun 25 '12 at 17:38
1

Yes you can use validation without scalaz, see here for a self containt implementation : http://applicative-errors-scala.googlecode.com/svn/artifacts/0.6/chunk-xhtml/apa.html HTH

AndreasScheinert
  • 1,918
  • 12
  • 18