This is my Blog.

It's full of opinions on a wide range of stuff.

Understanding Lift’s Box[T] monad

So im at Scala Days 2010 right now, and during dinner last night their seemed to be some misconceptions about what Lift's Box[T] monad actually does and how it differs from Option[T]. Now, im not computer scientist, so im not going to be discussing the algebraic validity of Box[T], not shall I be calling it a "tri state option"...

Why bother boxing values?

Have you ever thrown a NullPointerException? Frankly, I don't know any developers that haven't had this happen to them at some point because of something they hadn't considered during development... in short, a NPE is something that was caused by an unexpected series of events causing your application to explode in a variety of ways. This is not good.

Consider a scenario where I want to do something with a value returned from a database; its not uncommon to see programs where one assumes the database query got the correct result, then do some operation on it. Well, news flash, its highly plausible that the database query might not go as you had expected and return some other result....

...Boxes to the rescue! As the name might suggest, a Box is something that you can put stuff in, take stuff out of, and also find empty. Much as you would do with a real life box. Lets illustrate with a simple example:

    import net.liftweb.common.{Box,Full,Empty,Failure,ParamFailure}

scala> val x: Box[String] = Empty
x: net.liftweb.common.Box[String] = Empty

scala> val y = Full("Thing")
y: net.liftweb.common.Full[java.lang.String] = Full(Thing)

</code>

We have two simple examples here that assign new boxes to vals, and as you can see, x is assigned as a Box[String], yet its actual value is Empty which is a sub type of Box. The second assignment to y has the same type signature, but this time it is "Full" with a value; in this instance "Thing".

What about exceptions?

Lift has a helper method in net.liftweb.util.Helpers called "tryo" that is a specilized control structure so that if you can execute code in a block that returns a Box'ed value irrespective of what you did in the block and how its execution went. If your using Lift webkit then you already have access to these utility methods, however if you just want to stay light and are not using webkit then the definition of tryo looks like:

      def tryo[T](
      ignore: List[Class[_]], 
      onError: Box[Throwable => Unit])
      (f: => T): Box[T] = {
    try {
      Full(f)
    } catch {
      case c if ignore.exists(_.isAssignableFrom(c.getClass)) => 
        onError.foreach(_(c)); Empty
      case c if (ignore == null || ignore.isEmpty) => 
        onError.foreach(_(c)); Failure(c.getMessage, Full(c), Empty)
    }
  }
</code>

There are some other overloads for syntax sugar, but essentially it lets you do:

      tryo {
    // your code here
  }

</code>

So lets assume we had some remote API to invoke, and it could not connect, or blow up in some way you hadn't planned, what would happy given a tryo block / wrap? Consider:

    scala> tryo("www".toInt) 
res4: net.liftweb.common.Box[Int] = Failure(
  For input string: "www",
  Full(java.lang.NumberFormatException: For input string: "www"),
  Empty)

</code>

As "www" isnt an Int, it unsurprisingly cannot be converted to one, so it blows up with java.lang.NumberFormatException. However, using tryo (boxed values) we are left with a special Box subtype called Failure. This lets us handle the error in a concise way rather than needing to check all the possible outcomes or worry about some awful try-catch block.

scala> tryo("www".toInt) openOr 1234                       
res7: Int = 1234

scala> tryo("www".toInt).map(_.toString).openOr("Invalid Strings")
res8: java.lang.String = Invalid Strings

scala> for(x <- tryo("www".toInt)) yield x
res11: net.liftweb.common.Box[Int] = Failure(
  For input string: "www",
  Full(java.lang.NumberFormatException: For input string: "www"),
  Empty)

So you can see there several ways to interact with Box, Full and Failure subtypes, providing defaults inline and mapping the results etc.

Handling empty values?

But what if we need more information or the value we are looking for isnt a failure, its just the value is Empty (or None for scala Option)? For example, lets assume we were looking for a request parameter to a REST API or similar... rather than throwing a HTTP 500 error with no reason, it would be nice to give the user a much more granular reason. Consider getting a request paramater using Lift's S.param method.

scala> S.param("id") ?~ "You must supply an ID parameter" 
res17: net.liftweb.common.Failure = 
Failure(You must supply an ID parameter,Empty,Empty)

The ?~ method allows you to supply an error message and convert the Empty into a Failure. This is an incredibly helpful paradigm when used with for comprehensions.

Chaining boxes

Lets assume that we have a situation where by we could recive a value from several places, or we need to "fall through" to a specific result based on trying several values in a sequence... Box supports this by way of the "or" operand.

scala> val x = Empty
x: net.liftweb.common.Empty.type = Empty

scala> val y = tryo("qqq".toInt) 
y: net.liftweb.common.Box[Int] = 
  Failure(For input string: "qqq",
  Full(java.lang.NumberFormatException: For input string: "qqq"),
  Empty)

scala> x or y or Full("Default")
res18: net.liftweb.common.Box[Any] = Full(Default)

Box has a bunch of features I haven't covered here, but I hope this helps you understand the rational of this specialised option-esq type.

blog comments powered by Disqus