algal
algal

Reputation: 28094

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.

Upvotes: 5

Views: 6181

Answers (3)

Jason
Jason

Reputation: 12283

Here's another good solution.

Add the watch once and have the watch check for the specific change of interest in the value of my-atom using old-state and new-state (see add-watch). No need to add and remove the watch. I don't know what your end goal is, but it would be worth sharing it and considering more deeply how to solve the problem.

Here's an example assuming you only want the side effect when a certain part of the atom's value changes.

(def my-atom (atom {:items []
                    :messages []}))

(defn my-watch [_ _ old-state new-state]
  (when (not= (:items old-state) (:items new-state))
    (do-side-effect-on-change)))

(add-watch my-atom ::side-effect-watch my-watch)

(swap! my-atom update-in [:items] conj {:item :new-item})   ; triggers side effect

(swap! my-atom update-in [:message] conj {:message "hello"})   ; does not trigger side effect

I see the possibility that transform-item may actually modify an item already in :items and perhaps you want to know specifically if transform-item made a modification to an existing item.

In this case, refs may be good. I would not hesitate to use a ref over an atom just because you aren't synchronizing multiple refs. Don't get caught up in that particular use case. refs are also good for managing side effects because they have this special feature when interacting with agents: "Agents are integrated with the STM - any dispatches made in a transaction are held until it commits, and are discarded if it is retried or aborted." (see Clojure docs). There's a question and answer on SO where Alex Miller approves of this approach, albeit, by saying "seems like that should work". You can capture the before and after value with such an approach and include it in the call to send or send-off. Do the check for changes in your fn running in the agent thread and execute side effects (or just do the check within the transaction as in the OP). This approach would have the advantage of being able to only sometimes check for changes in the value of the ref - in particular only when transform-item is called and not when other value altering fns in other parts of the code.

In fact, this approach is exactly what clojure.tools.logging does! See source for log*, it has the call (send-off *logging-agent* ...).

Here's an example:

(def my-ref (ref {:items []
                  :messages []}))

(def my-side-effect-agent (agent :no-state))

; Triggers side effect on change from transform-item. No side effect if no change.
(dosync (let [old-state @my-ref]
          (alter my-ref transform-item)
          (when (not= old-state @my-ref)
            (send-off my-side-effect-agent do-side-effect-on-change))))

; Does not trigger side effect no matter if ref value changes or not
(dosync (alter my-ref another-fn-of-mine))

Upvotes: 2

Ning Sun
Ning Sun

Reputation: 2191

I'm running into the same situation and just come up 2 solutions.

state field :changed?

Keeping a meanless :changed mark in atom to track swap function. And take the return value of swap! to see if things changed. For example:

(defn data (atom {:value 0 :changed? false}))

(let [{changed? :changed?} (swap! data (fn [data] (if (change?) 
                                                    {:value 1 :changed? true} 
                                                    {:value 0 :change? false})))]
  (when changed? (do-your-task)))

exception based

You can throw an Exception in swap function, and catch it outside:

(try
  (swap! data (fn [d] (if (changed?) d2 (ex-info "unchanged" {})))
  (do-your-task)
  (catch Exception _
    ))

Upvotes: -1

Timothy Pratley
Timothy Pratley

Reputation: 10662

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

Upvotes: 1

Related Questions