Sometimes you want the benefits of a dynamically typed language with some of the benefits of a statically typed one.
Many languages these days encourage the user to use dictionaries or maps as a kind of loosely typed record.
These languages often differ on how they treat the situation where you look up a nonexistent key in the map.
That's because they have to conflate two different purposes.
The traditional data-structurish use of maps, where the keys are usually calculated, and a nonexistent one could conceivably be valid. The classical example is a word count program. If a program looks for an unknown key in the word count map, it's reasonable to just return
null
, because the result will clearly be identical tonil
.The record-like use of maps, where if you look up
:aghe
in a map with has keys:age
, it's obvious a programming error has occurred.
This distinction is roughly related to the (rather outdated now)
Exception
/RuntimeException
distinction in Java. Although the checked exception
feature was such a horrible pain that many now derive from RuntimeException, the
previous notion was that you avoided catching RuntimeException. The
RuntimeException
should kill your program, or at least the handling thread.
In the case of example 2, it's clear that (get mymap :aghe)
can kill the
program. However, in Clojure that expression will return nil
. At some other
time your code will get a wrong result. By that time, however, the stack trace
may be useless.
strict-get
will cause your code to break when you access a nonexistent field.
This function is also known as grab
in the tupelo
library.
(defn grab [key_ map_]
(let [val_ (get-in map_ [key_] ::not-found)]
(if (= val_ ::not-found)
(do
(debugp "input was" map_)
(throw (IllegalArgumentException.
(format "unable to grab key '%s' from map" key_))))
val_)))
(defn strict-get [map_ key_]
(grab key_ map_))
(defn strict-get-in [map_ path]
(have! sequential? path)
(reduce strict-get map_ path))
debugp
is a personal debugging macro, feel free to replace it as you wish.
Also, have!
is an assertion from the truss
library.
Personally, I'd love for this to become a Clojure idiom, I find it incredibly
useful, especially when first writing a new piece of functionality. It's
important to remember, however, that you always need to think about your
intention when you do a map lookup. For me, strict-get
is a good default, but
I still use get
, :foo
, and all the others in all their various permutations.
I wrote this macro that's a simplistic version of map destructuring, but using
strict-get
instead:
(defn generate-strict-get [keyword input-sym]
`(strict-get ~input-sym ~keyword))
(defn generate-let-pair [binding-sym keyword input-sym]
`[~binding-sym ~(generate-strict-get keyword input-sym)])
(defn generate-bindings-vector [bindings input-sym]
(->> (map (fn <span class="createlink">k v</span>
(generate-let-pair k v input-sym)) bindings)
(mapcat identity)
vec))
;; (strict-ds-bind {foo :bar
;; baz :quux} input
;; expr)
(defmacro strict-ds-bind [bindings input & expr]
`(let ~(generate-bindings-vector bindings input)
~@expr))
When you use this form, it's obvious what's happening, and the reader knows that
there's a chance that the continuation may throw at this stage. If you want to
get this to indent the body properly, you can use this snippet in your .emacs
.
(define-clojure-indent
(strict-ds-bind 2))
Thanks to this Stackoverflow
question
for the main inspiration. Apparently prismatic/plumbing
also calls this
safe-get
.
I find this so useful that I find myself writing it in JavaScript as well, YMMV
though. Python is the only mainstream language that works like this by default,
as far as I know. (Update 2019: C++'s at
method from std::map
also works
like this.)