Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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))
like image 733
Summitch Avatar asked Nov 22 '13 03:11

Summitch


3 Answers

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

like image 135
amalloy Avatar answered Sep 28 '22 03:09

amalloy


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
like image 44
Jacob Avatar answered Sep 28 '22 03:09

Jacob


(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))
like image 25
noisesmith Avatar answered Sep 28 '22 03:09

noisesmith