Skip to content

Instantly share code, notes, and snippets.

@pjstadig
Created March 6, 2017 16:28
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pjstadig/25c8a775f5403a2f0e3ad94f81ef58ff to your computer and use it in GitHub Desktop.
Save pjstadig/25c8a775f5403a2f0e3ad94f81ef58ff to your computer and use it in GitHub Desktop.
In Clojure you can fetch items from a map three different ways. Which should you use when?
(ns maps.core)
;; In Clojure you can fetch items from a map three different ways. Which should
;; you use when?
(= "bar" ({:foo "bar"} :foo)) ; map as function
(= "bar" (:foo {:foo "bar"})) ; key as function
(= "bar" (get {:foo "bar"} :foo)) ; `get` as function
;; <INCIDENTALLY>
;; Incidentally, all three of these can take a default value to return if the
;; key is not present in the map.
(= "not-here" ({:foo "bar"} :psych "not-here"))
(= "not-here" (:psych {:foo "bar"} "not-here"))
(= "not-here" (get {:foo "bar"} :psych "not-here"))
;; <INCIDENTALLY>
;; Incidentally, using a default may not be what you want; you probably want to
;; use `or`:
(not= "not-here" (:psych {:psych nil} "not-here"))
(= "not-here" (or (:psych {:psych nil}) "not-here"))
;; </INCIDENTALLY>
;; </INCIDENTALLY>
;; There are two factors in choosing how to access items in a map: 1) the nature
;; of the map and/or key, and 2) the semantics of the map and key.
;; == NATURE OF THE MAP AND/OR KEY
;; Could the map be nil? You want to use either 'key as function' or '`get` as
;; function':
(let [m nil]
(m :foo))
;; => NullPointerException ...
;; Could the key be anything other than a keyword? You want to use either 'map
;; as function' or '`get` as function':
(let [k nil]
(k {:foo "bar"}))
;; => NullPointerException ...
(let [k "foo"]
(k {:foo "bar"}))
;; => ClassCastException java.lang.String cannot be cast to clojure.lang.IFn ...
;; Could either the map or the key be nil? Could the key also be not-a-keyword?
;; Use '`get` as function'.
;; == SEMANTICS OF THE MAP AND KEY
;; Is the map a 'function'? For example, is it a transform:
(let [m {"ping" "pong"}]
(get m "ping"))
;; Realizing that it is a function and treating it semantically like a function
;; will help you later when you realize you want to do some more complicated
;; transform, or provide some default. In this case you should use 'map as
;; function':
(let [ping->pong {"ping" "pong"}]
(ping->pong "ping"))
;; Now you can make `ping->pong` into an arbitrary function.
;; Is the key a 'function'? For example, is it accessing data:
(let [k :height-in-cm]
(:height-in-cm {:height-in-cm 180.34}))
;; Realizing that accessing data and calculating data are equivalent (in a
;; purely functional sense) and treating the key as a function will help you
;; later.
(let [height-in-cm (fn [m] (* 2.54 (:height-in-inches m)))]
(height-in-cm {:height-in-inches 71}))
;; Now you can make `height-in-cm` into an arbitrary function.
;; This is function punning, and Clojure allows it in many ways.
;; == CONCLUSION
;; Which method of accessing a map you choose depends on whether the map and/or
;; key could be nil (in which case don't treat either as a function), and
;; whether semantically your code wants a function (in which case treat a map or
;; a keyword as a function, but make sure that your code could take an arbitrary
;; function just as easily).
@cloojure
Copy link

cloojure commented Apr 19, 2017

I agree that using a map as a lookup function is often handy. For other uses though, I prefer to be explicit about what is occurring and avoiding the ambiguity of nil results. The Tupelo library has a function fetch-in that is like get-in but will throw for invalid search paths:

(deftest t-fetch-in
  (testing "basic usage"
    (let [map1  {:a1 "a1"
                 :a2 { :b1 "b1"
                       :b2 { :c1 "c1"
                             :c2 "c2" }}
                 nil "NIL"
                 :nil nil} ]
      (is= "a1"  (fetch-in map1 [:a1]))
      (is= "b1"  (fetch-in map1 [:a2 :b1]))
      (is= "c1"  (fetch-in map1 [:a2 :b2 :c1]))
      (is= "c2"  (fetch-in map1 [:a2 :b2 :c2]))
      (is= "NIL" (fetch-in map1 [nil]))
      (is= nil   (fetch-in map1 [:nil]))
      (throws?   (fetch-in map1 [:a9]))
      (throws?   (fetch-in map1 [:a2 :b9]))
      (throws?   (fetch-in map1 [:a2 :b2 :c9])))))

For single-key lookups, a related function grab accepts args in the "key-first, map-second" order & simply delegates to fetch-in:

(deftest t-grab
  (let [map1  {:a 1 :b 2}]
    (is= 1    (grab :a map1))
    (is= 2    (grab :b map1))
    (throws?  (grab :c map1)) ))

No fuss, no muss, no uncertainty. :)
Alan

@joshjones
Copy link

joshjones commented Jun 29, 2017

find is worth mentioning as an often-overlooked but very useful way to look up items in a map when you need to know whether or not there is indeed a key present, even if its value is nil:

(if (first (find {:a 1 :b nil} :b))
  ":b is a key in this map"
  ":b is NOT a key in this map")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment