应该在什么情况下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. 对于IOmonad,它的定义非常简单。
class (Monad m) => MonadIO m where
liftIO :: IO a -> m a
instance MonadIO IO where
liftIO = id
那个简单的......liftIO实际上只是id针对IOmonad 并且基本上IO是唯一包含在类型类定义中的。
问题是,当我们有一个由几层 monad 转换器组成的 monad 类型时IO,我们最好MonadIO为这些 monad 转换器层中的每一层都有一个实例。例如,要求的MonadIO实例也必须是类型类。MaybeT mmMonadIO
编写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 相同的代码完成。liftIOidlift
所以这基本上就是为什么不管变压器堆栈配置如何,只要所有底层都是其中的成员MonadIO并且MonadTrans单个liftIO就可以了。