nha
nha

Reputation: 18005

clojure swap with arbitrary return value

Is there a way to swap! and return an arbitrary value along with the value of the atom?

For instance:

(swap! (atom {1 1 2 2 3 3 4 4 5 5})
         (fn [m]
           (loop []
             (let [i (rand-int 10)]
               (if (contains? m i)
                 (recur)
                 (assoc m i "ok"))))))

In the above function I have no way to know which key was added to the map (unless I list them beforehand).

I could use another atom to store the result, but it there something simpler I am overlooking?

Upvotes: 1

Views: 336

Answers (2)

ChrisBlom
ChrisBlom

Reputation: 1281

The easiest way is to just put both the return value and the result in a map:

(swap! (atom {:value {1 1 2 2 3 3 4 4 5 5}
              :return nil})
       (fn [{:keys [value]}]
           (loop []
             (let [i (rand-int 10)]
               (if (contains? value i)
                 (recur)
                 {:value (assoc value i "ok")
                  :return i})))))

Note that your example function is not a pure function (because it uses rand-int), so it is not a correct usage of swap!

Upvotes: 1

Taylor Wood
Taylor Wood

Reputation: 16194

Here's a weird but general approach:

(defn swap-diff! [atom f & args]
  (loop []
    (let [curr-value @atom
          new-value (apply f curr-value args)]
      (if (compare-and-set! atom curr-value new-value)
        [new-value (data/diff curr-value new-value)]
        (recur)))))

This uses compare-and-set! in a loop until it succeeds, similar to how swap! works internally. When it succeeds, it returns a tuple of the new value and the output of clojure.data/diff. The diff output will show you exactly how the atom's value changed.

(swap-diff! (atom {1 1, 2 2, 3 3})
            #(loop []
               (let [i (rand-int 10)]
                 (if (contains? % i)
                   (recur)
                   (assoc % i "ok")))))
=> [{1 1, 2 2, 3 3, 9 "ok"} (nil {9 "ok"} {3 3, 2 2, 1 1})]

{1 1, 2 2, 3 3, 9 "ok"} is the atom's new value. (nil {9 "ok"} {3 3, 2 2, 1 1}) is the output of diff, where the first item are items only in the old value, the second item are items only on the new value, and the third value are items in both. In your case, you only care about the new items.

Update: If you don't want to deal with a tuple return value, you could tag the return value with diff metadata:

(defn swap-diff! [atom f & args]
  (loop []
    (let [curr-value @atom
          new-value (apply f curr-value args)]
      (if (compare-and-set! atom curr-value new-value)
          (with-meta new-value
                     (zipmap [:before :after :both]
                             (data/diff curr-value new-value)))
        (recur)))))

And then call meta on the result to get the diff:

(meta *1)
=> {:before nil, :after {9 "ok"}, :both {3 3, 2 2, 1 1}}

Clojure 1.9

This becomes much cleaner with a new function swap-vals!:

(defn swap-diff! [atom f & args]
  (let [[old new] (apply swap-vals! atom f args)]
    (with-meta new (zipmap [:before :after :both]
                           (data/diff old new)))))

Upvotes: 3

Related Questions