Clojure-way - map? or try/catch

Hi Everybody :wink:

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?

Thank you.

Robert

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.

3 Likes

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.

The map? variant looks better to me as well, when compared with the exception version.


In cases like this itā€™s often possible (and fun!), depending on the end goal/whatā€™s downstream, to to one of two things:

  1. nil pun
  2. use the 3 arg version of get or the 2 arg version of applying the keyword to get a default value

Due to this contains? ends up being pretty rarely used.

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.

Thank you.

1 Like

Get over your OCD and use the things that make most sense :stuck_out_tongue:

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.

1 Like

Thatā€™s a good idea!

:grinning: 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).

1 Like

Good point.

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.

Fixed:

(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!

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.