在什么情况下应该 liftIO
使用?当我使用时ErrorT String IO
,该lift
功能可以将IO操作提升为ErrorT
,因此liftIO
似乎是多余的。
Answers:
lift
总是从“上一个”层抬起。如果您需要从第二层抬起,则需要lift . lift
依此类推。
另一方面,liftIO
总是从IO层提升(当存在时,IO层始终位于堆栈的底部)。因此,如果您拥有2层以上的monad,您将不胜感激liftIO
。
比较以下lambda中的参数类型:
type T = ReaderT Int (WriterT String IO) Bool
> :t \x -> (lift x :: T)
\x -> (lift x :: T) :: WriterT String IO Bool -> T
> :t \x -> (liftIO x :: T)
\x -> (liftIO x :: T) :: IO Bool -> T
liftIO只是IO Monad的快捷方式,无论您位于哪个Monad。基本上,liftIO等于使用可变数量的提升。最初,这听起来可能是多余的,但是使用liftIO有一个很大的优势:它使您的IO代码独立于实际的Monad构建,因此无论您构建最终Monad的层数如何,您都可以重用相同的代码(这很重要编写monad变压器时)。
另一方面,liftIO并不是免费提供的,就像lift一样免费:您使用的Monad变压器必须对此提供支持,例如,您所使用的Monad必须是MonadIO类的实例,但如今大多数Monad都这样做(当然,类型检查器会在编译时为您检查:这就是Haskell的优势!)。
先前的答案都很好地解释了差异。我只是想对内部工作方式有所了解,以便可以更容易地理解liftIO
它不是什么神奇的东西(对于像我这样的Haskellers新手来说)。
liftIO :: IO a -> m a
是一个明智的工具
lift :: (Control.Monad.Trans.Class.MonadTrans t, Monad m) => m a -> t m a
和最经常当底部单子是使用IO
。对于IO
monad,其定义非常简单。
class (Monad m) => MonadIO m where
liftIO :: IO a -> m a
instance MonadIO IO where
liftIO = id
这个简单的...liftIO
实际上仅id
适用于IO
monad,基本上IO
是类型类定义中唯一的一个。
问题是,当我们有一个monad类型,它由over上的多层monad变压器组成时IO
,最好MonadIO
为每个monad变压器层都有一个实例。例如,MonadIO
实例MaybeT m
需要m
是的MonadIO
requiretypeclass。
编写MonadIO
实例基本上也是一个非常简单的任务。因为MaybeT m
它的定义像
instance (MonadIO m) => MonadIO (MaybeT m) where
liftIO = lift . liftIO
或 StateT s m
instance (MonadIO m) => MonadIO (StateT s m) where
liftIO = lift . liftIO
他们都是一样的。当你有一个4层堆栈变压器,那么你要么需要做的想象lift . lift . lift . lift $ myIOAction
,或只是liftIO myIOAction
。如果你想想看,每次lift . liftIO
会带你下一层堆栈中,直到它挖一路下跌到IO
在这里liftIO
被定义为id
和与定型等构成相同的代码lift
S的上方。
因此,这基本上就是为什么不考虑变压器堆栈配置的情况,只要所有底层都是其中的成员MonadIO
并且MonadTrans
一个liftIO
就可以了。
liftIO
即使lift
足够,我通常也会使用IO层,因为这样我就可以更改monad堆栈,并且代码仍然有效。