Understanding Lift's Box[T] monad
April 16th, 2010
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)
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)
}
}
There are some other overloads for syntax sugar, but essentially it lets you do:
tryo {
// your code here
}
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)
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.
6 Responses to “Understanding Lift's Box[T] monad”
Sorry, comments are closed for this article.
April 17th, 2010 at 12:40 AM
Didn’t you see that Can[A] is catamorphic to Either[Exception,Option[A]] ? Come on man!
Watch this magic:
type Can[A] = Either[Exception,Option[A]] object Full { def applyA : Can[A] = Right(Some(a)) def unapplyA = a.right getOrElse None } object Failure { def applyA : Can[A] = Left(new RuntimeException(msg)) def unapplyA = a.left } object Empty { def applyA def unapplyA : Boolean = if(a.right.toOption.isDefined) a.right.toOption.get.isDefined else false }
The rest is “trivial” -> Actually that’s the real fun… Could you get an Either[Exception,Option[A]] to work well in a for loop (similar to how you do it in lift…). I think bolting on the operators via implicits would work, but as for for-expressions… well
Perhaps catamorphic doesn’t mean “can replace with 100%’. Perhaps it just means “Cats that mutate into larger cats to fight space aliens”.
April 22nd, 2010 at 09:25 PM
Wow. Yet another option to replace five lines handling exceptions by six lines unpacking values and handling the different content you might get from it.
I’m soo impressed with all this!
April 23rd, 2010 at 11:14 AM
Nice to see you were too much of a coward to leave your name ;-)
You must misunderstand the working of Box… unpacking it is a minimal operation and in practice much more elegant than using Either[Exception,Option[A]] which would take far more lines of code than doing a simple map or openOr
April 23rd, 2010 at 01:44 PM
We don’t have a single name. We are legion ;-)
Seriously though, the problem is not ignoring the error and using another value instead. This isn’t where Box shines (because imho it doesn’t at all).
openOr says, give me the real value or let’s take this default else. You can write another function that does the same using no boxes, resulting in the same overhead (a couple of {}s and an additional word).
map says, give me what you got, no matter if it’s an error or not. Again, you can write another function that does the same using no boxes, resulting in the same overhead (a couple of {}s and an additional word).
The real problem though is that you seriously want to handle: Is this a real value? ok, then use that instead. What was the error? can I fix it? retry? Do I have to inform the user? This is a structural problem no Box[T], Either[Exception, Option[T]] or whatever can solve. It requires checking, branching, taking choices if to be done right. It cannot be designed away.
April 28th, 2010 at 08:07 PM
Full(Some(‘Doubt))) ->
You are incorrect in your definition of map (it only maps real values), but that’s a minor point.
The reason behind Box is so that you can defer the branching/checking/taking choices logic. This way I can bulk up some handling logic in one spot as opposed to having a ton of minor checks here and there. It’s the same as using exception handling, only now I can continue to pass around more state up the stack after an exception occurred. This extra state makes that recovery easier, and helps hold on to values for recovery that would otherwise require more boilerplate using pure exceptions.
You are 100% right that the branching/checking logic remains. That’s what Box (or Either[Exception,Option[A]]) helps you deal with far better than raw exceptions and functions.
April 29th, 2010 at 02:54 PM
Hrm, ok, I mixed that up with flatMap, sorry.
Seems we’re meeting somewhere in the middle. I do get the benefit of Box there, but: if you start using it all over the place, you will Box up potential errors that need a different treatment (different error causes at different places because you’re – naturally – doing different things). Which then again will blow up your central error handling (and partially not even give you the chance to recover from a transient error which you could’ve done locally).
All this leads me to be Full(Some(‘Doubt)). I guess Box can help make some error handling easier / less repetitive, but it’s no panacea. There is no panacea. (Or silver bullet, you name it).