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

alg*_*gal 5 clojure clojurescript

触发副作用函数的最简单方法是仅在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))
Run Code Online (Sandbox Code Playgroud)

但这看起来似乎有点迂回,因为ref即使我没有尝试协调多个refs的变化,我也在使用它.基本上我只是为了方便地访问成功交易中的旧值和新值而使用它.

我觉得我应该可以使用一个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))
Run Code Online (Sandbox Code Playgroud)

这似乎也很迂回,因为我在每次通话时都会添加和删除手表swap!.但是我需要这个,因为我不希望手表闲逛会导致副作用功能在其他代码修改时触发atom.

重要的是,每个突变到原子时,副作用函数只被调用一次,并且只有当变换函数transform-item实际返回一个新值时才是这样.有时它会返回旧值,产生新的变化.

Tim*_*ley 2

(when (not= @a (swap! a transform))
  (do-side-effect))
Run Code Online (Sandbox Code Playgroud)

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

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

从这个问题来看,我不清楚这是可取的还是不可取的。如果您不希望出现这种行为,那么原子将无法完成这项工作,除非您引入带锁的并发控制。

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

(when (dosync (not= @r (alter r transform))
  (do-side-effect))
Run Code Online (Sandbox Code Playgroud)

您有什么理由不喜欢您的参考解决方案吗?

如果答案是“因为我没有并发性”,那么我会鼓励您无论如何使用 ref 。它并没有真正的缺点,而且它使你的语义变得明确。IMO 程序往往会增长并达到并发存在的程度,而 Clojure 非常擅长明确并发性存在时应该发生的情况。(例如,哦,我只是在计算东西,哦,我现在只是将这些东西公开为 Web 服务,哦,现在我是并发的)。

无论如何,请记住像 alter 和 swap 这样的函数!返回值,因此您可以将其用于简洁的表达式。