在函数式反应式编程中,如何在应用程序的两个部分之间共享状态?

2024-07-03

我有一些应用程序架构,其中用户输入流向一些自动机,该自动机在事件流的上下文中运行并将用户引导到应用程序的不同部分。应用程序的每个部分都可以根据用户输入运行一些操作。但是,应用程序的两个部分共享某些状态,并且从概念上讲,读取和写入相同的状态。需要注意的是,两个“线程”不同时运行,其中一个“暂停”,而另一个“产生”输出。在不诉诸某些全局变量的情况下,描述这种状态共享计算的规范方法是什么?对于两个“线程”来说,通过某种形式的消息传递来保持本地状态同步是否有意义,即使它们无论如何都不是并发的?

由于问题更具概念性,因此没有代码示例,但欢迎使用 Haskell(使用任何 FRP 框架)或其他语言提供示例答案。


我一直在研究这个问题的解决方案。概括地说,您:

A)将所有并发代码提炼为纯粹的单线程规范

B) 单线程规范使用StateT共享共同状态

整体架构的灵感来自模型-视图-控制器。你有:

  • 控制器,是有效的输入
  • 视图,是有效的输出
  • 模型,纯粹的流转换

该模型只能与一个控制器和一个视图交互。但是,控制器和视图都是幺半群,因此您可以将多个控制器组合成一个控制器,将多个视图组合成一个视图。从图表上看,它看起来像这样:

 controller1 -                                           -> view1
              \                                         /
 controller2 ---> controllerTotal -> model -> viewTotal---> view2
              /                                         \
 controller3 -                                           -> view3

                  \______ ______/   \__ __/   \___ ___/
                         v             v          v
                     Effectful        Pure    Effectful

该模型是一个纯粹的单线程流转换器,它实现Arrow and ArrowChoice。原因是这样的:

  • Arrow单线程相当于并行
  • ArrowChoice单线程相当于并发

在本例中,我使用基于推送的pipes,这似乎有一个正确的Arrow and 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)

该模型还需要是一个 monad 转换器。原因是我们想要嵌入StateT在基本 monad 中跟踪共享状态。在这种情况下,pipes符合要求。

难题的最后一部分是一个复杂的现实世界示例,该示例采用复杂的并发系统并将其提炼为纯单线程等效系统。为此,我使用即将推出的rcpl库(“读取并发打印循环”的缩写)。目的rcpl库的目的是为控制台提供一个并发接口,使您可以在并发打印到控制台的同时读取用户的输入,但打印的输出不会破坏用户的输入。它的 Github 存储库在这里:

链接到 Github 存储库 https://github.com/Gabriel439/Haskell-RCPL-Library/tree/ac117d32eac5009504b700f9f29e3fada111e91c

我最初对该库的实现具有普遍的并发性和消息传递,但受到几个我无法解决的并发错误的困扰。然后当我想出mvc(我的类似 FRP 的框架的代号,“模型-视图-控制器”的缩写),我认为rcpl将是一个很好的测试用例,看看是否mvc已经准备好迎接黄金时段了。

我把整个逻辑都掌握了rcpl并将其变成一个单一的、纯净的管道。这就是你会发现的该模块 https://github.com/Gabriel439/Haskell-RCPL-Library/blob/ac117d32eac5009504b700f9f29e3fada111e91c/RCPL.hs,并且总逻辑完全包含在rcplCore pipe https://github.com/Gabriel439/Haskell-RCPL-Library/blob/ac117d32eac5009504b700f9f29e3fada111e91c/RCPL.hs#L237.

这很巧妙,因为现在实现是纯粹的,我可以快速检查它并验证某些属性!例如,我可能想要快速检查的一个属性是,每次用户按下键盘上的按键时,恰好有一个终端命令。x键,我将这样指定:

>>> quickCheck $ \n -> length ((`evalState` initialStatus) $ P.toListM $ each (replicate n (Key 'x')) >-> runEdge (rcplCore t)) == n || n < 0

n是我按下的次数x钥匙。运行该测试会产生以下输出:

*** Failed! Falsifiable (after 17 tests and 6 shrinks):
78

QuickCheck发现我的财产是假的!此外,由于代码是引用透明的,QuickCheck 可以将反例缩小到最小复制违规。按下 78 键后,终端驱动程序会发出换行符,因为控制台有 80 个字符宽,提示符占用了两个字符 ("> "在这种情况下)。如果并发性和IO感染了我的整个系统。

拥有纯粹的设置还有另一个原因:一切都是完全可重现的!如果我存储所有传入事件的日志,那么每当出现错误时,我都可以重播这些事件,并完美地再现测试用例,并将其添加到我的测试套件中。

然而,纯粹性最重要的好处实际上是能够更轻松地推理代码,无论是非正式的还是正式的。当您从等式中删除 Haskell 的调度程序时,您可以静态地证明有关代码的事情,而当您必须依赖于具有非正式指定语义的并发运行时时,您无法证明这些事情。实际上,即使对于非正式推理,这也被证明非常有用,因为当我将代码转换为使用mvc它仍然有几个错误,但这些错误比我第一次迭代中的顽固并发错误更容易调试和删除。

