仅在原子值更改时调用副作用函数

Call a side effecting function only when atom value changes

仅当 atom 的值发生变化时触发副作用函数的最简单方法是什么?

如果我使用 ref,我想我可以这样做:

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

但这似乎有点迂回,因为我使用的是 ref,尽管我并没有尝试协调多个 ref 之间的变化。本质上,我使用它只是为了方便地访问成功交易中的旧值和新值。

我觉得我应该可以使用 atom 来代替。有没有比这更简单的解决方案?

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

这似乎也是迂回的,因为我在每次调用 swap! 时添加和删除监视。但我需要这个,因为我不希望挂起的手表会导致在其他代码修改 atom.

时触发副作用函数

重要的是,每次对原子进行突变时,仅调用一次副作用函数,并且仅当转换函数 transform-item 实际上 return 是一个新值时。有时它会 return 旧值,产生新的变化。

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

但是你应该很清楚你需要什么样的并发语义。例如,另一个线程可能会在读取原子和交换原子之间修改原子:

  1. 一=1
  2. 线程 1 读取 a 为 1
  3. 线程2修改a为2
  4. 线程 1 将 a 从 2 交换为 2
  5. 线程 1 确定 1 != 2 并调用 do-side-effect

我不清楚这个问题是可取的还是不可取的。如果您不想要这种行为,那么除非您引入带锁的并发控制,否则原子将不会完成这项工作。

看到您从引用开始并询问了原子,我认为您可能已经对并发性进行了一些思考。从您的描述看来,ref 方法更好:

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

你不喜欢你的 ref 解决方案有什么原因吗?

如果答案是 "because I don't have concurrency" 那么我还是鼓励您使用 ref。它并没有真正的缺点,它使您的语义明确。 IMO 程序往往会增长并达到并发存在的程度,而 Clojure 非常擅长明确说明当它存在时应该发生什么。 (例如,哦,我只是在计算东西,哦,我现在只是将这些东西公开为 Web 服务,哦,现在我是并发的)。

无论如何,请记住更改和交换等功能! return 值,因此您可以将其用于简洁的表达式。

我 运行 遇到了同样的情况,只是提出了 2 个解决方案。

状态字段:changed?

在原子中保留一个无意义的:changed标记来跟踪交换功能。并取 swap! 的 return 值,看看是否发生了变化。例如:

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

基于异常

你可以在 swap 函数中抛出一个 Exception,然后在外面捕获它:

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