这需要经验。要记住的一件事是,monad 转换器对它正在转换的 monad 一无所知,因此外部的 monad 受到内部行为的“约束”。所以
StateT s (ListT m) a
由于内部单子,首先是不确定的计算。然后,以非确定性为常态,添加状态——即非确定性的每个“分支”都有自己的状态。
与 相比ListT (StateT s m) a
,它主要是有状态的——即整个计算只有一个状态(模m
),并且计算将在状态中“单线程”运行,因为这就是State
意思。非确定性将是最重要的——因此分支将能够观察到先前失败分支的状态变化。(在这个特殊的组合中,这真的很奇怪,而且我从来不需要它)。
这是Dan Piponi的图表,它提供了一些有用的直觉:
我还发现扩展实现类型很有帮助,让我了解它是什么类型的计算。 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 正确是最重要的决定之一,而且并不总是那么容易。花点时间探索各种可能性。