Haskell 在函数式反应式编程中,如何在应用程序的两个部分之间共享状态?
我有一些应用程序架构,其中用户输入流到一些自动机,自动机在事件流的上下文中运行,并将用户引导到应用程序的不同部分。应用程序的每个部分都可以根据用户输入运行一些操作。但是,应用程序的两个部分共享某些状态,并且从概念上讲,读写状态相同。需要注意的是,这两个“线程”不是同时运行的,其中一个“暂停”,而另一个“产生”输出。在不使用全局变量的情况下,描述这种状态共享计算的规范方法是什么?这两个“线程”保持通过某种形式的消息传递同步的本地状态有意义吗,即使它们无论如何都不是并发的Haskell 在函数式反应式编程中,如何在应用程序的两个部分之间共享状态?,haskell,functional-programming,frp,Haskell,Functional Programming,Frp,我有一些应用程序架构,其中用户输入流到一些自动机,自动机在事件流的上下文中运行,并将用户引导到应用程序的不同部分。应用程序的每个部分都可以根据用户输入运行一些操作。但是,应用程序的两个部分共享某些状态,并且从概念上讲,读写状态相同。需要注意的是,这两个“线程”不是同时运行的,其中一个“暂停”,而另一个“产生”输出。在不使用全局变量的情况下,描述这种状态共享计算的规范方法是什么?这两个“线程”保持通过某种形式的消息传递同步的本地状态有意义吗,即使它们无论如何都不是并发的 没有代码示例,因为问题更具
没有代码示例,因为问题更具概念性,但欢迎使用Haskell(使用任何FRP框架)或其他语言的示例进行回答。我一直在研究解决此问题的方法。高级总结是您: A) 将所有并发代码提取为纯单线程规范 B) 单线程规范使用
StateT
共享公共状态
整体架构的灵感来自模型视图控制器。你有:
- 控制器,它们是有效的输入
- 视图,它们是有效的输出
- 一个模型,它是一个纯流转换
controller1 - -> view1
\ /
controller2 ---> controllerTotal -> model -> viewTotal---> view2
/ \
controller3 - -> view3
\______ ______/ \__ __/ \___ ___/
v v v
Effectful Pure Effectful
>>> quickCheck $ \n -> length ((`evalState` initialStatus) $ P.toListM $ each (replicate n (Key 'x')) >-> runEdge (rcplCore t)) == n || n < 0
该模型是一个纯单线程流转换器,它实现了箭头
和箭头选择
。原因是:
是与并行性等效的单线程箭头
是单线程的并发等价物ArrowChoice
管道
,它似乎有一个正确的箭头
和箭头选择
实例,尽管我仍在验证这些定律,所以在我完成它们的证明之前,这个解决方案仍然是实验性的。对于好奇的人,相关类型和实例如下:
newtype Edge m r a b = Edge { unEdge :: a -> Pipe a b m r }
instance (Monad m) => Category (Edge m r) where
id = Edge push
(Edge p2) . (Edge p1) = Edge (p1 >~> p2)
instance (Monad m) => Arrow (Edge m r) where
arr f = Edge (push />/ respond . f)
first (Edge p) = Edge $ \(b, d) ->
evalStateP d $ (up \>\ unsafeHoist lift . p />/ dn) b
where
up () = do
(b, d) <- request ()
lift $ put d
return b
dn c = do
d <- lift get
respond (c, d)
instance (Monad m) => ArrowChoice (Edge m r) where
left (Edge k) = Edge (bef >=> (up \>\ (k />/ dn)))
where
bef x = case x of
Left b -> return b
Right d -> do
_ <- respond (Right d)
x2 <- request ()
bef x2
up () = do
x <- request ()
bef x
dn c = respond (Left c)
n
是我按x
键的次数。运行该测试将产生以下输出:
*** Failed! Falsifiable (after 17 tests and 6 shrinks):
78
快速检查发现我的属性是假的!此外,由于代码是引用透明的,QuickCheck可以将反例缩小到最小的复制冲突。按下78键后,终端驱动程序会发出一个换行符,因为控制台宽80个字符,提示会占用两个字符(“>”
)。如果并发性和IO
感染了我的整个系统,我将很难验证这种属性
有一个纯粹的设置是伟大的另一个原因:一切都是完全可复制的!如果我存储了所有传入事件的日志,那么任何时候出现错误时,我都可以重播这些事件,并且可以将完全复制的测试用例添加到我的测试套件中
然而,纯度最重要的好处是能够更轻松地对代码进行非正式和正式的推理。当您从等式中删除Haskell的调度器时,您可以静态地证明代码的某些方面,而当您必须依赖具有非正式指定语义的并发运行时,您无法证明这些方面。事实证明,即使对于非正式的推理,这也是非常有用的,因为当我将代码转换为使用mvc
时,它仍然有几个bug,但这些bug比我第一次迭代中顽固的并发bug更容易调试和删除
rcpl
示例使用StateT
在不同组件之间共享全局状态,因此对您的问题的冗长回答是:您可以使用StateT
,但前提是您将系统转换为单线程版本。幸运的是这是可能的 我认为这个问题太宽泛,无法给出具体的答案。您建议的任何策略(同步、FRP、全局VAR)都可能适合给定的情况。或者可能是本地共享的IORef
或MVar
。或者,如果计算实际上是在单个线程中,则使用StateT
monad转换器。我不清楚“threads”
是指由forkIO
创建的实际线程,还是严格意义上的概念性线程,而您实际上只运行一个线程。@JohnL:既然这个问题提到了FRP,我想一个关于如何通过应用程序共享行为或事件流的答案会很好。我认为将一个行为(或多个行为,如果合适的话)贯穿应用程序大体上是一个不错的选择,但在将其转化为答案之前,我确实需要解决细节问题。也许几个小时后没人来……很抱歉,但是型号和类型Edge
之间有什么联系?整个MVC图是一个Edge
吗?是的,Edge
应该被称为Model
。这些名字仍在不断变化。而且,是的,整个MVC图只是一个大的边缘。