Search code examples
clojureis-emptyempty-list

testing if something is an empty list


Which way should I prefer to test if an object is an empty list in Clojure? Note that I want to test just this and not if it is empty as a sequence. If it is a "lazy entity" (LazySeq, Iterate, ...) I don't want it to get realized?.

Below I give some possible tests for x.

;0
(= clojure.lang.PersistentList$EmptyList (class x))

;1
(and (list? x) (empty? x))

;2
(and (list? x) (zero? (count x)))

;3
(identical? () x)

Test 0 is a little low level and relies on "implementation details". My first version of it was (instance? clojure.lang.PersistentList$EmptyList x), which gives an IllegalAccessError. Why is that so? Shouldn't such a test be possible?

Tests 1 and 2 are higher level and more general, since list? checks if something implements IPersistentList. I guess they are slightly less efficient too. Notice that the order of the two sub-tests is important as we rely on short-circuiting.

Test 3 works under the assumption that every empty list is the same object. The tests I have done confirm this assumption but is it guaranteed to hold? Even if it is so, is it a good practice to rely on this fact?

All this may seem trivial but I was a bit puzzled not finding a completely straightforward solution (or even a built-in function) for such a simple task.


update

Perhaps I did not formulate the question very well. In retrospect, I realized that what I wanted to test was if something is a non-lazy empty sequence. The most crucial requirement for my use case is that, if it is a lazy sequence, it does not get realized, i.e. no thunk gets forced.

Using the term "list" was a little confusing. After all what is a list? If it is something concrete like PersistentList, then it is non-lazy. If it is something abstract like IPersistentList (which is what list? tests and probably the correct answer), then non-laziness is not exactly guaranteed. It just so happens that Clojure's current lazy sequence types do not implement this interface.

So first of all I need a way to test if something is a lazy sequence. The best solution I can think of right now is to use IPending to test for laziness in general:

(def lazy? (partial instance? clojure.lang.IPending))

Although there are some lazy sequence types (e.g. chunked sequences like Range and LongRange) that do not implement IPending, it seems reasonable to expect that lazy sequences implement it in general. LazySeq does so and this is what really matters in my specific use case.

Now, relying on short-circuiting to prevent realization by empty? (and to prevent giving it an unacceptable argument), we have:

(defn empty-eager-seq? [x] (and (not (lazy? x)) (seq? x) (empty? x)))

Or, if we know we are dealing with sequences like in my case, we can use the less restrictive:

(defn empty-eager? [x] (and (not (lazy? x)) (empty? x)))

Of course we can write safe tests for more general types like:

(defn empty-eager-coll? [x] (and (not (lazy? x)) (coll? x) (empty? x)))
(defn empty-eager-seqable? [x] (and (not (lazy? x)) (seqable? x) (empty? x)))

That being said, the recommended test 1 also works for my case, thanks to short-circuiting and the fact that LazySeq does not implement IPersistentList. Given this and that the question's formulation was suboptimal, I will accept Lee's succinct answer and thank Alan Thompson for his time and for the helpful mini-discussion we had with an upvote.


Solution

  • Option 0 should be avoided since it relies on a class within clojure.lang that is not part of the public API for the package: From the javadoc for clojure.lang:

    The only class considered part of the public API is IFn. All other classes should be considered implementation details.

    Option 1 uses functions from the public API and avoids iterating the entire input sequence if it is non-empty

    Option 2 iterates the entire input sequence to obtain the count which is potentially expensive.

    Option 3 does not appear to be guaranteed and can be circumvented with reflection:

    (identical? '() (.newInstance (first (.getDeclaredConstructors (class '()))) (into-array [{}])))
    
    => false
    

    Given these I'd prefer option 1.