The rcpl示例用途StateT在不同组件之间共享全局状态,因此您问题的冗长答案是:您可以使用StateT,但前提是您将系统转换为单线程版本。幸运的是这是可能的!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在函数式反应式编程中,如何在应用程序的两个部分之间共享状态? 的相关文章

  • 不明确的类型变量

    相关我之前关于遍历数据结构的问题 https stackoverflow com questions 1855371 avoiding boilerplate when dealing with many unrelated types 当
  • 如何使用 STL 在 C++ 中复制映射、过滤和归约行为?

    我想我们可以使用 std transform 来复制 C 中的映射行为 如下所示 std vector
  • 带边界的 haskell 列表数据类型

    我有以下类型定义来表示卡片 data Suit Hearts Spades Diamonds Clubs data Rank Numeric Integer Jack Queen King Ace data Card Card Rank S
  • LISP - 破坏性和非破坏性构造

    正确的定义是什么破坏性的 and 非破坏性的在 LISP 或一般情况下 中构造 我试图寻找真正的含义 但我只发现了这些术语的很多用法 而没有真正解释它们 据我了解 通过破坏性的函数是指一个函数 它改变构造 或变量 的含义 所以当我将一个列表
  • 当您包含导入 Gloss 的项目时,“stack ghci”会失败

    如果您在 Stack 项目中导入 Gloss 并使用stack ghci 您会收到以下错误 GHCi version 7 10 2 http www haskell org ghc for help
  • JavaScript 中 Scala View 的等效项

    在斯卡拉中 view允许防止创建全新的集合 例如在Scala中 视图 有什么作用 https stackoverflow com questions 6799648 in scala what does view do JavaScript
  • 如何在 Haskell 中编写 MST 算法(Prim 或 Kruskal)?

    我可以用 C 或 Java 编写 Prim 和 Kruskal 算法来查找最小生成树 但我想知道如何在 Haskell 中以 O mlogm 或 O mlogn 实现它们 纯函数式程序更好 多谢 正如斯文宁森所说 优先搜索队列 http h
  • 如何在 Haskell 中处理这个简单的 IO 异常

    全部处理 在下面的代码中 getDirectoryContents dir可能会失败 例如dir可能不存在 如何捕获这个并向用户抛出有意义的消息 我知道 IO 异常处理已经被问过很多次了 但我仍然找不到一个简单的方法来做到这一点 walk
  • 从 SQL 数据库反序列化数据

    我有一个小应用程序 由数据库支持 SQLite 但它与问题并不真正相关 我定义了一些类型 例如 data Whatever Whatever Int Int String String data ImportantStuff Importa
  • 列出树中叶子的路径

    我正在尝试编写一个函数来查找树中叶子的所有路径 例如 给定一棵如下所示的树 1 2 5 3 4 6 输出列表将是 1 2 3 1 2 4 1 5 6 该函数的类型签名是 branches Tree a gt a 请注意 这使用了中定义的 T
  • Scala 不可变 Map 速度慢

    当我创建地图时 我有一段代码 val map gtfLineArr 8 split map split collect case Array k v gt k v toMap 然后我使用这张地图来创建我的对象 case class MyOb
  • 如何在haskell快速傅里叶变换上应用数据并行?

    我有一个 haskell 代码来解决快速傅里叶变换 并且我想对其应用数据并行性 然而 我使用的每一个策略都会产生太多的火花 而且大多数都溢出了 有谁知道如何在以下算法上应用良好的数据并行策略 radix 2 Cooley Tukey FFT
  • 最小 Warp 网络服务器示例

    我想使用创建一个网站Warp https hackage haskell org package warpHaskell 中的网络服务器 由于我是 Haskell 初学者 例如this one https langnostic blogsp
  • Haskell - 计算列表中每个不同元素出现的次数

    我是 Haskell 的新手 只是想编写一个列表理解来计算列表中每个不同值的频率 但我在最后一部分遇到了麻烦 到目前为止我有这个 frequency Eq a gt a gt Int a frequency list count y lis
  • 是否可以与类型类中未提及的变量关联类型同义词?

    In 关联类型同义词 http www cse unsw edu au chak papers CKP05 html Chakravarty Keller Jones 该论文似乎表明以下内容是有效的 class C a where type
  • 将列表拆分为可能的元组列表

    我需要将列表拆分为所有可能元组的列表 但我不确定如何执行此操作 例如 pairs cat dog mouse 应该导致 cat dog cat mouse dog cat dog mouse mouse cat mouse dog 我能够形
  • 将列表拆分为可能的元组列表

    我需要将列表拆分为所有可能元组的列表 但我不确定如何执行此操作 例如 pairs cat dog mouse 应该导致 cat dog cat mouse dog cat dog mouse mouse cat mouse dog 我能够形
  • 更新列表的第 'x' 个元素 - Haskell [重复]

    这个问题在这里已经有答案了 可能的重复 替换 Haskell 中的单个列表元素 https stackoverflow com questions 5852722 replace individual list elements in ha
  • 我在哪里可以学习高级 Haskell? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 在评论中我的答案之一 https stackoverflow com questions 4633584 algorithm to gen
  • 有什么理由不对函数使用 INLINABLE pragma 吗?

    The 文档 http www haskell org ghc docs latest html users guide pragmas html states 函数 f 上的 INLINABLE f 编译指示具有以下行为 INLINE 表

随机推荐