假设我有一个函数,它包含两个monad操作:
co :: Monad m => m a -> m a -> m a
你可以把co
看作一个更高阶的函数,它描述了两个一元动作如何相互合作来完成一项任务。
但现在我发现第一个一元动作可能被封装在一个一元变换器中,而第二个动作不是:
one :: (MonadTrans t, Monad m) => t m a
two :: Monad m => m a
但我仍然想把它们组合在一起,所以我需要一个函数:
co' :: (MonadTrans t, Monad m) => t m a -> m a -> t m a
使得第一t m a
可以通过将所有m
原语提升到上下文t
中来与m a
协作。
这里的技巧是在不知道m
或t
的实现的情况下构建co
。我觉得答案就在MFunctor软件包的某个地方,事实上昨天我也问了一个类似的问题。但是不能想出什么好主意,有什么想法吗?
您可以使用mmorph
包中的hoist
来执行此操作。hoist
允许您修改任何实现MFunctor
(它是大多数monad转换器)的基本monad:
hoist :: (MFunctor t) => (forall x . m x -> n x) -> t m r -> t n r
然后你可以用它来解决你的问题,比如:
co' tma ma = hoist (co ma) tma
那你完了!
为了理解为什么这样做,让我们一步一步地了解类型:
co :: (Monad m) => m a -> m a -> m a
ma :: (Monad m) => m a
co ma :: (Monad m) => m a -> m a
hoist (co ma) :: (Monad m, MFunctor t) => t m a -> t m a
tma :: (Monad m, MFunctor t) => t m a
hoist (co ma) tma :: (Monad m, MFunctor t) => t m a
请注意,hoist
有一些必须满足的定律,以确保它做了你所期望的"正确的事情":
hoist id = id
hoist (f . g) = hoist f . hoist g
这些只是函子定律,保证了hoist
的行为直观。
hoist
在mmorph
包的Control.Monad.Morph
模块中提供,您可以在此处找到。主模块的底部有一个教程,教您如何使用包
快速定义
您需要t m
拥有一个monad实例才能使其工作。
import Control.Monad
import Control.Monad.Trans
co :: Monad m => m a -> m a -> m a
co = undefined
co' :: (MonadTrans t, Monad m, Monad (t m)) => t m a -> m a -> t m a
co' one two = lift . flip co two . return =<< one
对这个问题的另一种看法
从transformers
包中看到lift
的定义应该可以帮助您找到答案,因为您想要类似的
lift . return = return
lift (m >>= f) = lift m >>= (lift . f)
因为你有flip co one :: Monad m => m a -> m a
,你想把它举起来得到一个类型为(MonadTrans t, Monad m, Monad (t m)) => t m a -> t m a
的函数。所以跟随电梯的脚步
lift' :: (MonadTrans t, Monad m, Monad (t m)) => (m a -> m a) -> t m a -> t m a
lift' f tma = tma >>= (lift . f . return)
现在定义co'
是琐碎的
co' one two = lift' (flip co two) one
问题
上面的解决方案只是满足类型,但它们也满足语义吗?为了解决这个问题,我们使用一个co
函数,它总是返回第二个操作,甚至不看第一个。现在,上面的co'
将永远无法做到这一点,因为它总是在决定任何事情之前运行第一个操作。因此,即使在co
中没有发生第一作用的副作用,在co'
中仍然会发生第一作用。
是否存在一般解决方案
我想不会。因为您想要实际实现该函数的是一个类似t m a -> (m a -> m b) -> t m b
的函数,在这里您需要从t m a
中获得操作m a
,而不需要实际运行m a
的副作用。
因此,假设m
是IO
monad,t
是具有某种状态的状态转换器,并且你的one
动作实际上发射了一枚导弹,并且根据其成功或失败来修改State
。现在你想要的是在不发射导弹的情况下实际修改状态。总的来说,这是不可能的。
如果您知道关于co
的一些信息,比如首先运行哪个操作,那么您可以使用上面的方法来实现co'
。
monad控制包可用于将函数和控制操作从基本monad提升到monad转换器中,甚至是那些进行回调的函数。
提升的基本包构建在monad控件上,包含许多处理异常、并发等常见函数的提升版本。。。特别是,它有一个提升版的finally from Control.Exception,其签名与co
的签名非常相似。
以下是一些链接:
http://www.yesodweb.com/book/monad-control
http://www.yesodweb.com/blog/2013/08/exceptions-and-monad-transformers