Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
scala code showing scalaz's Validation inside a for-comprehension with complex dependencies

###The Problem I have an app that uses neo4j as the underlying database. Similar to all database applications, I also have a data access layer that helps abstract out the use of neo4j.

The data access layer takes in string parameters, mostly, finds the neo4j nodes or relationships then manipulates the those objects to create a new object.

In this access layer, I must gather several different objects together to process. For example, the start node, the end node, another node that holds my dynamic type system, and various other objects.

When assembling these nodes, I typically find the pattern to go something like this:

  • Check the input parameters
  • Find the objects. This step typically takes an input parameter, then finds the object.
  • Then use that object, in sequence, to find another object and so on.
  • With all the objects assembled, create the new object

The standard approach that I use for error handling is to throw as few exceptions as possible. Instead, I would rather gather the "error" messages together and return them to the caller to handle. While this may seem like the same complexity, with its detractions, as forcing the caller to handler checked exceptions (except there are no checked exceptions in scala), I do want the caller to use non-breaking error handling so that results, especially if its a failure due to the caller's user-oriented input parameters, get back to the user in an user-friendly way.

###Scalaz Validation Here's where scalaz's Validation works. Some layers of my database management access layer return raw objects, some return Options, some may even return Trys. I need to integrate these into a more seemless sequence of function calls. Since those functions do not throw exceptions, I want to be able to return user-oriented error messages to the user if some of those objects cannot be found and hence, I cannot create the final object that was being targeted in the function call.

In other words, I want to gather error messages along the way of gathering the dependency objects that I need and return those messages to the caller.

I had started to use Trys and Options and also exceptions to communicate back to the calling function. I also tried using various "Result" objects that held a messages list in it or the created value if the call was successful.

But this quickly became burdensome because I wanted the caller to be able to use folds, maps and pattern matching to check whether the returned value was an error or not and process accordingly.

So I started to use scalaz's Validation object. It was easy to understand and use. There were a few blogs on it as well, so those helped. But here's the type of pattern I was able to implement quickly and easily. It saved me have to create many smaller "Result" classes and it worked the first time. Notice that that the dependency functions all return different types of objects: objects, strings, neo4j Nodes, neo4j Relationship/RelationshipTypes. Since I wanted to sequence these into a for-comprehension, I also need to convert these all to the same monad. I could convert everything to an Option but then I lose the ability to carry along the error messages.

###The Code

// Functions to return dependency objects.

def getNode1(path: String): Option[Node] = { ... }
def getNode2(path: String, otherArg: Int): Option[Node] = { ... }
def createLink(start: Node, end: Node, rt: RelationshipTyp): Relationship = { ... }
def relationshipType(rt: String): RelationshipType = { ... }
def findTypeByName(name: String): Node = { ... }

...

// The main function call

def createCoolObject(startPath: String, endPath: String, properties: Map[String, Any]):
  ValidationNel[String, Relationship] = {

   (for {
      // We could also convert each line to a ValidationNel but we don't, its more syntax overhead.
      ltype <- findTypeByName("...dynamic type...").toSuccess("Data type not found")

      linkName <- ltype.get("linkName").map(_.toString).toSuccess("linkName property was not present in type")

      // We can also use a fold on an Option to pluck out the values
      relType <- Option(relationship(linkName))
        .fold("No relationship found to create link".failNel[RelationshipType])(_.successNel[String])

      specNode <- find(fileSpecPath)
        .fold(("No file specification found at: " + fileSpecPath).failNel[Node])(_.successNel[String])

      formatNode <- find(fileFormatPath).toSuccess("No file format found at: " + fileFormatPath)

      rel <- Option(makeLink(specNode, formatNode, relType)).toSuccess("Could link")

    } yield {
      properties.map { entry => rel.update(entry._1, entry._2) }
      rel
    }).toValidationNel 
  }

###How it Solved My Problem Notice how each line in the for-comprehension uniformly handles different return values from the different layers I created. In this case, I get all the returned values back to a Validation. In some lines, I use fold to pluck out the return value and convert it to a Validation but that's not usually necessary.

Notice what his does not do. This does not accumulate messages from different lines into a list of messages and return them if something goes wrong. Since each line in the for-comprehension is dependent on the previous, we do not need to use a slightly different syntax (the applicative syntax) to evaluate several dependencies at once before creating the final object. We could use some applicative syntax here because formatNode and specNode lead to the makeLink call but doing some slighty rearrangement of the code would probably not buy us very much in terms of additional error reporting capabilities.

At the end, I add the properties to the created neo4j Relationship then return the relationship. Since the relationship object is really a ValidationNel[String, Relationship], I'm all set. In the caller function, I can use pattern matching or a simple boolean check (ValidationNel[String, Relationship].isFailure) to print out error messages and terminate the program or do something appropriate for the application.

###Other Links

Here's some links that describe why Validation is not a Monad and does not accumulate the messages in for comprehensions. Since each line of our for-comprehension example above was dependent on the first and we were using a fail-fast model, this issue was not a problem. We'll cover the other forms you can use in another post of course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.