15

文档Control.Monad.Trans.Error提供了这个组合两个 monad 的例子:

type ErrorWithIO e a = ErrorT e IO a
==> ErrorT (IO (Either e a))

我发现这有悖常理:即使ErrorT据说是wrapping IO,看起来错误信息已被注入IO 操作的结果类型中。我原以为会是

==> ErrorT (Either e (IO a))

基于“包装”一词的通常含义。

为了使事情更加混乱,StateT每个都有一些:

type MyError e = ErrorT e Identity  -- (see footnote)
type StateWithError s e a = StateT s (MyError e) a
==> StateT (s -> ErrorT (Either e (a, s)))

状态类型s已经注入到Either'Right端,但整体Either也被包裹在一个函数中。

令人困惑的是,如果将 monad 反过来组合:

type ErrorWithState e s a = ErrorT e (State s) a
==> ErrorT (StateT (s -> (Either e a, s)))

“外部”仍然是一个函数;它不会产生类似的东西Either e (s -> (a, s)),其中状态函数嵌套在错误类型中。

我确信这一切都有一些潜在的逻辑一致性,但我不太明白。因此,我发现很难思考将一个 monad 与另一个结合起来意味着什么,即使我可以毫不费力地理解每个 monad 各自的含义。

有人可以启发我吗?


脚注:为了说明的目的,我正在编写ErrorT这样Identity并且StateWithError彼此ErrorWithState一致。通常我只是使用StateWithError s e a = StateT s (Either e) a并放弃该ErrorT图层。

4

2 回答 2

18

我发现这违反直觉:即使 ErrorT 应该包装 IO,看起来错误信息已被注入到 IO 操作的结果类型中。

Monad 转换器通常不会“包装”它们所应用的 monad,至少在任何明显的意义上都没有。将其视为“包装”会在我的脑海中暗示函子组合,这尤其是这里没有发生的事情。

为了说明, 和 的函子组合State sMaybe随着定义的扩展,看起来像这样:

newtype StateMaybe s a = StateMaybe (s -> (Maybe a, s))    -- == State s (Maybe a)
newtype MaybeState s a = MaybeState (Maybe (s -> (a, s)))  -- == Maybe (State s a)

请注意,在第一种情况下,State行为正常,Nothing不会影响状态值;在第二种情况下,我们要么有一个普通的State函数,要么什么都没有。在这两种情况下,这两个 monad 的特征行为实际上都没有结合。这应该不足为奇,因为毕竟,这些与您通过简单地将值使用一个 monad 作为在另一个 monad 中使用的常规值所得到的相同。

将此与StateT s Maybe

newtype StateTMaybe s a = StateTMaybe (s -> Maybe (a, s))

在这种情况下,两者是交织在一起的;对于 ,事情以正常方式进行State,除非我们点击 a Nothing,在这种情况下计算被中止。这与上述情况根本不同,这就是为什么monad 转换器甚至首先存在的原因——天真地组合它们不需要任何特殊的机器,因为它们彼此独立运行。


就理解哪个在“外部”而言,在处理单子中的值时,将“外部”转换器视为其行为“优先”的转换器可能会有所帮助,而“内部” monad 只看到一切照旧。请注意,这就是为什么IO总是最里面的原因——它不会让其他任何事情发生在它的业务中,而假设的IOT转换器将被迫允许被包装的 monad 进行各种恶作剧,比如复制或丢弃RealWorld令牌。

  • StateT并且ReaderT都将“内部”单子放在函数的结果周围;在获得转换后的 monad 之前,您必须提供状态值或环境。

  • MaybeT并且ErrorT都将自己滑入转换后的 monad,确保它可以以通常的方式运行,除了可能不存在的值

  • Writer是完全被动的,只是依附于 monad 中的值,因为它根本不影响行为。

  • ContT将事情留给自己,通过仅包装结果类型来完全推迟处理转换后的 monad 。

这有点手忙脚乱,但是,单子转换器是一种临时的,一开始就令人困惑,唉。我不知道做出的特定选择是否有任何合理的理论依据,除了它们起作用的事实,并且做你通常希望两个单子的组合(而不是组合)做的事情。

因此,我发现很难思考将一个 monad 与另一个结合起来意味着什么,即使我可以毫不费力地理解每个 monad 各自的含义。

是的,这听起来像是预期的结果,我害怕。

于 2011-08-17T05:36:55.223 回答
5

考虑一下会发生什么,如果 ErrorT 以您想象的方式定义。您将如何编码失败的 IO 操作?当动作失败时,Either e (IO a)你不能给出一个Left值,因为在你到达动作的时候,它已经很清楚它是一个Right值——否则它就不是一个动作。

然而,IO (Either e a)情况并非如此。整个事情现在是一个 IO 操作,可以返回一个Left值来指示错误。正如其他人所指出的,不要将 monad 转换器视为包装器。而是将它们视为功能。他们把一个单子变成另一个单子。它们转换单子。

于 2011-08-17T08:54:36.380 回答