【问题标题】:How to handle state from external systems functionally?如何在功能上处理来自外部系统的状态?
【发布时间】:2012-04-23 07:29:36
【问题描述】:

我最近接触了函数式编程,并学习了几种以引用透明的方式处理某些副作用的方法:

  • the State monad 用于可变状态,例如更新变量
  • the IO monad 用于 I/O,例如从控制台读/写到控制台
  • FRP 用于图形和输入设备事件等交互性

但现在大多数“现实世界”应用程序都与外部系统(如 Web 服务、数据库等)交互,这些系统可以由多个用户同时修改,它们具有状态、长时间运行的操作等。所以情况是不像上述类别那么简单:向系统询问实体的状态或试图控制它的结果取决于它的状态。此外,交互性也是一个要求:有一些用户可以任意点击的 GUI,也许我们还必须对来自系统的变化做出自动反应。

通过最大化纯函数的好处来设计和实现这类应用程序的模式是什么?或者上述一些方法可以以我没有想到的方式应用于这个问题吗?该语言(比如 Java 或 Scala)并不强制要求 100% 的纯度,所以我对实践经验支持的实用解决方案感兴趣。

【问题讨论】:

    标签: functional-programming referential-transparency architectural-patterns


    【解决方案1】:

    我还没有完成很多实际以实际方式完成的此类事情,因此希望其他人能够做得更多。然而,我已经用 Scala 编写了一个 Android 应用程序,它有你的几个要求; UI 是交互式的,“状态”全部存储在 SQLite 数据库中。数据库和 UI 都需要与 Android 框架交互,这些框架非常面向 Java,因此不太适合 Scala 或函数式编程。

    我所做的是采用类似于 MVC 设计的东西,其中模型部分被实现为一组仅支持纯操作的 ADT。这样做的另一个好处是模型代码完全独立于 Android 框架,因此可以在模拟器之外以任何我喜欢的方式对其进行测试。

    这给我留下了控制器(视图是一个非常薄的层,主要是配置,与 Android 的工作方式一样),加上“从数据库加载模型”和“将模型保存到数据库”。作为 Scala,我只是用不纯代码实现了这些部分,这些代码调用纯模型代码来进行实际的数据操作。在 Haskell 中,这些部分可能完全在 IO monad[1] 中。

    总之,这让我能够以纯粹的功能性术语来思考我的问题域,而不必在与外部系统交互时“违背常规”。数据库层变成了数据库模式和我用于数据模型的 ADT 之间的映射问题;处理这些操作可能失败的事实是控制器(启动数据库操作)的责任,并且不会影响模型。控制器操作在概念上变得非常简单,例如“按下此按钮时,将当前状态设置为在当前状态上调用函数的结果,然后更新显示表”。最后,这种不纯的“胶水”代码比实际的模型代码要多得多,但我仍然认为以这种方式编写程序是一种胜利,因为我的应用程序的核心是处理复杂的结构化数据,所以做到这一点是最棘手的。其余的主要是编写乏味而不是难以设计。

    当您的程序中有大量计算(不一定是大量数据,只是您实际在其上进行计算)时,此方法有效。如果该程序几乎完全将不同的外部系统粘合在一起,那么您不一定有那么多收获。


    [1] 请记住,IO monad 不仅仅用于从控制台读取/写入。结果受程序外部状态影响的建模操作正是 IO monad 用于的;通常,如果 IO monad 不是一直与外部系统交互(或者最好即使是),Haskellers 会尽量避免在几乎整个程序中使用 IO monad。您可以对数据进行纯计算以响应来自“外部”的事件,或者甚至对响应外部事件需要执行的 IO 操作进行纯计算(如果这很复杂)。

    【讨论】:

      【解决方案2】:

      但现在大多数“现实世界”应用程序都与外部系统(如 Web 服务、数据库等)交互,这些系统可以由多个用户同时修改,它们具有状态、长时间运行的操作等。所以情况是不像上述类别那么简单:向系统询问实体的状态或试图控制它的结果取决于它的状态。此外,交互性也是一个要求:有一些用户可以任意点击的 GUI,也许我们还必须自动响应来自系统的变化。

      交互式地同时编辑共享状态只是状态单子的另一个示例。您可能会使用镜头或其他抽象来对数据结构进行编辑,但在后台您所拥有的只是共享的全局状态。

      如果您想要机器级并发支持,您可以使用并发结构(例如 STM var 或 MVar )来解决并发编辑的冲突。这意味着您将处于 STM 或 IO monad 中。

      在 Hackage 和 Haskell 包中,有很多为此类作业设计的单子环境示例。

      【讨论】:

        【解决方案3】:

        Asynchronous streams a.k.a. iteratees 似乎是一个有用且相关的抽象,似乎值得进一步探索它们......

        【讨论】:

          【解决方案4】:

          我对实践经验支持的务实解决方案感兴趣。

          GHC 用来提供不同标识符的 Unique 类型可能会让您感兴趣 - 可以在 John Launchbury 和 Simon Peyton Jones 的 State in Haskell 的第 39-41 页上找到基于相同方法的教学实施。这种方法可以追溯到 F. Warren Burton 的 Nondeterminism with Referential Transparency in Functional Programming Languages,其中还简要描述了使用 pseudo-data 在运行时访问信息以提供 timestampspacestamp 值。 p>

          既然你提到了IO

           -- abstract; single-use I/O source
          data Exterior
          getchar :: Exterior -> Char
          putchar :: Char -> Exterior -> ()
          
           -- from section 2 of Burton's paper
          data Tree a = Node { contents :: a,
                               left     :: Tree a,
                               right    :: Tree a }
          
           -- utility definitions
          type OI  =  Tree Exterior
          
          getChar' :: OI -> Char
          getChar' =  getchar . contents
          
          putChar' :: Char -> OI -> ()
          putChar' c = putchar c . contents
          
          part     :: OI -> (OI, OI)
          parts    :: OI -> [OI]
          
          part t   =  (left t, right t)
          parts t  =  let !(t1, t2) = part t in
                      t1 : parts t2
          

          现在从 Philip Wadler 的 How to Declare an Imperative 定义运行示例:

          echo     :: OI -> ()
          echo t   =  let !(t1:t2:t3:_) = parts t in
                      let !c = getChar' t1 in
                      if c == '\n' then
                         ()
                      else
                         let !_ = putChar' c t2 in
                         echo t3
          

          连同IO:

          type IO a = OI -> a
          
          unit     :: a -> IO a
          unit x   =  \ t -> let !_ = part t in x
          
          bind     :: IO a -> (a -> IO b) -> IO b
          bind m k =  \ t -> let !(t1, t2) = part t
                                 !x = m t1
                                 !y = k x t2
                             in y  
          
          {-
            getChar' :: IO Char
            putChar' :: Char -> IO ()
            echo     :: IO ()
          -}
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2017-02-10
            • 1970-01-01
            • 1970-01-01
            • 2021-05-09
            • 1970-01-01
            • 1970-01-01
            • 2021-11-18
            相关资源
            最近更新 更多