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
null
a 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. Usingnil
where 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,
nil
can 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 nil
s present.
- It is all-pervasive.
nil
s 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.nil
s 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, nil
s 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 nil
s 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 nil
s 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. nil
s need to be checked. In the worst cases,
nil
s fail silently. While I have learned to deal with these
situations, they are a wart on the language. The fact that nil
s 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
nil
isnil
- 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,
first
andrest
. 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 ofseq
is fornil
itself. - 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