我有一个问题,一堆单子变压器(甚至一个单子变压器)在上面IO
。一切都很好,除了在每个动作之前使用提升非常烦人!我怀疑这真的没什么可做的,但我想我还是会问。
我知道提升整个块,但如果代码真的是混合类型怎么办?如果 GHC 加入一些语法糖(例如<-$
= <- lift
)会不会很好?
我有一个问题,一堆单子变压器(甚至一个单子变压器)在上面IO
。一切都很好,除了在每个动作之前使用提升非常烦人!我怀疑这真的没什么可做的,但我想我还是会问。
我知道提升整个块,但如果代码真的是混合类型怎么办?如果 GHC 加入一些语法糖(例如<-$
= <- lift
)会不会很好?
对于所有标准的mtl monad,您根本不需要lift
。get
, put
, ask
, tell
— 它们都可以在任何单子中工作,并在堆栈中的某个位置使用正确的变压器。缺少的部分是IO
, 甚至可以liftIO
将任意 IO 操作提升到任意数量的层。
这是通过提供的每个“效果”的类型类完成的:例如,MonadState
提供get
和put
。如果您想围绕转换器堆栈创建自己的newtype
包装器,您可以deriving (..., MonadState MyState, ...)
使用GeneralizedNewtypeDeriving
扩展,或滚动您自己的实例:
instance MonadState MyState MyMonad where
get = MyMonad get
put s = MyMonad (put s)
通过定义一些实例而不是其他实例,您可以使用它来选择性地公开或隐藏组合变压器的组件。
(通过定义自己的类型类并为标准转换器提供样板实例,您可以轻松地将这种方法扩展到您自己定义的全新 monadic 效果,但全新的 monad 很少见;大多数情况下,您只需组成 mtl 提供的标准集。)
您可以通过使用类型类而不是具体的 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
函数!那么如何避免呢?
诀窍是使函数签名更加通用,因此它表明State
monad 可以出现在 monad 堆栈中的任何位置。这样做是这样的:
bangMe :: MonadState String m => m ()
这迫使m
它成为一个支持 monad 堆栈中任何位置(实际上)状态的 monad,因此该函数将在不提升任何此类堆栈的情况下工作。
但是,有一个问题;由于IO
不是 的一部分,因此默认情况下mtl
它没有转换器(例如),也没有方便的类型类。IOT
那么当你想任意解除 IO 动作时应该怎么做呢?
救援来了MonadIO
!它的行为几乎与 等相同MonadState
,MonadReader
唯一的区别是它的提升机制略有不同。它的工作原理是这样的:您可以采取任何IO
行动,并使用liftIO
它来将其变成与 monad 无关的版本。所以:
action :: IO ()
liftIO action :: MonadIO m => m ()
通过以这种方式转换您希望使用的所有 monadic 动作,您可以随意地将 monads 交织在一起,而无需任何繁琐的提升。