【问题标题】:How do you structure a stateful module in Haskell?你如何在 Haskell 中构建一个有状态的模块?
【发布时间】:2011-06-14 16:44:01
【问题描述】:

我正在寻找一个通用模块,允许 Haskell 程序与 Cassandra 交互。模块需要维护自己的状态。例如,它将有一个连接池和一个在保存新记录时要调用的回调列表。我应该如何构造代码以便该模块可以保持其状态?以下是我一直在考虑的一些方法。我在正确的轨道上吗? (我是 Haskell 的新手,仍在学习函数式思考的最佳方法。)

选项 1:

模块在 (StateT s IO) monad 中运行,其中 s 是使用 Cassandra 模块的整个程序的全局状态。当然,由于 Cassandra 模块可以被多个程序使用,所以 s 中的内容的详细信息应该对 Cassandra 模块是不可见的。该模块必须导出一个类型类,使其能够从 s 中提取 CassandraState 并将新的 CassandraState 推回 s 中。然后,任何使用该模块的程序都必须使其主状态成为该类型类的成员。

选项 2:

模块在 (StateT CassandraState IO) monad 中运行。每次有人在模块中调用一个操作时,他们都必须从他们隐藏的任何地方提取 CassandraState,使用 runState 调用该操作,然后获取结果状态并再次将其隐藏(无论在哪里)。

选项 3:

根本不要将 Cassandra 模块的函数放在 StateT monad 中。相反,让调用者在需要时显式传递 CassandraState。选项 2 的问题是,并非模块中的所有函数都会修改状态。例如,获取连接将修改状态,并要求调用者隐藏结果状态。但是,保存新记录需要读取状态(以获取回调),但不需要更改状态。选项 2 不会给调用者任何暗示 connect 会改变状态,而 create 不会。

但是,如果我不再使用 StateT monad,而只使用将状态作为参数并返回简单值或简单值和新状态的元组的函数,那么当状态需要时调用者会很明显得救了。 (在我的模块中,我会获取传入的状态并将它们构建到一个 (StateT CassandraState IO) monad 中,但是这个细节将对调用者隐藏。所以,对于调用者来说,接口是非常明确的,但在幕后,它只是选项 2。)

选项 4:

还有什么?

在构建可重用模块时,这个问题必须经常出现。有什么标准的方法可以解决吗?

(顺便说一句,如果有人知道从 Haskell 与 Cassandra 交互比使用 Thrift 更好的方法,请告诉我!也许我根本不需要写这个。:-)

【问题讨论】:

  • 仅供参考 - 在 Haskell 圈子中,“模块”是编译单元,即 - 单个源文件。尽管对于您所描述的内容,我真的没有更好的名词,但谈到具有状态的“模块”时,我还是有一段时间。
  • 糟糕。我应该说“包”或“库”。

标签: haskell


【解决方案1】:

类似于 HDBC 模型的东西将具有明确的 CassandraConnection 数据类型。它内部有一个带有一些可变状态的 MVar。因为无论如何我想你的所有动作都在 IO 中,所以他们可以将 CassandraConnection 作为这些动作的参数。然后,用户可以将该连接打包到 state 或 reader monad 中,或显式线程化,或做任何他们想做的事情。

在内部,您可以使用或不使用 monad - 这真的是您的决定。但是,我更喜欢那些尽可能不强迫用户进入任何特定 monad 的 API,除非确实有必要。

所以这是选项 3 的一种版本。但用户不应该真正关心他们是否正在更改连接状态 - 在那个级别上,您可以真正隐藏细节。

【讨论】:

  • 我刚刚发现另一个问题是关于使用 MVar 汇集 HDBC 连接。我认为这说明了你的建议。 stackoverflow.com/questions/1141677/…
  • 另外,如果您将所有函数的类型构造为以... -> CassandraConnection -> IO a 结尾,您的用户也可以通过ReaderT monad 转换器非常轻松地获得与选项2 相同的抽象。他们可以简单地将库函数包装在ReaderT 构造函数中。例如,foo :: A -> B -> CassandraConnection -> IO C 被包装为:myFoo a b = ReaderT (foo a b)
  • @Clint 似乎很相似。你基本上想要一个资源的抽象句柄,具有连接、断开等显式操作。
  • 我认为使用 MVar 来保存状态是我缺少的关键部分。 MVar 似乎提供了一种将可变状态隐藏在 IO monad 中的非常简单的方法……它可以帮助您同时获得线程安全的代码。
【解决方案2】:

我会选择选项 2。您模块的用户不应直接使用 runState;相反,您应该提供一个不透明的Cassandra 类型,其中包含Monad 类型类的实例和一些runCassandra :: Cassandra a -> IO a 操作以“转义”Cassandra。您的模块导出的操作应该都在Cassandra monad 中运行(例如doSomethingInterestingInCassandra :: Int -> Bool -> Cassandra Char),并且它们的定义可以访问包装的CassandraState

如果您的用户需要为他们的应用程序提供一些额外的状态,他们总是可以在 Cassandra 周围包裹一个 monad 转换器,例如StateT MyState Cassandra.

【讨论】:

  • 感谢您的回答。假设一个应用程序不仅在使用我的 Cassandra 模块,而且还使用了许多其他有状态的模块。还假设应用程序需要定义自己的状态(MyState)并在 IO monad 中运行。您是否最终会在 StateT 中嵌入一组令人讨厌的 StateT,以及为了从中获得任何东西而进行的令人讨厌的提升序列?当应用程序开始使用新的有状态模块或停止使用有状态模块时会发生什么?这会搞乱所有嵌套的 StateT 和提升吗?
  • @ClintMiller 是绝对正确的。 Youtube 视频让我被这个“每个模块的 monad 转换器堆栈”模型愚弄了,但在实践中它很可怕。我认为只有主模块应该在 monad 转换器中,而像 DB 访问或 App 逻辑模块这样的“侧模块”应该只是简单的函数,要求他们从调用者那里完成工作所需的任何状态。当每次调用 API 都需要解包(也就是运行)它们的 monad 时,为子模块使用 monad 有什么意义?
猜你喜欢
  • 2023-03-16
  • 2011-12-07
  • 2018-03-30
  • 2020-11-09
  • 1970-01-01
  • 2021-11-07
  • 2010-09-13
  • 2014-10-17
  • 2023-01-31
相关资源
最近更新 更多