如何设计monadic堆栈?

时间:2013-05-09 07:57:47

标签: design-patterns haskell types monad-transformers

如何设计和构建monadic堆栈?我第一次需要构建一个monadic堆栈(使用变换器)来解决现实世界的问题,但我并不完全确定在哪个 order 中堆叠变换器。正如您所知,只要计算具有类* -> *,基本上任何东西都可以在变换器中扮演内部monad的角色,因此有几个问题:

  • 某个特定的变压器应该位于堆栈的顶部(例如ReaderT?WriterT?)
  • 什么应该驱动设计?直觉?类型? (例如,根据API的需要调整堆栈)
  • 每个堆栈是否彼此同形(在某种程度上)或者可能是,如果我错误地构建堆栈,我可能最终无法使用某些底层monad或者有一个大的混乱{ {1}}?我的直觉是,如果变形金刚得到一些实例(例如MonadReader,MonadIO等,就像lift . lift . liftIO [...]中的大多数变换器一样),那么放置变换器的顺序并不重要。

我有兴趣听取经验丰富的Haskellers关于最佳实践或经验法则的意见。

mtl

一个。

2 个答案:

答案 0 :(得分:23)

需要经验。要记住的一点是,monad变换器对它正在变换的monad一无所知,所以外层变换器被内部行为“绑定”。所以

StateT s (ListT m) a

首先是因为内部monad而不确定的计算。然后,正常地采取非确定性,你可以添加状态 - 即非确定性的每个“分支”都有自己的状态。

ListT (StateT s m) a约束,它主要是有状态的 - 即整个计算只有一个状态(模m),并且计算将在状态中扮演“单线程”状态,因为那是State的意思。非确定性将是最重要的 - 所以分支将能够观察先前失败的分支的状态变化。 (在这个特殊的组合中,这真的很奇怪,我从来不需要它。)

以下是Dan Piponi的diagram,它提供了一些有用的直觉:

monad doodles

我还发现扩展到实现类型很有帮助,让我感觉它是什么样的计算。 ListT难以扩展,但您可以将其视为“非确定性”,StateT很容易扩展。所以对于上面的例子,我会看看

StateT s (ListT m) a =~ s -> ListT m (a,s)

即。它需要一个传入状态,并返回许多传出状态。这可以让您了解它是如何工作的。类似的方法是查看堆栈所需的run函数的类型 - 这是否与您拥有的信息和所需信息相匹配?

以下是一些经验法则。它们无法代替花时间通过扩展和观察找出你真正需要的那个,但如果你只是想要在一种强制意义上寻找“添加功能”,那么这可能会有所帮助。

ReaderTWriterTStateT是最常见的变形金刚。首先,他们都互相通勤,所以你把它们放在哪个顺序是无关紧要的(如果你使用全部三个,请考虑使用RWS)。另外,在实践中,我通常在外面想要这些变体,内部有ListTLogicTContT等“更丰富”的变形金刚。

ErrorTMaybeT通常在上述三个之外;让我们看一下MaybeTStateT的互动方式:

MaybeT (StateT s m) a =~ StateT s m (Maybe a) =~ s -> m (Maybe a, s)
StateT s (MaybeT m) a =~ s -> MaybeT m (a,s) =~ s -> m (Maybe (a,s))

MaybeT位于外部时,即使计算失败,也可以观察到状态变化。当MaybeT位于内部时,如果计算失败,则不会出现状态,因此必须中止在失败计算中发生的任何状态更改。你想要哪一个取决于你想要做什么 - 然而,前者对应于命令式程序员的直觉。 (并不是说这是必须要努力的事情)

我希望这能让您了解如何考虑变换器堆栈,因此您可以使用更多工具来分析堆栈的外观。如果你将问题确定为monadic计算,那么让monad成为正确的决定是最重要的决定之一,而且并不总是那么容易。花点时间探索各种可能性。

答案 1 :(得分:12)

这是一个非常广泛的问题。我只想给你一些基本的想法。

首先,我建议尽可能保持基础monad多态。这将允许您在纯设置和IO设置中重用代码。这也将使您的代码更具组合性。使用像MonadIO这样的各种类也可以帮助保持代码更加多态,这通常是一件好事。

需要注意的一件重要事情是,monad变换器的顺序实际上控制了它们的语义。我最喜欢的例子是将ListT¹与EitherT结合起来进行错误处理。如果外部有ListT,则整个计算可能会失败并显示错误。如果外部有EitherT,则每个分支都可能单独失败。因此,只需改变变换器的顺序,就可以实际控制错误与非确定性相互作用的方式!

如果您使用的monad变压器不依赖于订单 - 例如。我认为,将ReaderTWriterT结合起来并不重要 - 然后只需用耳朵播放,然后选择最适合您应用的内容。这种选择随着经验变得更容易。

¹:来自ListT的{​​{1}}有一些问题,所以假设它是ListT done right