Вопрос

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:

  • Is this behavior of get considered correct and expected? If so, please, explain why.
  • How can I access a nested element in such a structure of maps and lists?
Это было полезно?

Решение

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 associng 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"
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top