I wrote a few functions that parse a nested map and returns some data from it. Nothing special except that in some cases some keys might have another map as value or just a string or number.
I quickly realized that for example:
(let [value "not map"]
(contains? value :a))
throws and I need to add a check map? like this:
(btw. I know that (get "not map" :a) doesnāt throw, but letās say I want to use contains?)
(let [value "not map"]
(if (and (map? value)
(contains? value :a))
"do something with map"
; or just return value
value))
And now it is working fine.
But later I realized I might also use try/catch, like this:
(let [value "not map"]
(try
((if (contains? value :a)
; do something with map
"abc"
"def"))
(catch IllegalArgumentException e
; or just return value
value)))
Can anybody please advise, what is the Clojure-way of solving this problem?
From a practical perspective, the exception-based variant looks to be about 100x slower. I think it is common to either add these kinds of checks since you are working in a dynamically typed language, or to leverage protocols to implement your stuff and extend them to the type(s) in question (in which type is implicitly checked and thrown if no implementation exists). Multimethods are another option.
I would reserve exceptions for when you want to communicate error/failure and force the issue.
I would almost always use the explicit test. An IllegalArgumentException typically indicates a bug in your code and should not be caught and suppressed. There may be other cases that it might be thrown that you arenāt expecting. In general with Clojure, favor explicit error handling rather than exceptions.
Thank you. I realized I can also use associative? which will make the code less static / more oriented to shape of the data, not type of the data.
Iāll remember the rule. Thank you.
Yes, I try to use nil punning as much as possible but in that specific case I needed to know if the key is there. I donāt like 3 arg version of get for testing that, I think itās more for getting default value like for example if your program is reading configuration and needs some default value if that thing hasnāt been configured yet.
Get over your OCD and use the things that make most sense
Look at the doc for get:
(get map key not-found)
The last argument is called not-found, it can be used as a sentinel or a default.
(defn get-if-map
"Gets key from value if value is a map and key is found in it, otherwise returns value as-is."
[value key]
(if (map? value)
(get value key value)
value))
Thereās no need to add an additional lookup with contains? in my opinion.
My OCD reason is that for example the following code:
(def my-map {:color :red :size 100})
; using as default
(get my-map :color :blue)
Works great. But in the following case:
; doing something when not set
(let [value (get my-map :color :not-found)]
(case value
:not-found "save new value in DB"
(str "do something with " value)))
It might fail when :color is set to :not-found. Of course there is no color of that name but if Iām dealing with external data and I need to do something differently when the key is missing (not just nil value), should I not use contains?
If the data is external, you probably ought to be validating it at the boundary of your code so that only vaguely sensible values get to the part of your code.
For example, if the color comes in as a string, a basic alphanumeric validation before you turn it into a keyword would be a good idea, as well as perhaps a length check (just keywordizing everything your given can open you up to a form of denial of service attack ā since a bad actor could just keep passing you random strings).
Then if you know the color canāt contain /, you can safely use ::not-found which will resolve to a fully-qualified keyword that matches the ns the code is in ā safe for localized uses (but donāt let it āescapeā the ns).
Nice! The color was not a great example, the field could be something more obscure with more options but If I know for sure (after validation) that the keywords canāt be namespaced, ::not-found is totally unique. Thank you.
(let [value (get my-map :color ::not-found)]
(case value
::not-found "save new value in DB"
(str "do something with " value)))
Notice the use of namespaced keys for ::not-found that way there can be no clash with actual keys in the data.
If you are paranoid somehow the data could even have a key using your namespace for some reason, you can also do:
(let [not-found 'not-found
value (get my-map :color not-found)]
(if (identical? not-found value)
"save new value in DB"
(str "do something with " value)))
Two symbols of the same name are not identical, so even if the data contained a symbol called 'not-found it would fail the identical check, because identical checks the memory location.
You can also use this trick with your sentinel being an Object, but thatās not portable to ClojureScript I would think:
(let [not-found (Object.)
value (get my-map :color not-found)]
(case value
not-found "save new value in DB"
(str "do something with " value)))
In this case you donāt even need to switch to identical?, because in Java, two objects are equal if they point to the same memory.
This is a general computer science concept by the way, called sentinel value, see here: Sentinel value - Wikipedia
Thank you, I learned something new (comparing symbols / objects). I wonāt use it immediately since ::not-found is enough for my code but Iāll keep it in mind in case I need something truly unique. Thanks!