在什么情况下应该使用liftIO
?当我使用ErrorT String IO
时,lift
函数可以将IO操作提升为ErrorT
,因此liftIO
似乎是多余的。
答案 0 :(得分:84)
lift
始终从“上一个”图层中提升。如果您需要从第二层升级,则需要lift . lift
,依此类推。
另一方面,liftIO
始终从IO层(当存在时,始终位于堆栈的底部)提升。所以,如果你有超过2层的monad,你会很感激liftIO
。
比较以下lambdas中的参数类型:
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
答案 1 :(得分:31)
liftIO只是IO Monad的捷径,无论你使用的Monad是什么。基本上,liftIO等于使用可变数量的升降机。起初这可能听起来多余,但使用liftIO有一个很大的优势:它使你的IO代码独立于实际的Monad构造,所以你可以重复使用相同的代码,无论你的最终Monad构建的层数是多少(这是非常重要的)写一个monad变压器时)。
另一方面,liftIO不是免费提供的,就像电梯一样:你正在使用的Monad变压器必须支持它,例如你所在的Monad必须是MonadIO类的一个实例,但现在大多数Monad都是这样做的(当然,类型检查器会在编译时为你检查这个:这就是Haskell的优势!)。
答案 2 :(得分:1)
以前的答案都很好地解释了差异。我只是想了解一下内部工作原理,以便更容易理解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
,基本上IO
是类型类定义中唯一的一个
问题是,当我们有一个monad类型,该类型由IO
上的多层monad变换器组成时,最好为每个monad变换器层有一个MonadIO
实例。例如,MonadIO
的{{1}}实例也要求MaybeT m
属于m
类型类。
基本上,编写MonadIO
实例也是一项非常简单的任务。对于MonadIO
,它的定义类似于
MaybeT m
或instance (MonadIO m) => MonadIO (MaybeT m) where
liftIO = lift . liftIO
StateT s m
它们都是一样的。想象一下,当您有一个4层变压器堆栈时,您要么需要执行instance (MonadIO m) => MonadIO (StateT s m) where
liftIO = lift . liftIO
,要么只需执行lift . lift . lift . lift $ myIOAction
。如果您考虑一下,每个liftIO myIOAction
会将您带到堆栈的最下一层,直到它一直深入到lift . liftIO
,在IO
被定义为liftIO
的地方并使用与上面组成的id
相同的代码完成。
因此,这基本上就是为什么不考虑变压器堆栈配置的情况,只要所有底层都是lift
和MonadIO
的成员,那么一个单独的MonadTrans
就可以了。