Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call a side effecting function only when atom value changes

What is the simplest way to trigger a side-effecting function to be called only when an atom's value changes?

If I were using a ref, I think I could just do this:

(defn transform-item [x] ...)
(defn do-side-effect-on-change [] nil)

(def my-ref (ref ...))
(when (dosync (let [old-value @my-ref
                    _ (alter! my-ref transform-item)
                    new-value @my-ref]
                (not= old-value new-value)))
  (do-side-effect-on-change))

But this seems seems a bit roundabout, since I'm using a ref even though I am not trying to coordinate changes across multiple refs. Essentially I am using it just to conveniently access the old and new value within a successful transaction.

I feel like I should be able to use an atom instead. Is there a solution simpler than this?

(def my-atom (atom ...))
(let [watch-key ::side-effect-watch
      watch-fn (fn [_ _ old-value new-value]
                 (when (not= old-value new-value)
                   (do-side-effect-on-change)))]
  (add-watch my-atom watch-key watch-fn)
  (swap! my-atom transform-item)
  (remove-watch watch-key))

This also seems roundabout, because I am adding and removing the watch around every call to swap!. But I need this, because I don't want a watch hanging around that causes the side-effecting function to be triggered when other code modifies the atom.

It is important that the side-effecting function be called exactly once per mutation to the atom, and only when the transform function transform-item actually returns a new value. Sometimes it will return the old value, yielding new change.

like image 990
algal Avatar asked Oct 30 '22 09:10

algal


1 Answers

(when (not= @a (swap! a transform))
  (do-side-effect))

But you should be very clear about what concurrency semantics you need. For example another thread may modify the atom between reading it and swapping it:

  1. a = 1
  2. Thread 1 reads a as 1
  3. Thread 2 modifies a to 2
  4. Thread 1 swaps a from 2 to 2
  5. Thread 1 determines 1 != 2 and calls do-side-effect

It is not clear to me from the question whether this is desirable or not desirable. If you do not want this behavior, then an atom just will not do the job unless you introduce concurrency control with a lock.

Seeing as you started with a ref and asked about an atom, I think you have probably given some thought to concurrency already. It seems like from your description the ref approach is better:

(when (dosync (not= @r (alter r transform))
  (do-side-effect))

Is there a reason you don't like your ref solution?

If the answer is "because I don't have concurrency" Then I would encourage you to use a ref anyway. There isn't really a downside to it, and it makes your semantics explicit. IMO programs tend to grow and to a point where concurrency exists, and Clojure is really great at being explicit about what should happen when it exists. (For example oh I'm just calculating stuff, oh I'm just exposing this stuff as a web service now, oh now I'm concurrent).

In any case, bear in mind that functions like alter and swap! return the value, so you can make use of this for concise expressions.

like image 179
Timothy Pratley Avatar answered Nov 12 '22 21:11

Timothy Pratley