如何从外部系统正常处理状态



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

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

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

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

我还没有做很多这种实际上以实际方式完成的事情,所以其他人希望能够做更多的事情。然而,我已经用 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 操作进行纯计算(如果这很复杂)。

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

交互式地,并发编辑共享状态只是状态 monad 的另一个示例。您可以使用镜头或其他一些抽象来对数据结构进行编辑,但在幕后,您所拥有的只是共享的全局状态。

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

在Hackage上,为Haskell软件包设计了很多很多一元环境的例子。

异步流又名迭代似乎是一个有用且相关的抽象,似乎值得进一步探索它们......

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

GHC用于提供不同缩进器的Unique类型可能会引起您的兴趣 - 基于相同方法的教学实施可以在John Launchbury和Simon Peyton Jones的Haskell状态的第39-41页上找到。这种方法可以追溯到F. Warren Burton的《函数式编程语言中的引用透明度的非确定性》,该书还简要描述了使用伪数据在运行时访问信息以提供timestampspacestamp值。

既然你提到了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 ()
-}

最新更新