Introduction
During a recent code review, a coworker who had the good fortune of jumping straight from Java to Clojure was asking me a few questions about some Scala code she'd been asked to edit. The particular problem involved recursive accumulation of some list data and our conversation drifted towards some questions regarding the different-yet-similar-sounding terms in these JVM languages. Java, Scala, and Clojure together have Null, Nil, nil, Some, and None. Most of these are related in their respective languages to how exceptional behaviors are handled with respect to references and/or collections. Here I explain each of these from the perspective of their host languages and my opinion of the effectiveness of each solution.
Before diving in, let me be clear that null exists in all JVM languages, including Scala and Clojure. However, these and other more modern languages provide additional facilities for dealing with situations where Java would default to using null and/or throw NPEs.
During a recent code review, a coworker who had the good fortune of jumping straight from Java to Clojure was asking me a few questions about some Scala code she'd been asked to edit. The particular problem involved recursive accumulation of some list data and our conversation drifted towards some questions regarding the different-yet-similar-sounding terms in these JVM languages. Java, Scala, and Clojure together have Null, Nil, nil, Some, and None. Most of these are related in their respective languages to how exceptional behaviors are handled with respect to references and/or collections. Here I explain each of these from the perspective of their host languages and my opinion of the effectiveness of each solution.
Before diving in, let me be clear that null exists in all JVM languages, including Scala and Clojure. However, these and other more modern languages provide additional facilities for dealing with situations where Java would default to using null and/or throw NPEs.
In Java, you may encounter null when using an uninitialized reference (the default value for all references is null), when attempting to get a collection element that doesn't exist, or when someone decides to return null from their function to indicate a bad result. Java has no special handling for null. If you try doing anything with a null value, you get a NullPointerException and all is lost.
Not only is trying to do something with null exceptionally bad, Java (pre 8) has no built-in mechanisms to help you out. You just do lots of null checking.
Here are some methods for dealing with null in Java:
Not only is trying to do something with null exceptionally bad, Java (pre 8) has no built-in mechanisms to help you out. You just do lots of null checking.
Here are some methods for dealing with null in Java:
- Always check your references. Liberally spread if(foo != null){ /*Do stuff with foo here*/ } everywhere you don't have absolute control of your references and aren't absolutely sure foo is initialized.
- Always initialize your references at declaration or (preferably) make them final. Effective Java Item #15 says "Minimize Mutability." I find it very hard to be effective when writing Java, but this is definitely a good tip. Whenever possible, make everything final with a meaningful (i.e. non-null) value.
- Don't write your code in Java. There are some perfectly good alternatives (See below).
- Use the new Optional class from Java 8. This is the similar to Scala's Option type so I won't say much here aside from saying that Scala had it first. There's a trend there.
Scala
Recognizing the evils of null and NullPointerExceptions, Scala designed a better way - the Option type. Rather than returning null when something goes bad, the Scala way is to return either Some value if the computation succeeded or None if it failed. Here is an example of how you might use Option types to model a random food grab into your refrigerator:
If you made a call to randomFood you could then call isDefined or isEmpty on the result to see if a meaningful result was returned. If the result is meaningful you can then call get to retrieve the value stored in the Some. In reality, nobody ever does this because it isn't any better than performing a null check. The only time you might do this is when calling Scala from Java because Java doesn't understand Scala functions.
What people really do with Options is use the fact that Scala implicity converts them to Iterables to perform functional operations on them. You can use functions on Options like map, foreach, flatten, reduce, and so on. Here's an example:
Scala also has Nil, which is the empty list. Nil is the odd man out in this post as it is a specific instance of an empty collection rather than a construct for handling exceptional behavior, especially with respect to null. Due to Scala's powerful type system and inferencing, the Scala compiler will figure out what kind of list you are working with as soon as you add some items to your list.
As you can see in the last line, Nil can't infer a type when no elements of any type are provided. Sometimes using empty collections like this in Scala can cause problems in which the compiler can't infer the type information. In such cases, you can just declare the type when declaring the def or val (we don't use vars). Here's an example that illustrates the point:
As can be seen from the example, using operations such as map and getOrElse are convenient ways to transform Optional types to desired outputs. At the same time, care must be taken to make sure type information isn't lost so that you get the expected result. Despite these potential pitfalls, Scala's ability to treat Options as Iterables makes dealing with them very convenient.
Overall, Option is a much better solution than dealing with frequent null checking.
/** * A really lame function that might return a random food. */ def randomFood = math.random match { case x if x > 0.75 => Some("Ham") case x if x > 0.5 => Some("Cheese") case x if x > 0.25 => Some("Bacon") case _ => None }
What people really do with Options is use the fact that Scala implicity converts them to Iterables to perform functional operations on them. You can use functions on Options like map, foreach, flatten, reduce, and so on. Here's an example:
val sandwich = Some("Bread") :: ((0 until 10) map { _ => randomFood }).toList ::: Some("Bread") :: Nil //Prints something like List(Some(Bread), Some(Ham), Some(Ham), None, Some(Bacon), Some(Bacon), Some(Ham), None, Some(Ham), Some(Bacon), None, Some(Bread)) println(sandwich) //Prints something like List(Bread, Ham, Ham, Bacon, Bacon, Ham, Ham, Bacon, Bread) println(sandwich.flatten)
scala> val x = 1 :: 2 :: 3 :: Nil x: List[Int] = List(1, 2, 3) scala> Nil res0: scala.collection.immutable.Nil.type = List()
scala> Some(4) map { x => x * x } getOrElse 0 res0: Int = 16 scala> Some(4.0) map { x => x * x } getOrElse 0 res1: AnyVal = 16.0 scala> None map { x => x * x } getOrElse 0 <console>:8: error: value * is not a member of Nothing None map { x => x * x } getOrElse 0 ^ scala> val n : Option[Double] = None n: Option[Double] = None scala> n map { x => x * x } getOrElse 0 res3: AnyVal = 0 scala> n map { x => x * x } getOrElse 0.0 res4: Double = 0.0
Overall, Option is a much better solution than dealing with frequent null checking.
Clojure
Being hosted on the JVM, Clojure's nil is identical to Java's null. However, how Clojure treats nil is very different from Java. Rather than exploding every time nil is used a la Java or adding special features to handle exceptional behavior a la Scala, Clojure has a few simple rules that make it easy to work with nil. First, in logical expressions nil logically evaluates to false (along with false itself). In Clojure we call this "truthiness" and it is awesome. Second, when dealing with collections nil is treated like an empty list. These simple rules result in a simple, elegant solution to the dreaded null problem and it also results in a lot less code. To illustrate, here's our random food grab function in Clojure:
As you can see, I don't do anything special (No Some/None construct) and just return nil (which is null in Java). It is the simplest solution, which is what Clojure is all about. In Java, this solution would be error-prone since it is using null. Here's how it works in Clojure:
As with Scala, Clojure provides a better way to handle null than Java. In contrast to Scala, Clojure does not add any special classes to handle null. Rather, it uses slightly different rules so that nil has meaning in collection and logical operations. For more on nil in Clojure, read about "nil punning" here.
Hands down, I prefer Clojure's handling of nil to either Scala or Java's approach.
Conclusion
Null is a unavoidable fact of life. Sometimes things go wrong and the best answer is no answer. How this situation is dealt with depends very much on the language you are working in. Java, realizing that null is bad, throws lots of exceptions that you get to deal with, usually with lots of error checking. Scala provides a mechanism to return something (Some value) or nothing (None) that allows you to more easily deal with situations that might go bad. Clojure has a few rules for how to deal with nil, primarily the concepts of "truthiness" and treating nil like an empty list. The rules pretty much solve all your problems when it comes to nil.
These approaches shed light on the designs of these languages, especially the more recent ones. Scala is, in my opinion, a complicated language that solves many problems via additional mechanisms to deal with the problems inherited from Java. I still think it is a great language, but it does take a decidedly baroque approach to solving problems. Clojure, despite seeming otherworldly at first, is a truly simple language. The more I use it, the more impressed I am with its simple design and philosophy. Either way, I think you'll find either of these language's solutions to the null problem preferable to Java's.
(defn rand-food [] (condp < (rand) 0.75 "Ham" 0.5 "Cheese" 0.25 "Bacon" nil))
(let[food (take 10 (repeatedly rand-food)) layers (filter identity food)] (conj (into ["Bread"] layers) "Bread")) => ["Bread" "Ham" "Bacon" "Cheese" "Ham" "Bacon" "Ham" "Bacon" "Bacon" "Ham" "Ham" "Bread"]
Here's another snippet of examples that show how Clojure deals with nil when it is placed in the position of a null argument or collection. Compare or in Clojure with getOrElse in Scala. Much simpler.
(reduce + (map #(* % %) [1 2 3])) ;A non-nil example => 14 (reduce + (map #(* % %) nil)) ;I can reduce into a nil collection => 0 (or nil 4) ;Compare to getOrElse in Scala => 4 (and nil 4) ;Both items must exist => nil (conj nil 4) ;I can conjoin 4 to nil to create the list (4) => (4)
Hands down, I prefer Clojure's handling of nil to either Scala or Java's approach.
Conclusion
Null is a unavoidable fact of life. Sometimes things go wrong and the best answer is no answer. How this situation is dealt with depends very much on the language you are working in. Java, realizing that null is bad, throws lots of exceptions that you get to deal with, usually with lots of error checking. Scala provides a mechanism to return something (Some value) or nothing (None) that allows you to more easily deal with situations that might go bad. Clojure has a few rules for how to deal with nil, primarily the concepts of "truthiness" and treating nil like an empty list. The rules pretty much solve all your problems when it comes to nil.
These approaches shed light on the designs of these languages, especially the more recent ones. Scala is, in my opinion, a complicated language that solves many problems via additional mechanisms to deal with the problems inherited from Java. I still think it is a great language, but it does take a decidedly baroque approach to solving problems. Clojure, despite seeming otherworldly at first, is a truly simple language. The more I use it, the more impressed I am with its simple design and philosophy. Either way, I think you'll find either of these language's solutions to the null problem preferable to Java's.
No comments:
Post a Comment