Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic clojure map lookup by keyword

Tags:

clojure

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?

like image 278
gregspurrier Avatar asked Aug 12 '11 01:08

gregspurrier


3 Answers

(: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."

like image 191
amalloy Avatar answered Nov 19 '22 02:11

amalloy


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)
    
like image 23
kotarak Avatar answered Nov 19 '22 02:11

kotarak


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/

like image 8
Chase Sandmann Avatar answered Nov 19 '22 02:11

Chase Sandmann