Welcome! Please see the About page for a little more info on how this works.

+2 votes
in Collections by

Why does (conj) returns vector but (conj nil 1) returns list and (conj nil) returns nil?

;=> []
(conj nil)
;=> nil
(conj nil 1)
;=> (1)

It seems a little inconsistent no?

1 Answer

+3 votes
edited by
Best answer
  • (conj nil 1) returns an empty list, because nil and empty list are often conceptually treated the same by Clojure. So conjoining on nil is like asking to conjoin on empty list.

  • (conj nil) returns nil because conj is a transducer function as well, and the 1-ary for the transducer of conj has to be the identity function. Thus the 1-ary of conj is the identity function and will always just return the passed argument as-is, so if you pass nil you get nil back, if you pass in :foo you get :foo back: (conj :foo) ;=> :foo

  • (conj) the 0-arity was also added when conj was made a transducer and is the init arity for transducers. Thus it was probably deemed that when used with transduce, vector would be a better default, as most would expect that: (transduce (map inc) conj [1 2 3]) would return 2 3 4 and not 4 3 2

Initially conj only had a 2-arity: (conj nil 1) or (conj [1 2] 3) or (conj '() 1). This was in the time before transducers, and conj was exclusively a collection function. From that perspective, the question was, what should (conj nil 1) return? And the logical answer was, we should probably treat nil as the empty list, so nil should default to an empty list and thus (conj nil 1) ;=> (1) was made to return a list.

I believe this decision was due to initially Clojure copying a lot of the Common Lisp idioms. In Common Lisp, there is no such thing as an empty list, when a list is empty it is nil. Thus nil is the empty list. Initially, Clojure copied this idiom, but later evolved to distance itself slowly from some of the Common Lisp idioms.

Such a later evolution is the evolution of conj to also be a transducer, which meant that a 0-arity and a 1-arity were added to it. The 1-ary in transducers is the completion function, it says what to do at the end of applying a transducer, and in the case of conj we don't need to do anything except return the result, thus the 1-ary of conj just returns its argument as is, aka it's an identity function.

Now the 0-ary in transducers is the init function, this will be called when there is no starting collection in order to decide what the default is, and it could be used to setup some other things related to the particular transducer. Now the question here was again: what should be the default when conj is used in a transducer with no starting collection specified? In this case it was decided that would be vector, and I believe this is simply because as Clojure evolved like I said before, it started to change some of its idioms away from Common Lisp, one of those being that in Clojure vectors tend to just be more useful, and defaulting to them allow for the more intuitive insertion order, so when you process a collection as a vector, you get your elements back in their original order which is just more often what you want.

"Initially, Clojure copied this idiom [of nil being the empty list], but later evolved to distance itself slowly from some of the Common Lisp idioms."

I cannot find any evidence to support this statement.  I can find statements from Rich Hickey's talk "Clojure for Lisp Programmers" that say explicitly otherwise, e.g.:

"But nil means nothing in Clojure. It means you have nothing. It does not mean some magic name that also means the empty list. Because there is empty vectors, and there is empty all kinds of things. "nil" means you do not have something. So either you have it, a sequence, or you have nothing, nil."

also this quote from the same talk: "Clojure has nil. nil means nothing. You do not have anything. It is not the name of some magic thing. It means nothing. Common Lisp has nil and the empty list, and those notions are unified. It might have made sense, because you really only had one primary aggregate data structure. I do not, right? Why should nil be the empty list? Why should not it be the empty vector, or the empty whatever? There is no good reason."

Source: https://github.com/matthiasn/talk-transcripts/blob/master/Hickey_Rich/ClojureIntroForLispProgrammers.md