Idiomatic clojure map lookup by keyword

Say I have a clojure map that uses keywords as its keys:

(def my-car {:color "candy-apple red" :horsepower 450})

I know that I can look up the value associated with the keyword by either using the keyword or the map as a function and the other as its argument:

(my-car :color)
; => "candy-apple red"
(:color my-car)
; => "candy-apple red"

I realize that both forms can come in handy for certain situations, but is one of them considered more idiomatic for straightforward usage like shown above?


(:color my-car) is fairly standard. There are a few reasons for this, and I won't go into all of them. But here's an example.

Because :color is a constant, and my-car is not, hotspot can completely inline the dynamic dispatch of color.invoke(m), which it can't do with m.invoke(color) (in some java pseudo-code).

That gets even better if my-car happens to sometimes be a record with a color field instead of a plain map: the clojure compiler can emit code to check "hey, if my-car is an instance of CarType, then just return my-car.color; otherwise do all the complicated, slow, hashmap lookup."


From the library coding standards:

  • Use keyword-first syntax to access properties on objects:

    (:property object-like-map)
    
  • Use collection-first syntax to extract values from a collection (or use get if the collection might be nil).

    (collection-like-map key)
    (get collection-like-map key)
    

I put together a list of arguments for and against the two forms. (Edit: Added third option - (get map :key) which is my new favorite despite being a little bit more verbose)

Arguments for (:key map)

1) Requested in coding standards

http://dev.clojure.org/display/community/Library+Coding+Standards

2) Still works when map is nil

> (:a nil)
  nil
> (nil :a)
  ERROR: can't call nil

---counterargument--- if key may be nil, other forms are better

> ({:a "b"} nil)
  nil
> (nil {:a "b"})
  ERROR: can't call nil

3) Works better for threading and mapping over collections of objects

(-> my-map
  :alpha
  fn-on-alpha
  :beta
  fn-on-beta
  :gamma

> (def map-collection '({:key "values"} {:key "in"} {:key "collection"}))
> (map :key map-collection)
  ("values" "in" "collection")

---counterargument--- the code structure of threading is different than usual so different idiomatic tendencies could be applied for map access when needed

4) Potential optimization benefit? (needs verification)

Arguments for (map :key)

1) Does not throw error when key is non-keyword or nil

> ({:a "b"} nil)
  nil
> (nil {:a "b"})
  ERROR: can't call nil
> ({"a" "b"} "a")
  "b"
> ("a" {"a" "b"})
  ERROR: string cannot be cast to IFn

2) Consistency with list access in Clojure

> ([:a :b :c] 1)
  :b
> (1 [:a :b :c])
  ERROR: long cannot be cast to IFn

3) Similarity to other forms of object access

java>         my_obj  .alpha  .beta  .gamma  .delta
clj >     ((((my-map  :alpha) :beta) :gamma) :delta)
clj > (get-in my-map [:alpha  :beta  :gamma  :delta])
cljs> (aget   js-obj  "alpha" "beta" "gamma" "delta")

4) Alignment when accessesing multiple keys from the same map (separate lines)

> (my-func
    (my-map :un)
    (my-map :deux)
    (my-map :trois)
    (my-map :quatre)
    (my-map :cinq))
> (my-func
    (:un my-map)
    (:deux my-map)
    (:trois my-map)
    (:quatre my-map)
    (:cinq my-map))

---counterargument--- alignment worse when accessing same key from multiple maps

> (my-func
    (:key map-un)
    (:key map-deux)
    (:key map-trois)
    (:key map-quatre)
    (:key map-cinq)
> (my-func
    (map-un :key)
    (map-deux :key)
    (map-trois :key)
    (map-quatre :key)
    (map-cinq :key)

Arguments for (get map :key)

1) NEVER causes error if arg1 is map/vector/nil and arg2 is key/index/nil

> (get nil :a)
  nil
> (get nil nil)
  nil
> (get {:a "b"} nil)
  nil
> (get {:a "b"} :q)
  nil
> (get [:a :b :c] nil)
  nil
> (get [:a :b :c] 5)
  nil

2) Consistency in form with other Clojure functions

> (get {:a "b"} :a)
  :b
> (contains? {:a "b"} :a)
  true
> (nth [:a :b :c] 1)
  :b
> (conj [:a :b] :c)
  [:a :b :c]

3) Alignment benefits of map-first

> (my-func
    (get my-map :un)
    (get my-map :deux)
    (get my-map :trois)
    (get my-map :quatre)
    (get my-map :cinq))

4) Get-in can be used for nested access with a single call

> (get-in my-map [:alpha  :beta  :gamma  :delta])
> (aget   js-obj  "alpha" "beta" "gamma" "delta")

Source: testing on http://tryclj.com/