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/