Recently people have been talking about the Maybe monad and its myriads of implementation possibilities in Ruby. Because of its dynamic nature and powerful meta-programming facilities, Ruby allows you to write this ..
@phone = Location.find(:first, ...elided... ).andand.phone
Here
andand
is an abstraction of the Maybe monad that you can seamlessly compose with core Ruby syntax structures, effectively growing the Ruby language. Now, compare this with the Null Object design pattern in Java and C++, which is typically used in those languages to write null safe code. Abstractions like Null Object pattern encapsulate null handling logic, but do not relieve programmers from writing repetitive code structures - you need to have a Null Object defined for each of your abstractions!I have been working with Scala in recent times and really enjoying every bit of it. Professionally I belong to the Java/JVM community, and the very fact that Scala is a language for the JVM has made it easier for me. After working for a long time in enterprise projects, I have also been affiliated to the statically typed languages. I always believed that static encoding of type information on your values serve as the first level of unit testing, constraining your data to the bounds of the declared type, as early as the compile time. However, at times with Java, I also felt that complete static typing annotations adds to the visual complexity in reading a piece of code. Now working in Scala made me realize that static typing is not necessarily a cognitive burden in programming - a good type inferencing engine takes away most of the drudgery of explicit type annotations of your values.
Enough of Scala virtues, let's get going with the main topic .. Scala's Maybe monad, it's marriage to for-comprehensions and it's implicit extensibility ..
Scala offers the
Option[T]
datatype as its implementation of the monad. Suppose you have the following classes ..case class Order(lineItem: Option[LineItem])
case class LineItem(product: Option[Product])
case class Product(name: String)
and you would like to chain accessors and find out the name of the
Product
corresponding to a LineItem
of an Order
. Without drudging through boilerplate null-checkers and Baton Carriers, Scala's Option[T]
datatype and for-comprehensions offer a nice way of calling names ..def productName(maybeOrder: Option[Order]): Option[String] = {
for (val order <- maybeOrder;
val lineItem <- order.lineItem;
val product <- lineItem.product)
yield product.name
}
The example demonstrates a very powerful abstraction in Scala, the for-comprehensions, which works literally on anything that implements
map
, flatMap
and filter
methods. This is also an illustration of how for-comprehensions in Scala blends beautifully with its implementation of the Maybe monad (Option[T]
), leading to concise and powerful programming idioms. I think this is what Paul Graham calls orthogonal language in his On Lisp book.David Pollack implements similar usage of the Maybe monad in his Lift framework. Here is a snippet from his own blog ..
def confirmDelete {
(for (val id <- param("id"); // get the ID
val user <- User.find(id)) // find the user
yield {
user.delete_!
notice("User deleted")
redirectTo("/simple/index.html")
}) getOrElse {error("User not found"); redirectTo("/simple/index.html")}
}
The
find
method on the model class and the param
method that extracts from the request, all return Option[T]
- hence we do not need any explicit guard to check for null id or "user not found" checks.In the first example, if the method
productName()
gets any null component in the path of accessing the name, it returns None
(as it should be). What if I would like to know which part is None
as part of the diagnostic message ? Just pimp my Option !
class RichOption[T](value: Option[T]) {
def ifNone(message: String) = {
value.getOrElse(error(message))
}
}
and add the
implicit
conversion ..implicit def enrichOption[T](opt: Option[T]): RichOption[T]
= new RichOption(opt)
Now the method becomes ..
def productName(maybeOrder: Option[Order]): Option[String] = {
try {
val nm = maybeOrder.ifNone("null order")
.lineItem.ifNone("null line item")
.product.ifNone("null product")
.name
if (nm == null)
None
else
Some(nm)
} catch {
case e => println(e.toString()); None
}
}
and the method invocation reports with meaningful messages in case of null accesses.
8 comments:
Just a note on the for-syntax, you don't need to use 'val', and if you use braces instead of parantheses you can skip the semi-colons:
def productName(maybeOrder: Option[Order]): Option[String] =
for {
order <- maybeOrder
lineItem <- order.lineItem
product <- lineItem.product
} yield product.name
Debasish,
Is it correct to say that the Option class is a glorified implementation of the Null Object pattern ?
Thanks
X
@rickynils:
Thanks .. I am only learning Scala.
@xanana:
reminds me of the generalization of Greenspun's 10th rule, once again .. "Any sufficiently complicated platform contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of a functional programming language."
As I mentioned in the post, Null Object pattern has the same objective, and when implemented in a language like Java or C++ leads to lots of boilerplates and repetitive code. OTOH, monads are true abstractions that can be composed together with your core language leading to more powerful abstractions.
I would also write the for as rickynils suggested. Another option is to write the function like this (i'm not sure if I prefer it though):
def productName(maybeOrder: Option[Order]) =
maybeOrder flatMap (_.lineItem) flatMap (_.product) map (_.name)
This should be pretty close to what the for {} expression is translated to by the compiler. But I think it's somewhat difficult to remember where to use 'map' and where 'flatMap' (flatMap "flattens" the options, but several maps would result in something like Option[Option[Option[String]]] )
@Debasish,
The Option class seems to be more verbose than the Option type feature found in Nice.
@xanana:
Sure, Nice has nullable types (?Type), which are more concise. However, Option[T] in Scala being a monad (and implemented as a case class) can be composed more easily. You can manipulate it's behavior (as I have shown, using implicits) and make it more malleable. Also, Groovy's safe operator order?.lineitem?.product?.name is a good concise implementation of the same concept.
@villane:
Yeah .. for-comprehensions are a syntactic sugar for the flatmap/map combinations that u mention. I prefer to use the for-comprehensions though. It is nice and intuitive, having less accidental complexity and more intention-revealing.
+1 for Groovy's safe operator. Would be nice if scala would have something similar. The Scala option system is indeed more powerful / felxible, but is overkill and verbose for the ~90% use case.
Post a Comment