Clojure 如何以纯功能的方式实现观察器设计模式?

Clojure 如何以纯功能的方式实现观察器设计模式?,clojure,functional-programming,lisp,scheme,observer-pattern,Clojure,Functional Programming,Lisp,Scheme,Observer Pattern,假设我想使用OO编程语言实现一个事件总线。 我可以这样做(伪代码): 这实际上是观察器模式,但用于应用程序的事件驱动控制流 您将如何使用函数式编程语言(如lisp语言之一)实现此模式 我这样问是因为如果不使用对象,仍然需要某种状态来维护所有侦听器的集合。此外,由于listeners集合会随着时间的推移而变化,因此不可能创建纯功能解决方案,对吗 此外,由于listeners集合会随着时间的推移而变化,因此不可能创建纯功能解决方案,对吗 这不是什么问题——一般来说,只要在命令式解决方案中修改对象的属

假设我想使用OO编程语言实现一个事件总线。 我可以这样做(伪代码):

这实际上是观察器模式,但用于应用程序的事件驱动控制流

您将如何使用函数式编程语言(如lisp语言之一)实现此模式

我这样问是因为如果不使用对象,仍然需要某种状态来维护所有侦听器的集合。此外,由于listeners集合会随着时间的推移而变化,因此不可能创建纯功能解决方案,对吗

此外,由于listeners集合会随着时间的推移而变化,因此不可能创建纯功能解决方案,对吗

这不是什么问题——一般来说,只要在命令式解决方案中修改对象的属性,就可以在纯函数式解决方案中使用新值计算新对象。我认为实际的事件传播有点问题——它必须由一个函数来实现,该函数接收事件、一整套潜在的观察者加上
EventBus
,然后过滤掉实际的观察者,并返回一组全新的对象,其中包含由其事件处理函数计算的观察者的新状态。非观察者在输入和输出集合中当然是相同的

如果这些观察者响应调用的方法(这里是函数)上的生成新事件,这会很有趣——在这种情况下,您需要递归地应用函数(可能允许它接受多个事件),直到它不再生成要处理的事件

通常,该函数将获取一个事件和一组对象,并返回一组新的对象,这些对象具有表示由事件传播引起的所有修改的新状态

TL;DR:我认为以纯功能的方式对事件传播进行建模是复杂的。

对此有一些评论:

我不知道它是如何实现的,但是有一个名为“”的东西,可以作为许多函数式语言的库使用。这实际上或多或少是正确的观察者模式

此外,观察者模式通常用于通知状态的变化,就像在各种MVC实现中一样。但是,在函数式语言中,没有直接的方法来更改状态,除非您使用一些技巧(如monad)来模拟状态。但是,如果您使用monad模拟状态更改,您还可以在monad中添加观察者机制


从您发布的代码来看,似乎您实际上正在进行事件驱动编程。因此,观察者模式是在面向对象语言中获得事件驱动编程的典型方式。因此,在面向对象的世界中,您有一个目标(事件驱动编程)和一个工具(观察者模式)。如果您想充分利用函数式编程的强大功能,您应该检查有哪些其他方法可用于实现这一目标,而不是直接从面向对象的世界移植工具(对于函数式语言来说,这可能不是最佳选择)。只需查看此处提供的其他工具,您可能会找到更适合您的目标的工具。

如果观察者模式本质上是关于发布者和订阅者的,那么Clojure有两个功能可供您使用:

addwatch函数接受三个参数:引用、watch函数键和在引用更改状态时调用的watch函数

显然,由于可变状态的变化,这不是纯粹的功能(如您明确要求的),但是
add watcher
将为您提供一种对事件做出反应的方法,如果这是您所寻求的效果,例如:

(def number-cats (ref 3))

(defn updated-cat-count [k r o n]
  ;; Takes a function key, reference, old value and new value
  (println (str "Number of cats was " o))
  (println (str "Number of cats is now " n)))

(add-watch number-cats :cat-count-watcher updated-cat-count)

(dosync (alter number-cats inc))
输出:

Number of cats was 3
Number of cats is now 4
4

我建议创建一个ref,其中包含一组侦听器,每个侦听器都是作用于事件的函数

比如:

(def listeners (ref #{}))

(defn register-listener [listener]
  (dosync
     (alter listeners conj listener)))

(defn unregister-listener [listener]
  (dosync
     (alter listeners disj listener)))

(defn fire-event [event] 
  (doall
    (map #(% event) @listeners)))
请注意,您在这里使用的是可变状态,但这是可以的,因为您试图显式解决的问题需要状态来跟踪一组侦听器


请注意,感谢C.A.McCann的评论:我正在使用一个“ref”,它存储一组活动侦听器,该侦听器具有一个很好的额外属性,即解决方案对于并发是安全的。所有更新都受(dosync…)构造内的STM事务保护。在这种情况下,这可能是杀伤力过大(例如,一个atom也可以做到这一点),但在更复杂的情况下,这可能会派上用场,例如,当您注册/取消注册一组复杂的侦听器,并希望在单个线程安全的事务中进行更新时。

+1,但是请注意,Clojure确实有一些非常强大的方法来管理可变状态,同时保持在函数范式中(即,您的评论更适用于“纯”函数式语言,如Haskell)@mikera:Haskell也有很多方法来管理可变状态,我不知道人们从何处得到这样的想法。它只需要效果跟踪和显式排序,这与懒惰比其他任何事情都有更多关系。FRP之所以有趣,主要是因为它在概念上比使用可变状态更好。@C A McCann-当我上次使用Haskell(承认是几年前)时,Haskell中涉及可变状态的所有内容都必须使用一元构造,以便将可变概念转换为纯函数。这种情况改变了吗?e、 如果我有一个函数Int->Int,它会有可变的副作用吗?你可以在Clojure,就我最近所知,在Haskell你不能,但很高兴能学到其他的东西。@mikera:就像我说的,这实际上只是效果跟踪和显式排序;monadicapi是一个实现细节(是的,您可以使用
状态
monad在纯代码中模拟状态,但是
(def listeners (ref #{}))

(defn register-listener [listener]
  (dosync
     (alter listeners conj listener)))

(defn unregister-listener [listener]
  (dosync
     (alter listeners disj listener)))

(defn fire-event [event] 
  (doall
    (map #(% event) @listeners)))