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).
@cursive-ide
Copy link

Very nice.

Incidentally, using a default may not be what you want; you probably want to use or

Why is this?

@visibletrap
Copy link

@cursive-ide I think differences between line 15 and line 22 explains it

@cursive-ide
Copy link

@visibletrap Actually, I think it's between 22 and 23 - it's the handling of nil values in the map (default value will not be returned if the key exists but the value is nil, but if you use or you'll get the default either way).

@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