What Is a List? The Ultimate Predicate Showdown

This post was originally written in a notebook. Check out Gorilla REPL if you haven't done it yet!

A sizeable number of Clojure developers had some Common Lisp experience in the past. When asked what the main Clojure advantages over CL are they often mention having a single equality operator (compared to Lisp's eq, eql, equal, equalp). It might come as a minor point, but in practice, it is very cognitively exhausting to keep track of which one you should use. What's, even more, jarring is that eql — not the most intuitive one — is usually the default. Can you explain with a straight face to a beginner that their string-keyed hashtable didn't work because it was created with a wrong equality operator? I never could.

But Clojure has a similar sin of its own — the multitude of list type predicates. If woken up at 3 A.M. and asked what the standard Clojure data structures are, you would likely name lists, vectors, maps, and sets. But how do you tell if the given object is a data structure of a certain type? "Well", you'd say, "there are vector?, map?, set?, and… list?? Leave me alone, man, I'm trying to catch some z's."

Gotcha! The thing is, list? is a weak predicate. It checks if the object is precisely a PersistentList, but there are plenty of things in Clojure that look like lists without being ones.

(list? (range 10)) => false

(list? (rest [1 2 3])) => false

(list? (concat '(1 2) '(3))) => false

(list? `(1 2 3)) => false

Lies everywhere! "Hold on, wildman. Every Clojure developer worth their salt should know that seq? is the way to roll." Indeed, seq? returns true in all of the examples above, while returning false on vectors. It is the very "list" predicate where you define a list as "something in round parentheses".

For convenience, I've made a table comparing all these predicates.

Expression Type list? seq? seque ntial? coll? (instance? List %)
'(1 2 3) PersistentList
(list 1 2 3) PersistentList
(range 10) LongRange
(repeat 1) Repeat
(rest [1 2 3]) ChunkedSeq
(concat '(1 2) '(3)) LazySeq
`(1 2 3) Cons
[1 2 3] PersistentVector
#{1 2 3} PersistentHashSet
{1 2, 3 4} PersistentArrayMap
(LinkedList. [1 2 3]) LinkedList
(ArrayList. [1 2 3]) ArrayList
\"123\"" String

Nothing really surprising in the results:

  • seq? should be your default list predicate
  • use list? only to check exactly for IPersistentList children
  • use sequential? to check for both lists and vectors
  • use coll? to check for any of the four main Clojure collections
  • use #(instance? java.util.List %) if you interoperate with Java code.

I hope that after reading this article, you will never again be bitten by Clojure list predicates. Hack and be merry!