开发者

How to implement the Observer Design Pattern in a pure functional way?

开发者 https://www.devze.com 2023-03-25 07:23 出处:网络
Let\'s say I want to implement an event bus using a OO programming language. I could do this (pseudocode):

Let's say I want to implement an event bus using a OO programming language. I could do this (pseudocode):

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)

This is actually the the observer pattern, but used for eve开发者_运维技巧nt-driven control flow of an application.

How would you implement this pattern using a functional programming language (such as one of the lisp flavors)?

I ask this because if one doesn't use objects, one would still need some kind of state to maintain a collection of all the listeners. More over, since the listeners collection changes over time, it would not be possible to create a pure functional solution, right?


Some remarks on this:

I am not sure how it is done, but there is something called "functional reactive programming" which is available as a library for many functional languages. This is actually more or less the observer pattern done right.

Also the observer pattern is usually used for notifying changes in state, as in the various MVC implementations. However in a functional language there is no direct way to do state-changes, unless you use some tricks such as monads to simulate the state. However if you simulate the state changes using monads you will also get points where you can add the observer mechanism inside the monad.

Judging from the code you posted it seems that you are actually doing event driven programming. So the observer pattern is a typical way to get event driven programming in Object oriented languages. So you have a goal (event driven programming) and a tool in the object oriented world (observer pattern). If you want to use the full power of functional programming you should check what other methods are available for achieving this goal instead of directly porting the tool from the object oriented world (it might not be the best choice for a functional language). Just check what other tools are available here and you will probably find something that fits your goals much better.


If the Observer pattern is essentially about publishers and subscribers then Clojure has a couple of functions that you could use:

  • add-watch
  • remove-watch

The add-watch function takes three arguments: a reference, a watch function key, and a watch function that is called when the reference changes state.

Clearly, because of the changes in mutable state, this is not purely functional (as you clearly requested), but add-watcher will give you a way to react to events, if that's the effect you were seeking, like so:

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

Output:

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


I'd suggest creating a ref which contains a set of listeners, each of which is a function that acts on an event.

Something like:

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

Note that you are using mutable state here, but that is OK because the problem you are trying to solve explicitly requires state in terms of keeping track of a set of listeners.

Note thanks to C.A.McCann's comment: I'm using a "ref" which stores the set of active listeners which has the nice bonus property that the solution is safe for concurrency. All updates take place protected by the STM transaction within the (dosync ....) construct. In this case it's possibly overkill (e.g. an atom would also do the trick) but this might come in handy in more complex situations, e.g. when you are registering/unregistering a complex set of listeners and want the update to take place in a single, thread-safe transation.


More over, since the listeners collection changes over time, it would not be possible to create a pure functional solution, right?

This is less of a problem - in general, whenever you'd modify an object's attribute in an imperative solution, you can compute a new object with the new value in a pure functional solution. I believe that the actual event propagation is a bit more problematic - it would have to be implemented by a function that takes the event, the whole set of potential observers plus the EventBus, then filters out the actual observers and returns a whole new set of objects with the new states of observers computed by their event processing functions. Non-observers would of course be the same in the input and output sets.

It gets interesting if those observers generate new events in response to their on methods (here: functions) being called - in this case you need to apply the function recursively (perhaps allowing it to take more than one event) until it produces no more events to process.

In general, the function would take an event and a set of objects and return the new set of objects with new states representing all modifications resulting from the event propagation.

TL;DR: I think that modeling event propagation in a pure functional way is complicated.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号