84

应该在什么情况下liftIO使用?当我使用ErrorT String IO时,该lift功能可以将 IO 操作提升为ErrorT,因此liftIO似乎是多余的。

4

3 回答 3

96

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
于 2010-10-13T06:32:06.863 回答
39

liftIO 只是 IO Monad 的快捷方式,无论您在哪个 Monad 中。基本上,liftIO 等于使用可变数量的升降机。起初这听起来可能是多余的,但使用 liftIO 有一个很大的优势:它使您的 IO 代码独立于实际的 Monad 构造,因此无论您的最终 Monad 构建了多少层,您都可以重用相同的代码(这非常重要编写单子变压器时)。

另一方面,liftIO 不是免费的,就像 lift 一样:你使用的 Monad 转换器必须支持它,例如你所在的 Monad 必须是 MonadIO 类的一个实例,但现在大多数 Monad 都支持(当然,类型检查器会在编译时为您检查:这就是 Haskell 的优势!)。

于 2010-10-13T13:06:13.810 回答
4

以前的答案都很好地解释了差异。我只是想对内部工作原理有所了解,以便更容易理解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就可以了。

于 2020-04-12T13:52:12.350 回答