单子变换器内部结构顺序

3

我一直在思考为什么Monad Transformers的结构顺序是例如m(Maybe(m(Maybe a))),而不是Maybe(m(Maybe( m a)))。我尝试实现了第二个,但由于我对Haskell的知识不足,可能无法完成。

在所有的monad transformers中,我们是否总是有这样的结构?如果是,为什么?如果不是,什么情况下选择其中一个?

newtype MaybeOT m a = MaybeOT {runMaybeOT :: Maybe (m  a) }
instance (Monad m) => Monad (MaybeOT m) where
return = pure
(MaybeOT mma) >>= f = MaybeOT $ 
                        case mma of
                            Nothing -> Nothing
                            (Just ma) -> inner ma where
                                         inner ma = do
                                            a <- ma
                                            undefined

你是不是指的是 where inner ma = ... 而不是 let - Will Ness
我曾经实现了一个Applicative实例,但甚至不确定它是否符合法律规定。 - leftaroundabout
@WillNess 修复了示例代码。 - hanan
1
主要问题在于,最终你必须对类型 m a 执行 bind/>>= 操作。然而,你得到的是返回类型为 MaybeOT (m b) 的函数。当然,对于任何 bm a 上的 bind 必须给出返回 m b 的函数。你可以从 MaybeOT (m b) 中获取 Maybe (m a)。但接下来呢?外部 monad 的信息是你所拥有的全部信息,也是你实现 monadic composition(transformers)所需的信息——然而,在这一步中,你需要将其丢弃。 - Chase
1
相比之下,对于 MaybeT,其中 Maybe 是内层单子。你只需一次在 m (Maybe a) 上进行绑定。在这里,你有机会检查内部的 Maybe 是否为 Nothing。在这种情况下,pure 帮助你构造外层单子。此外,给定的函数返回 MaybeT m b。你可以从中获得 m (Maybe b) - 这正是你成功在 m (Maybe a) 上进行 bind 所需要的。因此,它很好地工作了。你已经使用关于 Maybe 的信息以及它在实现中的具体表示,而不必将其丢弃。 - Chase
1
注意到你在使用 MaybeOT 时面临的主要问题是你被困在了 Maybe (m a),而你需要的是 m (Maybe a)这也是你在实现任意嵌套单子bind 时面临的主要问题。因此,需要使用单子变换器。有趣的是,可遍历的 单子具有交换外部和内部上下文的能力 - 因此使它们能够绕过所有这些问题。 - Chase
1个回答

1
您已经发现了单子变换器为什么是“内部”的而不是“外部”的(注:我相信这里使用的术语不正确,但您知道我的意思-请随时在这里纠正我)。将您知道的单子放在数据位置(内部)而不是容器位置(外部)要容易得多(易=约束较少)。直观地说,您可以看到为什么-毕竟,单子的本质要求容器必须相同。您可以对数据进行任何操作,但在绑定期间不得更改容器的类型。
当然,通过实际尝试实现荒谬的事情更容易实现所有这些。这就是这个问题所涉及的内容。在为MaybeOT实现>>=/bind时,您将被卡住的最后一步是这个-
m a >>= ??? . runMaybeOT . f

在这里,m 是一个 Monadm a :: m af :: a -> MaybeOT m a,以及 runMaybeOT :: MaybeOT m a -> Maybe (m a)(还有 ??? :: ???,呵呵)

现在,您必须使用容器类型 m 来获取一个具有绑定到 m a 成功的单子。但遗憾的是,您无法从 Maybe (m a) 中获取 m a!当它为 Nothing 时会发生什么?实现单子变换器的主要工具是关于您正在为其实现变换器的 特定单子 的知识。您的知识告诉您这是荒谬的。

相比之下,实现 MaybeT>>= 的最后一步非常顺利。为了完整起见,这里是 MaybeT 的-

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

在这里,你将绑定类型为 m (Maybe a)。在绑定期间,你必须返回类型为m b的任何b。你有一个类型为a -> MaybeT m b的函数f,你可以轻松从MaybeT m b获取m (Maybe b)
啊哈!现在你可以看到解决方案了。你获得的函数返回与你要绑定的相同的外部 monad 。这正是你需要的。在MaybeOT的情况下,你被卡在了绑定在monad m上,而函数没有返回值与外面的monad m
这就是关键所在-你获得的函数必须能够给你一个具有相同外部 monad 的值。这就是为什么MaybeT(以及其他变换器)将未知的 monad 保持在外部-因为当你实现>>=用于MaybeT时,你知道绑定函数将需要构造那个外部 monad。
至少从直觉上来说,需要注意的是你在这里卡住的问题——即实现单子组合时所面临的问题——与嵌套单子的>>=相同。单子不可组合!这就是为什么你需要单子转换器的原因!
如果你将这个问题分解开来,你会注意到,如果你只有一个可以交换单子的函数,从m (Maybe a)得到Maybe (m a),反之亦然——一切都会好起来。单子将组合,单子转换器可以看起来任何你喜欢的样子,事实上,单子转换器实际上没有任何用处。这个细节在上面链接的答案中已经注意到了。你所需要的只是——
swap :: (Monad m, Monad n) => m (n a) -> n (m a)

这里是英文文本,大意为:This exists。它们被称为可遍历的单子,如果你仅仅将Traversable约束添加到MaybeOTMonad实现中,你就能得到很好的效果。
instance (Traversable m, Monad m) => Monad (MaybeOT m) where
    return = pure
    MaybeOT Nothing >>= _   = MaybeOT Nothing
    MaybeOT (Just ma) >>= f = MaybeOT $ sequence $ ma >>= sequence . runMaybeOT . f

我认为它符合法律,但我需要检查一下。

另外,将MaybeOT变成符合法律的应用程序是完全可能的。毕竟,在实现Monad时面临的问题在这里根本不存在。应用程序确实可以组合。

instance Applicative f => Applicative (MaybeT f) where
    pure = MaybeT . pure . Just
    MaybeT f <*> MaybeT a = MaybeT $ liftA2 (<*>) f a

(或者,更简单地说)
instance Applicative f => Applicative (MaybeT f) where
    pure = MaybeT . pure . Just
    MaybeT f <*> MaybeT a = MaybeT $ (<*>) <$> f <*> a

根据我所知,应该符合法律规定。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接