4

例如,拥有...

consumer :: Proxy p => () -> Consumer p a (EitherT String IO) ()
producer :: Proxy p => () -> Producer p a (EitherT ByteString IO) r

...我如何使这项工作?

session :: EitherT ByteString (EitherT String IO) ()
session = runProxy $ producer >-> consumer

注意:我读过Mixing Base Monads in Control.Proxy.Tutorial。我得到了第一个示例,但无法理解人为的示例。更多的例子,不是那么明显但不是那么做作,可能会阐明如何使用hoistlift匹配基本单子的任何组合。

4

3 回答 3

8

假设你有一个单子变压器堆栈,比如基础单子MT1 MT2 MT3 M a在哪里M

使用lift,您可以在左侧添加一个新的 monad 转换器。它可以是任何变压器,所以我们用 来表示它?

lift :: MT1 MT2 MT3 M a -> ? MT1 MT2 MT3 M a

使用hoist,您可以操作最左侧元素右侧的 monad 堆栈。怎么操作?例如通过提供lift

hoist lift :: MT1 MT2 MT3 M a -> MT1 ? MT2 MT3 M a

使用 和 的组合hoistlift您可以在 monad 转换器堆栈中的任何位置插入这些“通配符”。

hoist (hoist lift) :: MT1 MT2 MT3 M a -> MT1 MT2 ? MT3 M a

hoist (hoist (hoist lift)) :: MT1 MT2 MT3 M a -> MT1 MT2 MT3 ? M a

此技术可用于均衡您示例中的两个 monad 堆栈。

于 2013-02-14T15:09:34.183 回答
5

实际上有两种解决方案。

第一个解决方案是 Daniel Wagner 提出的:修改两个基本单子以使用相同的Left类型。例如,我们可以将它们标准化为都使用ByteString. 为此,我们首先采用ByteString'spack函数:

pack :: String -> ByteString

然后我们将其提升以处理 an 的左值EitherT

import Control.Error (fmapLT)  -- from the 'errors' package

fmapLT pack :: (Monad m) => EitherT String m r -> EitherT ByteString m r

现在我们需要将该转换定位到您Consumer的基本 monad,使用hoist

hoist (fmapLT pack)
 :: (Monad m, Proxy p)
 => Consumer p a (EitherT String m) r -> Consumer p a (EitherT ByteString m) r

现在您可以直接与您的生产者组合您的消费者,因为它们具有相同的基本单子。

第二种解决方案是 Daniel Diaz Carrete 提出的解决方案。相反,您可以让您的两个管道就包含两个EitherT层的通用 monad 转换器堆栈达成一致。您所要做的就是决定以何种顺序嵌套这两层。

假设您选择将变压器分层放置在EitherT String变压器之外EitherT ByteString。这意味着您的最终目标 monad 转换器堆栈将是:

(Proxy p) => Session (EitherT String (EitherT ByteString p)) IO r

现在您需要提升两个管道以针对该变压器堆栈。

对于您的Consumer,您需要EitherT ByteString在两者之间插入一个层,EitherT String如果IO您想匹配最终的 monad 转换器堆栈。创建层很容易:您只需使用lift,但您需要在这两个特定层之间定位提升,因此您使用hoist, 两次,因为您需要跳过代理单子转换器和EitherT String单子转换器:

hoist (hoist lift) . consumer
 :: Proxy p => () -> Consumer p a (EitherT String (EitherT ByteString IO)) ()

对于您的,如果要匹配最终的 monad 转换器堆栈,则需要在代理 monad 转换器和转换器之间Producer插入一个层。同样,创建层很容易:我们只使用,但您需要在这两个特定层之间定位该提升。你只是,但这次你只使用它一次,因为你只需要跳过代理 monad 转换器就可以将它放置在正确的位置:EitherT StringEitherT ByteStringlifthoistlift

hoist lift . producer
 :: Proxy p => () -> Producer p a (EitherT String (EitherT ByteString IO)) r

现在您的生产者和消费者拥有相同的 monad 转换器堆栈,您可以直接组合它们。

现在,您可能想知道:这个hoistinglift的过程是否在做“正确的事情”?答案是肯定的。范畴论的部分神奇之处在于,我们可以严格定义正确插入“空 monad 转换器层”的lift含义,并且我们可以类似地hoist通过指定几个理论启发的法律并验证lifthoist遵守这些法律。

lift一旦我们满足了这些法则,我们就可以忽略所有关于具体做什么和做什么的细节hoist。类别理论让我们可以在非常高的抽象级别上工作,我们只需要考虑在 monad 转换器之间在空间上“插入升降机”,而代码神奇地将我们的空间直觉转化为严格正确的行为。

我的猜测是您可能想要第一个解决方案,因为您可以在单层中共享生产者和消费者之间的错误处理EitherT

于 2013-02-14T17:14:52.023 回答
5

一包(我猜你的EitherT类型来自哪里)提供了几个用于修改第一个参数的函数,例如

bimapEitherT :: Functor m => (e -> f) -> (a -> b) -> EitherT e m a -> EitherT f m b

您可以使用它以及一些适当的编码(或解码)将 anEitherT String IO a转换为 an EitherT ByteString IO a(反之亦然),然后hoist将该转换转换为Consumeror Producermonad 转换器。

于 2013-02-14T14:36:23.643 回答