I understand that lists and vectors in Clojure can be used almost interchangeably in most situations. Here is a simple case that surprised me
(nth [2 4] 0) ;=> 2
(nth '(2 4) 0) ;=> 2
(get [2 4] 0) ;=> 2
(get '(2 4) 0) ;=> nil -- wtf???
The documentation of get
talks mapping a key, but it works with vectors or sets just fine. The documentation of nth
even mentions get
, talking about their differences only in edge-cases.
The real-world situation where I faced this weird behavior was that I loaded a yaml file. It resulted in a nested structure of maps and lists. I wanted to access an element with get-in
.
(def form (parse-yaml some-yaml-file))
(def best-friend (get-in form [:friends 0 :first-name]))
It doesn't work, because get-in
uses get
internally. So I have a theoretical and a practical question:
get
considered correct and expected? If so, please, explain why.The behaviour of get
is correct and expected. get
works on "keyed" data structures, in which values are mapped to keys. This includes vectors, which map indices to values1, and sets2.
Lists do not provide random access to elements; they're meant to be traversed linearly. Since the supported access patterns are so different, lists and vectors are absolutely not meant to be used interchangeably and the core Clojure collections library makes no effort to support such usage. (nth
is an odd example of a function which does perform both low-performance constant-or-logarithmic-time lookups and linear traversals; a strange beast in Clojure land).
There are of course further differences to do with "modification" (in the persistent data structure sense: creating modified copies), such as the way in which conj
works and the availability of assoc
for vectors (as already mentioned in a footnote; replacing an element in a list involves rebuilding the entire prefix up to that point).
If you'd like to use vector-like access patterns with your data, you should put it in a vector. Lists can be converted to vectors (in linear time) with vec
. If you're dealing with a serialization format where it's ambiguous whether lists or vectors should be returned for some data and your parser doesn't accept an option to tell it which it should use, you might have to do some post-processing yourself (clojure.walk
might be useful, in particular the prewalk
and postwalk
functions; that's assuming only basic Clojure data types are involved).
1 In fact, more is true of vectors: they are associative, so you can use them with assoc
((assoc [0 1 2] 0 :foo)
returns [:foo 1 2]
; only indices up to (count the-vector)
are supported, for assoc
ing to indices which already exist in the vector and immediately past the end).
2 For the purposes of this discussion, sets can be considered to map their members to themselves. This is actually true in Clojure in the sense that a set used as a function returns the member itself when applied to it -- and nil
for non-members -- and also in the sense that that's what the implementation looks like under the hood.
Code example supplement to Michał Marczyk's excellent answer:
(def form
{:friends
'({:id 1, :first-name "bob"}
{:id 2, :first-name "sue"})
:languages
'({:id 1, :name "Clojure"})})
(-> form :friends (nth 0) :first-name)
;=> "bob"
(def form'
(clojure.walk/prewalk #(if (list? %) (vec %) %) form))
(get-in form' [:friends 0 :first-name])
;=> "bob"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With