实际上有两种解决方案。
第一个解决方案是 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 String
EitherT ByteString
lift
hoist
lift
hoist lift . producer
:: Proxy p => () -> Producer p a (EitherT String (EitherT ByteString IO)) r
现在您的生产者和消费者拥有相同的 monad 转换器堆栈,您可以直接组合它们。
现在,您可能想知道:这个hoist
inglift
的过程是否在做“正确的事情”?答案是肯定的。范畴论的部分神奇之处在于,我们可以严格定义正确插入“空 monad 转换器层”的lift
含义,并且我们可以类似地hoist
通过指定几个理论启发的法律并验证lift
并hoist
遵守这些法律。
lift
一旦我们满足了这些法则,我们就可以忽略所有关于具体做什么和做什么的细节hoist
。类别理论让我们可以在非常高的抽象级别上工作,我们只需要考虑在 monad 转换器之间在空间上“插入升降机”,而代码神奇地将我们的空间直觉转化为严格正确的行为。
我的猜测是您可能想要第一个解决方案,因为您可以在单层中共享生产者和消费者之间的错误处理EitherT
。