1

I am wondering how to update two values at the same time. For instance, I want to increase month and decrease age at the same time. My code is

user=>(def users {:month 1 :age 26})
user=>(update-in users [:month :age] inc dec)

I know this syntax is not correct, but how can I fix this? And I need to update at the same time. Since if I update month first and then update age, then the first map will lost when I update the second map. Or is there other way to figure out this problem?

octopusgrabbus
  • 10,555
  • 15
  • 68
  • 131
Xiufen Xu
  • 531
  • 1
  • 3
  • 19
  • 1
    Hi leeor! Actually it should be atomic, but I don't think it is necessary to use ref with dosync to solve this problem. Thank you so much! – Xiufen Xu Apr 26 '16 at 19:24
  • 3
    because clojure data structures are immutable it it completely impossible for the data to change between these two calls. In Clojure any sequence of operations on immutable collections **is** atomic. – Arthur Ulfeldt Apr 26 '16 at 20:19

3 Answers3

5

update does not modify a value, it just returns a new value, so it's just a function. If you need to update 2 fields of a map, the straightforward way to do that is just call update twice, first on the original map and then on the result of the first update:

(defn update-month-and-age [user]
  (update (update user :month inc) :age dec))

Which looks nicer using ->:

(defn update-month-and-age [user]
  (-> user
      (update :month inc)
      (update :age dec)))
Sam Estep
  • 12,974
  • 2
  • 37
  • 75
Joost Diepenmaat
  • 17,633
  • 3
  • 44
  • 53
  • I agree with your first solution and it does work. But for the second one, I know you want to show me it is nicer to use the arrow sign, but this method does not work for this problem. Since the first update will not be saved when you update the second one. The result for the first method is `{:month 2 :age 25}`. But the result for the second one is `{:month 1 :age 25}`. Thank you for your help! – Xiufen Xu Apr 26 '16 at 19:22
  • 2
    @XiufenXu No, that's not correct. `(update-month-and-age {:month 1 :age 26})` returns `{:month 2 :age 25}` for both implementations. – Sam Estep Apr 26 '16 at 19:52
  • 1
    `user> (macroexpand-1 '(-> user (update :month inc) (update :age dec))) => (update (update user :month inc) :age dec)` – Arthur Ulfeldt Apr 26 '16 at 20:17
  • @Elogent Thank you so much! It was my fault, I write something wrong when I use your code. Thanks a lot – Xiufen Xu Apr 26 '16 at 22:46
  • @ArthurUlfeldt Thank you so much! Your macroexpand is very clear and it is more eaiser for me to understand. – Xiufen Xu Apr 26 '16 at 22:47
2

in this simple case (update functions without additional params) you could also do it like this:

user> (def users {:month 1 :age 26 :records [1 2 3]})
#'user/users

user> (reduce-kv update users {:month inc :age dec :records reverse})
{:month 2, :age 25, :records (3 2 1)}

with additional params it would be a little bit more verbose:

user> (reduce-kv (partial apply update)
                 users
                 {:month [+ 2] :age [dec] :records [conj 101]})
{:month 3, :age 25, :records [1 2 3 101]}

well, it is still worse then simple usage of threading macro.

leetwinski
  • 17,408
  • 2
  • 18
  • 42
0

I'm tempted to solve this problem generally, by writing a function that takes a map: keyword -> function, and turns out a function that applies all the functions to the relevant record/map entries.

Such a function is

(defn field-updater [fn-map]
  (fn [record]
    (reduce
     (fn [m [k f]] (assoc m k (f (m k))))
     record
     fn-map)))

And we use it to generate your required field updater:

(def update-in-users (field-updater {:month inc :age dec}))

... which we can then apply

(update-in-users users)
;{:month 2, :age 25}
Thumbnail
  • 13,293
  • 2
  • 29
  • 37