应该在什么情况下liftIO
使用?当我使用ErrorT String IO
时,该lift
功能可以将 IO 操作提升为ErrorT
,因此liftIO
似乎是多余的。
3 回答
lift
总是从“上一个”层提升。如果您需要从第二层抬起,则需要lift . lift
依此类推。
另一方面,liftIO
总是从 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 构建了多少层,您都可以重用相同的代码(这非常重要编写单子变压器时)。
另一方面,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
并且最常在底部 monad 是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 转换器组成的 monad 类型时IO
,我们最好MonadIO
为这些 monad 转换器层中的每一层都有一个实例。例如,要求的MonadIO
实例也必须是类型类。MaybeT m
m
MonadIO
编写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
. 如果您考虑一下,everylift . liftIO
将带您在堆栈中向下一层,直到它一直深入到定义为IO
的位置,并使用与上面的composed 相同的代码完成。liftIO
id
lift
所以这基本上就是为什么不管变压器堆栈配置如何,只要所有底层都是其中的成员MonadIO
并且MonadTrans
单个liftIO
就可以了。