Summitch
Summitch

Reputation: 379

Clojure: idiomatic update a map's value IF the key exists

Here's my problem: I want a function helpme that takes a map and replaces the keys :r and :g with empty vectors if and only if those keys exist. For example:


Input:

(helpme {:a "1" :r ["1" "2" "3"] :g ["4" "5"]})

Output:

{:a "1" :r [] :g []}

Input:

(helpme {:a "1" :r ["1" "2" "3"]})

Output:

{:a "1" :r []}

I can define a function "helpme" that does this, but it's overly complicated, and I feel like there must be an easier (more idiomatic) way...

Here's the overly complicated way I've done, as requested below:

(defn c [new-doc k] (if (contains? new-doc k) (assoc new-doc k []) new-doc))
(defn helpme [new-doc] (c (c new-doc :r) :g))

Upvotes: 7

Views: 5792

Answers (8)

Reut Sharabani
Reut Sharabani

Reputation: 31339

I really like the helpme API to include the keys to reset and the default value to reset to:

(defn helpme [m & {:keys [ks v]
                   :or   {ks #{:r :g}
                          v  []}}]
              (apply assoc m (-> m
                                 ;; select only existing keys by selecting from original map
                                 (select-keys ks)
                                 keys
                                 ;; generate defaults for each (handled by applying `assoc`)
                                 (interleave (repeat v)))))

This uses assocs variadic form by production the arguments to it.

If you give up the general API it can be as short as:

(defn helpme [m]
  (apply assoc m (-> m
                     (select-keys #{:r :g})
                     keys
                     (interleave (repeat [])))))

Upvotes: 0

Erez Rabih
Erez Rabih

Reputation: 15788

what about using cond-> for that purpose?

(defn helpme [m]
  (cond-> m
    (:r m) (assoc :r [])
    (:g m) (assoc :g [])))

Upvotes: 2

TheDrev
TheDrev

Reputation: 489

By testing for the expected key in the compare function

(sort-by #(if (number? (:priority %))
             (:priority %)
             (java.lang.Integer/MAX_VALUE))
          <
          [{:priority 100} {:priority 10} {:test 1}])




>({:priority 10} {:priority 100} {:test 1})

Upvotes: 0

Jacob
Jacob

Reputation: 1004

In my search for a version of update-in which only updated the map if the key actually existed, Google insisted that I could find my answer here. For others in search of the same thing I've created the following helper functions:

(defn contains-in?
  [m ks]
  (not= ::absent (get-in m ks ::absent)))

(defn update-if-contains
  [m ks f & args]
  (if (contains-in? m ks)
    (apply (partial update-in m ks f) args)
    m))

That way you could:

> (def my-data {:a {:aa "aaa"}})

> (update-if-contains my-data [:a :aa] clojure.string/upper-case)
{:a {:aa "AAA"}}

> (update-if-contains my-data [:a :aa] clojure.string/split #" ")
{:a {:aa ["a" "aa"]}}

> (update-if-contains my-data [:a :b] clojure.string/upper-case)
{:a {:aa "aaa"}} ; no change because [:a :b] didn't exist in the map

Upvotes: 8

amalloy
amalloy

Reputation: 91857

(defn helpme [m]
  (into m (for [[k _] (select-keys m [:r :g])]
            [k []])))

Short, and only requires editing in one place when the number of items to set to [] changes.

Upvotes: 9

overthink
overthink

Reputation: 24443

If this is really as simple as conditionally setting the value of two fixed keys, I'd just write it out long hand to keep it simple.

(defn clean [m]
  (let [m (if (:r m) (assoc m :r []) m)
        m (if (:g m) (assoc m :g []) m)]
    m))

If you want something more general and reusable, here's an option:

(defn cond-assoc [m & kvs]
  (reduce
    (fn [acc [k v]]
      (if (get acc k)
        (assoc acc k v)
        acc))
    m
    (partition 2 kvs)))

(cond-assoc {:a "1" :r ["1" "2" "3"] :g ["4" "5"]}
            :r []
            :g [])  ; {:r [] :a "1" :g []}

(cond-assoc {:a "1" :r ["1" "2" "3"]}
            :r []
            :g [])  ; {:r [] :a "1"}

Upvotes: 0

noisesmith
noisesmith

Reputation: 20194

(defn helpme
  [mp]
  (as-> mp m
        (or (and (contains? m :r) (assoc m :r []))
            m)
        (or (and (contains? m :g) (assoc m :g []))
            m)
        m))

if there were a third replacement, I would use this function:

(defn replace-contained [m k v] (or (and (contains? m k) (assoc m k v)) m))

as-> is new in clojure 1.5 but the definition is very simple if you are stuck using an older clojure version:

(defmacro as->
  "Binds name to expr, evaluates the first form in the lexical context
  of that binding, then binds name to that result, repeating for each
  successive form, returning the result of the last form."
  {:added "1.5"}
  [expr name & forms]
  `(let [~name ~expr
         ~@(interleave (repeat name) forms)]
     ~name))

Upvotes: 2

Derek Slager
Derek Slager

Reputation: 13841

One option:

(defn helpme [m]
  (merge m
         (apply hash-map (interleave
                          (clojure.set/intersection
                           (set (keys m)) #{:r :g})
                          (repeat [])))))

Upvotes: 0

Related Questions