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

ivo*_*ivo 23 lisp scheme functional-programming clojure observer-pattern

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

class EventBus

    listeners = []

    public register(listener):
        listeners.add(listener)

    public unregister(listener):
        listeners.remove(listener)

    public fireEvent(event):
        for (listener in listeners):
            listener.on(event)
Run Code Online (Sandbox Code Playgroud)

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

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

我问这个是因为如果一个人不使用对象,人们仍然需要某种状态来维护所有听众的集合.此外,由于听众集合随着时间的推移而变化,因此无法创建纯粹的功能解决方案,对吧?

LiK*_*Kao 24

对此有一些评论:

我不确定它是如何完成的,但有一种称为" 功能反应式编程 "的东西可用作许多函数式语言的库.这实际上或多或少是正确的观察者模式.

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

根据您发布的代码判断,您实际上正在进行事件驱动编程.因此,观察者模式是在面向对象语言中获取事件驱动编程的典型方式.因此,您有一个目标(事件驱动编程)和面向对象世界中的工具(观察者模式).如果您想充分利用函数式编程,那么您应该检查可用于实现此目标的其他方法,而不是直接从面向对象的世界移植工具(它可能不是函数式语言的最佳选择).只需检查这里有哪些其他工具,您可能会找到更适合您目标的东西.

  • @CA McCann - 当我上次使用 Haskell 时(诚然是几年前),Haskell 中涉及可变状态的所有内容都必须使用 monadic 构造来完成,以便将可变性的概念转换为纯函数。有没有改变?例如,如果我有一个函数 Int -> Int 它会产生可变的副作用吗?你可以在 Clojure 中,据我最近的知识你不能在 Haskell 中,但很高兴学习其他方式。 (2认同)
  • @mikera:就像我说的,它实际上只是效果跟踪和显式排序;monadic API 是一个实现细节(是的,您可以使用 `State` monad 在纯代码中*模拟*状态,但这与真正的可变状态完全不同)。[Clojure 使用](http://clojure.org/state) 的哲学在精神上几乎与 Haskell 相同,只是去掉了静态类型——只需将 `IO a` 读作一个抽象标识,类型为 `a `. 函数 `Int -> Int` 中的副作用就像直接在 Clojure 中修改一个值。 (2认同)

Sco*_*ott 22

如果Observer模式主要是关于发布者和订阅者,那么Clojure有几个你可以使用的函数:

add-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))
Run Code Online (Sandbox Code Playgroud)

输出:

Number of cats was 3
Number of cats is now 4
4
Run Code Online (Sandbox Code Playgroud)


Raf*_*ird 6

此外,由于听众集合随着时间的推移而变化,因此无法创建纯粹的功能解决方案,对吧?

这不是一个问题 - 通常,只要您在命令式解决方案中修改对象的属性,就可以在纯功能解决方案中使用新值计算新对象.我相信实际的事件传播有点问题 - 它必须由一个接受事件的函数,整组潜在的观察者加上EventBus,然后过滤掉实际的观察者并返回一组全新的对象来实现.通过事件处理函数计算的新观察者状态.非观察者当然在输入和输出集中是相同的.

如果那些观察者在响应他们的on方法(这里:函数)被调用时生成新事件会变得有趣- 在这种情况下,你需要递归地应用函数(可能允许它接受多个事件),直到它不再产生事件为止处理.

通常,该函数将获取事件和一组对象,并返回新的对象集,其中新的状态表示由事件传播产生的所有修改.

TL; DR:我认为以纯粹的功能方式建模事件传播是复杂的.


mik*_*era 6

我建议创建一个包含一组侦听器的引用,每个侦听器都是一个对事件起作用的函数.

就像是:

(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)))
Run Code Online (Sandbox Code Playgroud)

请注意,您在此处使用了可变状态,但这是正常的,因为您尝试明确解决的问题需要跟踪一组侦听器的状态.

请注意,感谢CAMcCann的评论:我正在使用一个"ref"来存储一组活跃的监听器,它具有很好的奖励属性,该解决方案对于并发性是安全的.所有更新都由(dosync ....)构造中的STM事务保护.在这种情况下,它可能是矫枉过正(例如,一个原子也可以做到这一点),但这可能会在更复杂的情况下派上用场,例如,当您注册/取消注册一组复杂的侦听器并希望更新发生在一个单独的时候,线程安全的转换.