Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Collections by

I have some code that's massaging a data structure from one format to another (SVG hiccup attributes to CLJFX attributes). The format are quite similar but when an attribute is present I need to occasionally futz with it a bit. For instance, maybe I need to (read-string ..) a string into a number, or update a keyword so it's spelled a bit different, etc.

Right now my code is of the form

(-> svg-attributes
  (update :stroke #(case %
                     nil :black
                     "none" :black
                     (if (= \# %)
                       %
                       (keyword %))))
  (update :fill #(case %
                   nil "transparent"
                   "none" "transparent"
                   (if (= \# %)
                     %
                     (keyword %))))
  (update :points #(case %
                     nil []
                     [] []
                     (map read-string (-> %
                                          (clojure.string/split #"[ ,]")))))
  (update :stroke-width #(case %
                           nil 1.0
                           %))
  (update :stroke-dasharray #(case %
                               nil []
                               (map read-string (-> %
                                                    (clojure.string/split #"[ ,]")))))
  (clojure.set/rename-keys {:stroke-dasharray :stroke-dash-array})
  (update :font-size #(case %
                        nil 10
                        "none" 10
                        %))))

The issue is that I keep having to catch the nil case and jamming in some defaults... which works most of the time.. but I'd rather skip updating when there is nothing to update.

I saw this question, which is quite similar: https://ask.clojure.org/index.php/8387/how-to-avoid-nil-values-in-maps

Is the idiomatic solution to do a (cond-> with a (some? (:somekeyword %)) on each line ? I feel like I'm not reaching for the right tool here! Maybe someone can give me a better suggestion :)

3 Answers

+1 vote
by

I've seen hand-written variations of update-if-exists in a couple of projects, here is an example implementation in medley util library.

AFAIK, no such thing exists in clojure core.

by
That's what I suspected. Thought I'd double check. Sometimes I see some map handling jujitsu and I learn a new trick :)
+1 vote
by

update-if-exists is good to know about, but it wouldn't quite hit the nail on the head here..

The primary and overriding problem with the original procedure is that it complects per-key logic and map-updating logic and thereby obscures the nature of the translation (e.g., both keys and values change) and the relation of translations to each other (whether later translations depend on earlier translations).

Separate the two concerns, and each one gets easier to improve.

To translate 1 attribute, you could have a function (a multimethod maybe), or a lookup table. For example, suppose you had a function svg->cljfx that took a [k v] (e.g., a map entry) and produced a new [k v]; then translating the whole svg-attributes could be done by

(->> svg-attributes
     (map svg->cljfx)
     (into {}))

Easy to read, although not super CPU-efficient if unchanged attributes outnumber changed attributes.

Alternatively, suppose you had a lookup table (map) of svg key to a function (maybe anonymous) that yielded a new [key value] pair. A loop could iterate the known conversions. It would have to skip missing keys, but at least that case could be covered once-and-for-all instead of once-per-key. The logic would be more involved, but it might run faster.

by
P.S.  You're using `edn/read-string`, right?  The `read-string` in core is worth avoiding, unless you really want it for its side effects.
by
Oh, mapping over the key-val pairs is clever! I'd completely forgotten you can use `map` that way! Thanks for the suggestion. I kinda like this solution, though the multimethod will be a bit verbose b/c it's just gunna be returning a key at each instance. None the less, it's cleaner than what I've got
0 votes
by

If all defaults are "values", and function is in tail position, this would do:

(defn set-defaults [v m f]
  (or (get m v) (f v)))

(defn points [s]
  (mapv read-string (clojure.string/split s #"[ ,]")))

(-> {:points    "1,2,3,4"
     :stroke    \#
     :font-size "none"}
  (update :stroke set-defaults {nil :black "none" :black \# \#} keyword)
  (update :fill set-defaults {nil "transparent" "none" "transparent" \# \#} keyword)
  (update :points set-defaults {nil [] [] []} points)
  (update :stroke-width set-defaults {nil 1.0} identity)
  (update :stroke-dasharray set-defaults {nil []} points)
  (update :font-size set-defaults {nil 10 "none" 10} identity)
  (clojure.set/rename-keys {:stroke-dasharray :stroke-dash-array}))

;=>
{:points            [1 2 3 4]
 :stroke            \#
 :font-size         10
 :fill              "transparent"
 :stroke-width      1.0
 :stroke-dash-array []}

Alternatively, if any of defaults could be a function too:

(defn set-defaults2 [v m defaultf]
  (let [f (get m v defaultf)]
    (f v)))

(-> {} 
  (update :stroke set-defaults2 {nil (constantly :black)}))

And, of course, there is a :>> "feature" in condp: https://clojuredocs.org/clojure.core/condp#example-542692cbc026201cdc326be4

...