Observer in Haskell

当我们贯彻数据和操作分离的原则时,常常需要实现这样的一种模式:一个数据源(subject),若干观察者(observer)。每当数据源发生变化的时候,观察者可以看到它的变化,并且作出相应的行为。在OOP的领域当中我们就称呼此为Observer模式。
经受OOP思维熏陶之后,我自然能很画出Observer模式的类图,清楚的表达subject和observer之间的相互关系。但是现在我需要在Haskell中表达这样的思想,如何办?对象不再是思考的核心所在,对象之间的消息传递才是关键。经过一段时间的思考,我作出基本设计。观察到我需要Observer模式的根本原因是我希望针对源的一个变化,有一组对象需要获得这个新的数据施加某些变换后的结果。我采用多线程的方式,观察者位于一个独立的线程,其功能就是每当数据源发生变动时,对新的数据施加一个变换函数,获得一个结果,保存起来,供今后的时候。而在数据源不变的时候,这个线程保持阻塞,保持上一次的结果。看似采用多线程似乎把问题做复杂了,但是通过STM(Software Transactional Memory)的编程接口,一切异常轻松。

— MessageSlot是数据源与观察者直接共享的消息通道
type MessageSlot a = TChan a
— SharedObject是观察者根据数据源,作出变换操作后存储的结果
type SharedObject a = TVar (Maybe a)
— Subject 是 某个数据类型和其消息共享变量的偶对
type Subject a = (a, MessageSlot a)

— 创建数据源,参数是数据源的初始值,将这个初始值和构造的消息变量做一个偶对
newSubject a = (,) a `fmap` atomically newTChan

— 创建观察者线程,参数ms是消息共享通道,参数onChange是每当数据源变化时施以的变换操作
newObserver ms onChange = do{ so <- newTVarIO Nothing — 创建一个结果共享变量
                            ; ch <- atomically $ dupTChan ms
                            ; forkIO $ notifier ch so — 创建观察者线程
                            ; return so
                            }
— 观察者线程从消息共享通道读取消息,施加变换操作,然后写入结果变量
    where notifier ch so = forever . atomically $ do{ a <- readTChan ch
                                                    ; writeTVar so (Just $ onChange a)
                                                    }
                                                   
— 更新数据源操作,向消息共享通道写入新的数据
updateS (a,ms) f = (atomically $ writeTChan ms (f a)) >> return (f a,ms)

— 从结果共享变量中获取结果
fetchS so = atomically $ do{ v <- readTVar so
                           ; case v of
                               Just a -> return a
                               _ -> retry
                           }
蜻蜓点水般不负责任的解释一下STM:
1、核心的数据结构是TVar,它起到了线程间消息通知的作用。它是这样的共享变量,每当从中读取(readTVar)时,如果没有数据,那么就会阻塞,直到读到数据为止。写入(writeTVar)则是立刻执行。
2、STM提供一组Combinator,通过组合构成一个原子操作,使用atomically执行这个原子操作。
3、retry表示在合适的时机重头来执行它所在的原子操作,在这之间保持阻塞。
4、消息共享通道为什么使用TChan。原因是一个数据源有多个观察者,每当一个观察者从数据通道中读到一个新的消息时,它一方面应当将其擦除(或者标记我已经读过了),另一方面它不能影响到其他的观察者从通道中读取这个新的消息。这虽然是一个单写者多读者的问题,但是标记已读的要求使得问题变得复杂。一个可行的方法是写者有一个标记,每次写着写入时,应当顺带修改这个标记,这是一个复杂的想法,实现也很复杂。于是我想到了STM中的TChan。TChan提供给我们这样一个很好的特性:它是一个无限长的管道,写着只管写;读者通过(dupTChan)往这个管道开着窗口,各自维护了自己在管道中的位置,也就是它很清楚自己什么已经读过了,所以它也只管读就可以了。这个很符合我的想法。

此条目发表在Uncategorized分类目录。将固定链接加入收藏夹。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 登出 /  更改 )

Google photo

您正在使用您的 Google 账号评论。 登出 /  更改 )

Twitter picture

您正在使用您的 Twitter 账号评论。 登出 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 登出 /  更改 )

Connecting to %s