Nil Punning (Or Null Pointers Considered Not So Bad)
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 Java^1, 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
In Clojure, 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 type^3, meaning it can be compared to other values, it can implement protocols, and be used as the key or value of a map, etc. Usingnilwhere 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, nil plays false as a
boolean. It plays the empty seq as a seq^4. 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 nils present.
- 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. Because nil is a seq, first works on nil, and returns
nil. ^6. 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 seq.
(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.
- I don't mean to pick on Java alone. I just wanted to be specific.
- Note that this is very different from weak typing as you find in Javascript or Python. Nil-punning is more like polymorphism.
- The type of
nilisnil - 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,
firstandrest. 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 ofseqis fornilitself. - 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
