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

Clojure 仅当原子值更改时调用副作用函数,clojure,clojurescript,Clojure,Clojurescript,只有当原子的值发生变化时,触发副作用函数调用的最简单方法是什么 如果我使用的是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)

只有当原子的值发生变化时,触发副作用函数调用的最简单方法是什么

如果我使用的是
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时都会添加和删除手表。但是我需要这个,因为我不想让手表挂在那里,当其他代码修改原子时会触发副作用函数

重要的是,副作用函数在原子的每一次变异中准确调用一次,并且仅当转换函数
transform item
实际返回一个新值时才调用。有时它会返回旧值,产生新的变化

(when (not= @a (swap! a transform))
  (do-side-effect))
但是您应该非常清楚您需要什么样的并发语义。例如,另一个线程可能会在读取和交换atom之间修改atom:

  • a=1
  • 线程1将a读取为1
  • 线程2将a修改为2
  • 螺纹1将a从2交换到2
  • 线程1确定1!=2.打电话有副作用
  • 从这个问题上我不清楚这是可取的还是不可取的。如果您不希望出现这种行为,那么atom将不会执行此任务,除非您引入带有锁的并发控制

    当您从一个ref开始并询问一个atom时,我想您可能已经考虑过并发性了。根据您的描述,ref方法似乎更好:

    (when (dosync (not= @r (alter r transform))
      (do-side-effect))
    
    您不喜欢ref解决方案有什么原因吗

    如果答案是“因为我没有并发性”,那么我会鼓励您使用ref。它并没有真正的缺点,它使您的语义更加明确。IMO程序趋向于增长,并且达到了并发存在的程度,Clojure非常擅长于明确它存在时应该发生什么。(例如,哦,我只是在计算东西,哦,我只是将这些东西作为web服务公开,哦,现在我是并发的)


    在任何情况下,请记住,类似alter和swap的功能!返回值,这样您就可以使用它来获得简洁的表达式。

    我遇到了同样的情况,并提出了两种解决方案

    状态字段
    :已更改?
    保持一个无意义的
    :在atom中更改
    标记以跟踪交换功能。并获取
    swap的返回值查看情况是否发生了变化。例如:

    (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)))
    
    基于异常的 您可以在交换函数中抛出异常,并在外部捕获它:

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

    您的
    atom
    解决方案甚至没有达到预期效果;如果原子的值在
    交换之前被另一个线程更改完成后,手表将为该更改触发一次,并在
    交换后再次触发完成。很好!add watch和remove watch没有封装在事务中,因此我实际上没有将监视隔离到对
    交换的特定调用。谢谢。我不喜欢我的ref解决方案的原因是,根据文档,我认为ref是为“协调”状态保留的,所以我认为仅仅为了触发更改的副作用而使用它们肯定是错误的。但听起来这是最好的方法,因为您描述的原子语义不适合我的情况。我现在将自豪地讲述我的dosync,而不是带着羞耻和怀疑。