Accidental sparse sequences
Nothing's going to change my world.
Sequences are absolutely everywhere in clojure - hiding as functions, they're almost certainly the first thing a beginner encounters. They're a primary abstraction, not the least as your code and data. Pick up almost anything, and there wll be a sequence of it somehow.
Literal, concrete sequences are so common that we can overlook the important idiom of empty sequences - something
we almost never write by hand. I'd argue that a likely culprit of this is the ubiquity of
nil in situations where we
might otherwise have to define a concrete collection. Destructuring is a common place to see this.
nil, if you aren't familiar with clojure, is a bit special. It's a real value, can implement protocols, can be used as
a key in a map... the list goes on. Even so, it remains a source of much gnashing of teeth and NPEs. I'd love a version
of clojure that doubled down on "nil in, nil out" - but that's an argument for another day. Java interop, we see you.
Eric Normand has done a fantastic job explaining this "punning" behaviour, so if that is an intriguing topic to you, stop, click, and go read.
With that in mind, what's the big difference between
 anyway? Nil is nothing, so we're good, right?
Well, no. As we've hinted above,
nil is special, but it will still cause booms, even without getting exotic and trying
to upper case a string or so on.
Check this out:
user=> (defn foo [[a b]] (+ a b)) user=> (foo [1 2]) 3 user=> (foo nil) Execution error (NullPointerException) at user/foo (REPL:1). null
Here we have a perfectly fine, standard piece of code - destructuring a tuple argument. It cannot
under any circumstances be invoked with
nil, or we get an exception. There's sadly many other examples. Hand on heart,
it's one of the things that gets in my way most with clojure, tracking down NPEs and similar.
Let's look at a way we can mess up with a sparse sequence, by defining it in a concrete way in our code. Not an unreasonable thing to do, in this contrived example we know it's two items, so...
(let [admins [(user/primary-admin :group) (user/secondary-admin :group)]] (doseq [admin admins] (send-mail! admin)))
When this code is first written, all may be well. The functions that return the admins could either do that or throw, for example.
Let's imagine someone keen comes along later and those functions are refactored such that they return
nil instead of throwing.
Suddenly we have a sparse collection, and
doseq and its friends will happily chew through the values we provide, and invoke
send-mail! function with
nil. Now we have an exception in our mail function, sending us off on a wild goose chase in a completely wrong
direction, or worse.
Better, here, is not to do anything at all, and that's why the concrete collection in this case is arguably a smell.
(let [admins (keep (fn [f] (f :group)) [user/primary-admin user/secondary-admin])] (doseq [admin admins] (send-mail! admin)))
keep is our friend here - pretty often we want to map over something and discard
nils. Yes, you can do this with
remove and so on, but
keep is a neat idiom.
doseq with an empty collection is effectively a noop, nothing happens. This is what we want.
By discarding a concrete collection, we've not changed the original behaviour of the code, and have gained resilience and flexibility for free. There's different semantics, but if the intention is to "email all admins or none", then that's probably better expressed in an actual invariant.
As always, there are no hard and fast rules - but if you do find yourself pondering a concretely defined collection, might be worth keeping this kind of thing in mind.