4
votes

This is probably straightforward, but I just can't get over it. I have a data structure that is a nested map, like this:

(def m {:1 {:1 2 :2 5 :3 10} :2 {:1 2 :2 50 :3 25} :3 {:1 42 :2 23 :3 4}})

I need to set every m[i][i]=0. This is simple in non-functional languages, but I cant make it work on Clojure. How is the idiomatic way to do so, considering that I do have a vector with every possible value? (let's call it v)

doing (map #(def m (assoc-in m [% %] 0)) v) will work, but using def inside a function on map doesn't seems right. Making m into an atomic version and using swap! seems better. But not much It also seems to be REALLY slow.

(def am (atom m))
(map #(swap! am assoc-in[% %] 0) v)

What is the best/right way to do that?

UPDATE

Some great answers over here. I've posted a follow-up question here Clojure: iterate over map of sets that is close-related, but no so much, to this one.

3
For more complicated cases, when updating deeply nested structures, Specter might be of interest: github.com/nathanmarz/specter | youtube.com/watch?v=mXZxkpX5nt8Paul

3 Answers

7
votes

You're right that it's bad form to use def inside a function. It's also bad form to use functions with side-effects (such as swap) inside map. Furthemore, map is lazy, so your map/swap attempt won't actually do anything unless it is forced with, e.g., dorun.

Now that we've covered what not to do, let's take a look at how to do it ;-).

There are several approaches you could take. Perhaps the easiest for someone coming from an imperative paradigm to start with is loop:

(defn update-m [m v]
  (loop [v' v
         m' m]
    (if (empty? v')
      ;; then (we're done => return result)
      m'
      ;; else (more to go => process next element)
      (let [i (first v')]
        (recur (rest v')                  ;; iterate over v
               (assoc-in m' [i i] 0)))))) ;; accumulate result in m'

However, loop is a relatively low-level construct within the functional paradigm. Here we can note a pattern: we are looping over the elements of v and accumulating changes in m'. This pattern is captured by the reduce function:

(defn update-m [m v]
  (reduce (fn [m' i]
            (assoc-in m' [i i] 0)) ;; accumulate changes
          m   ;; initial-value
          v)) ;; collection to loop over

This form is quite a bit shorter, because it doesn't need the boiler-plate code the loop form requires. The reduce form might not be as easy to read at first, but once you get used to functional code, it will become more natural.

Now that we have update-m, we can use it to transform maps in the program at large. For example, we can use it to swap! an atom. Following your example above:

(swap! am update-m v)
4
votes

Using def inside a function would certainly not be recommended. The OO way to look at the problem is to look inside the data structure and modify the value. The functional way to look at the problem would be to build up a new data structure that represents the old one with values changed. So we could do something like this

(into {} (map (fn [[k v]] [k (assoc v k 0)]) m))

What we're doing here is mapping over m in much the same way you did in your first example. But then instead of modifying m we return a [] tuple of key and value. This list of tuples we can then put back into a map.

2
votes

The other answers are fine, but for completeness here's a slightly shorter version using a for comprehension. I find it more readable but it's a matter of taste:

(into {} (for [[k v] m] {k (assoc v k 0)}))