这需要经验。要记住的一件事是,monad 转换器对它正在转换的 monad 一无所知,因此外部的 monad 受到内部行为的“约束”。所以
StateT s (ListT m) a
首先是由于内部单子而导致的不确定性计算。然后,将不确定性视为正常,添加状态 - 即不确定性的每个“分支”都有自己的状态。
对比ListT (StateT s m) a,它主要是有状态的——即整个计算只有一个状态(模m),计算将在该状态下“单线程”运行,因为这就是@ 987654327@ 表示。非确定性将是最重要的——因此分支将能够观察到先前失败分支的状态变化。 (在这种特殊的组合中,这真的很奇怪,而且我从来不需要它)。
这是 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 正确是最重要的决定之一,而且并不总是那么容易。花点时间探索各种可能性。