52

我有一个问题,一堆单子变压器(甚至一个单子变压器)在上面IO。一切都很好,除了在每个动作之前使用提升非常烦人!我怀疑这真的没什么可做的,但我想我还是会问。

我知道提升整个块,但如果代码真的是混合类型怎么办?如果 GHC 加入一些语法糖(例如<-$= <- lift)会不会很好?

4

2 回答 2

61

对于所有标准的mtl monad,您根本不需要liftget, put, ask, tell— 它们都可以在任何单子中工作,并在堆栈中的某个位置使用正确的变压器。缺少的部分是IO, 甚至可以liftIO将任意 IO 操作提升到任意数量的层。

这是通过提供的每个“效果”的类型类完成的:例如,MonadState提供getput。如果您想围绕转换器堆栈创建自己的newtype包装器,您可以deriving (..., MonadState MyState, ...)使用GeneralizedNewtypeDeriving扩展,或滚动您自己的实例:

instance MonadState MyState MyMonad where
  get = MyMonad get
  put s = MyMonad (put s)

通过定义一些实例而不是其他实例,您可以使用它来选择性地公开或隐藏组合变压器的组件。

(通过定义自己的类型类并为标准转换器提供样板实例,您可以轻松地将这种方法扩展到您自己定义的全新 monadic 效果,但全新的 monad 很少见;大多数情况下,您只需组成 mtl 提供的标准集。)

于 2012-01-29T16:45:06.860 回答
52

您可以通过使用类型类而不是具体的 monad 堆栈来使您的函数与 monad 无关。

假设您有此功能,例如:

bangMe :: State String ()
bangMe = do
  str <- get
  put $ str ++ "!"
  -- or just modify (++"!")

当然,您意识到它也可以用作变压器,因此可以这样写:

bangMe :: Monad m => StateT String m ()

但是,如果您有一个使用不同堆栈的函数,比如说ReaderT [String] (StateT String IO) ()或其他什么,您将不得不使用可怕的lift函数!那么如何避免呢?

诀窍是使函数签名更加通用,因此它表明Statemonad 可以出现在 monad 堆栈中的任何位置。这样做是这样的:

bangMe :: MonadState String m => m ()

这迫使m它成为一个支持 monad 堆栈中任何位置(实际上)状态的 monad,因此该函数将在不提升任何此类堆栈的情况下工作。

但是,有一个问题;由于IO不是 的一部分,因此默认情况下mtl它没有转换器(例如),也没有方便的类型类。IOT那么当你想任意解除 IO 动作时应该怎么做呢?

救援来了MonadIO!它的行为几乎与 等相同MonadStateMonadReader唯一的区别是它的提升机制略有不同。它的工作原理是这样的:您可以采取任何IO行动,并使用liftIO它来将其变成与 monad 无关的版本。所以:

action :: IO ()
liftIO action :: MonadIO m => m ()

通过以这种方式转换您希望使用的所有 monadic 动作,您可以随意地将 monads 交织在一起,而无需任何繁琐的提升。

于 2012-01-29T16:48:12.057 回答