我有一个问题,在IO
之上有一堆monad变换器(甚至是一个monad变换器)。一切都很好,除了在每次动作之前都使用电梯非常烦人!我怀疑这与此无关,但我想我还是会问。
我知道解除了整个块,但如果代码实际上是混合类型怎么办?如果GHC投入一些语法糖(例如,<-$
= <- lift
)会不会很好?
答案 0 :(得分:55)
对于所有标准mtl monad,您根本不需要lift
。 get
,put
,ask
,tell
- 它们都可以在堆栈中某处使用正确变换器的任何monad中工作。缺少的部分是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提供的标准集来获得。)
答案 1 :(得分:48)
通过使用类型类而不是具体的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动作,你可以随心所欲地交织monad而不需要任何繁琐的提升。