如何设计和构建monadic堆栈?我第一次需要构建一个monadic堆栈(使用变换器)来解决现实世界的问题,但我并不完全确定在哪个 order 中堆叠变换器。正如您所知,只要计算具有类* -> *
,基本上任何东西都可以在变换器中扮演内部monad的角色,因此有几个问题:
lift . lift . liftIO [...]
中的大多数变换器一样),那么放置变换器的顺序并不重要。我有兴趣听取经验丰富的Haskellers关于最佳实践或经验法则的意见。
mtl
一个。
答案 0 :(得分:23)
需要经验。要记住的一点是,monad变换器对它正在变换的monad一无所知,所以外层变换器被内部行为“绑定”。所以
StateT s (ListT m) a
首先是因为内部monad而不确定的计算。然后,正常地采取非确定性,你可以添加状态 - 即非确定性的每个“分支”都有自己的状态。
与ListT (StateT s m) a
约束,它主要是有状态的 - 即整个计算只有一个状态(模m
),并且计算将在状态中扮演“单线程”状态,因为那是State
的意思。非确定性将是最重要的 - 所以分支将能够观察先前失败的分支的状态变化。 (在这个特殊的组合中,这真的很奇怪,我从来不需要它。)
以下是Dan Piponi的diagram,它提供了一些有用的直觉:
我还发现扩展到实现类型很有帮助,让我感觉它是什么样的计算。 ListT
难以扩展,但您可以将其视为“非确定性”,StateT
很容易扩展。所以对于上面的例子,我会看看
StateT s (ListT m) a =~ s -> ListT m (a,s)
即。它需要一个传入状态,并返回许多传出状态。这可以让您了解它是如何工作的。类似的方法是查看堆栈所需的run
函数的类型 - 这是否与您拥有的信息和所需信息相匹配?
以下是一些经验法则。它们无法代替花时间通过扩展和观察找出你真正需要的那个,但如果你只是想要在一种强制意义上寻找“添加功能”,那么这可能会有所帮助。
ReaderT
,WriterT
和StateT
是最常见的变形金刚。首先,他们都互相通勤,所以你把它们放在哪个顺序是无关紧要的(如果你使用全部三个,请考虑使用RWS
)。另外,在实践中,我通常在外面想要这些变体,内部有ListT
,LogicT
和ContT
等“更丰富”的变形金刚。
ErrorT
和MaybeT
通常在上述三个之外;让我们看一下MaybeT
与StateT
的互动方式:
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变压器不依赖于订单 - 例如。我认为,将ReaderT
和WriterT
结合起来并不重要 - 然后只需用耳朵播放,然后选择最适合您应用的内容。这种选择随着经验变得更容易。
¹:来自ListT
的{{1}}有一些问题,所以假设它是ListT
done right。