Null pointers are considered by their inventor to be a huge mistake. Clojure inherits its null pointer, called
nil in Clojure, from the JVM. In contrast to Java1, Clojure seems to embrace the null pointer. In this post, I'd like to explore how Clojure uses the null pointer in what is often called nil-punning.
Nil-punning has its roots in the very first Lisps, where
nil was both false (the boolean value) and the end of a list (the empty list). It was also often used to represent "no answer", as in what is the first element of an empty list. It is called punning because you can use it to mean different things in different contexts.2
nil, as a value, is nearly void of meaning. And it is all pervasive, because it can be returned from any Clojure function or Java method.
Let's go through that last part bit by bit.
- It is a value. Java made the mistake of making
nulla lack of object even though it was pointed to by an object pointer. You can't call methods on it. It is not an object. It has a weird nameless type. Clojure did not make this mistake. It is a first-class value and type3, meaning it can be compared to other values, it can implement protocols, and be used as the key or value of a map, etc. Using
nilwhere it doesn't make sense in Clojure code is usually a type error, not a NullPointerException, just as using a number as a function is a type error.
- It is nearly void of meaning. It means "no answer", but not much more. Because of this, it can take whatever form fits the context. With proper wisdom in choosing what form it takes,
nilcan become an asset instead of a liability. Clojure takes nil-punning to an extreme.
nil can be many things. To name but a few,
false as a boolean. It plays the empty
seq as a
seq4. It plays the empty map as a map. Because
nil has a role to play in most of the major abstractions of core Clojure, it rarely leads you into an error situation. An unexpected nil can surprise a good programmer, just as much as an unexpected
Nothing from a Haskell function can bewilder even the most experienced Haskeller. 5 Finding out where a
nil came from is the hardest problem that
- It is all-pervasive.
nils are normal parts of Clojure programs. They are not anomalous as in Java, where you often have to check it everywhere. This means it is always on the experienced programmer's mind.
nils flow like water through s-expressions.
first has nothing to return if the seq is empty, and so it returns
nil is a seq,
first works on
nil, and returns
rest works on
nil as well, because
nil is a seq.
These examples show the best of nil-punning. When nil-punning works right,
nils are expected and they give the expected results.
nil is everywhere, but it can be used mostly everywhere as well--without error and often with exactly the desired result. There are many abstractions that
nil does not participate in (for instance IFn, which is Clojure's interface for things that can be called like functions). In these places,
nil can present a problem--a problem of type, the same as if you tried to call a number as a function.
The best thing to do, in my experience, is simply to wrap the expression in a
(when ) to catch the
nil cases, if appropriate, while also preserving it. Otherwise, perhaps letting the Exception bubble up is the best answer. If you got a
nil where you couldn't use one, the stack trace is probably your best clue to where it came from.
After a bit of experience with Clojure, I rarely have difficult problems with out-of-place
nils in pure Clojure code. However, there is often some Java interop--namely, calling Java methods directly--that will cause a NullPointerException if the object of the method call is
nil. In these cases, wrapping a Java method call in a
(when ) is often appropriate. But sometimes not, and the NullPointerException is welcome.
There are some decisions in Clojure that I think make poor use of nil-punning. These places actually make working with Clojure more difficult than they need to be. For instance,
(str nil) is the empty string. Printing this out prints nothing--a form of silence, which is rarely what you want so you have to check for
nils in these cases. But
nil is not the empty string, like it is the empty
(clojure.string/trim nil) throws a NullPointerException. This is inconsistent behavior. When
nil acts inconsistently, nil-punning does not work right.
nils need to be checked. In the worst cases,
nils fail silently. While I have learned to deal with these situations, they are a wart on the language. The fact that
nils are so common does help surface the bugs sooner. A small consolation.
Let me make it clear: null pointers are still a costly problem in Clojure. But I can make a claim similar to what Haskellers claim about the type system: nil-punning eliminates a certain class of errors. A fortuitous set of decisions in Clojure has reduced the magnitude of the problem. And some decisions have made the problem worse by hiding it. In general, I find that by embracing nil-punning, my code gets better.
You might also like
- 3 Things Java Programmers Can Steal from Clojure
- Atom code explanation
- How I made my Clojure database tests 5x faster
- Clojure Gazette Looking Forward
I don't mean to pick on Java alone. I just wanted to be specific.↩
The type of
Clojure's core is built on several small, powerful abstractions. The most prominent abstraction is seq, which stands for sequence. seq basically has two operations,
rest. The most obvious use for them is to iterate through items of a collection. There are built-in implementations for lists, vectors, sets, hashmaps, and even strings. But anything that has a notion of sequential values can implement seq, including Java Iterators. I would also like to posit that the most important and often overlooked implementation of
Even the best Haskellers complain about not knowing where a Nothing came from.↩
You might look at this as nil-preserving behavior--much like the Nothing-preserving behavior of the Maybe Monad↩