Haskell: lift vs liftIO

22,436

Solution 1

lift always lifts from the "previous" layer. If you need to lift from the second layer, you would need lift . lift and so on.

On the other hand, liftIO always lifts from the IO layer (which, when present, is always on the bottom of the stack). So, if you have more than 2 layers of monads, you will appreciate liftIO.

Compare the type of the argument in the following 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

Solution 2

liftIO is just a shortcut to the IO Monad, whichever the Monad you are in. Basically, liftIO equals to using a variable number of lifts. At first this might sound redundant but using liftIO has one big advantage: it makes your IO code indpendent of the actual Monad construction so you can reuse the same code no matter the number of layer your final Monad has been built of (this is quite important when writing a monad transformer).

On the ohter hand, liftIO is not coming for free, as lift does: the Monad transformers you're using must have support for it, e.g. the Monad you're in must be an instance of the MonadIO class, but most Monads nowadays do (and of course, the type-checker will check this for you at compile time: that's the strength of Haskell!).

Solution 3

Previous answers all explain the difference quite well. I just wanted to shed a some light on the inner workings so that it could be easier to understand how liftIO isn't something magical (for the novice Haskellers like me).

liftIO :: IO a -> m a

is a wise tool just build upon

lift :: (Control.Monad.Trans.Class.MonadTrans t, Monad m) => m a -> t m a

and most often used when the bottom monad is IO. For the IO monad it's definition is quite simple.

class (Monad m) => MonadIO m where
  liftIO :: IO a -> m a

instance MonadIO IO where
  liftIO = id

That simple... liftIO is in fact just id for the IO monad and basically IO is the only one that comes within the definition of the type class.

The thing is, when we have a monad type which is composed of several layers of monad transformers over IO, we better have an MonadIO instance for each one of those monad transformer layers. For example the MonadIO instance of MaybeT m requires m to be of MonadIO typeclass as well.

Writing a MonadIO instance is basically a very simple task too. For MaybeT m it is defined like

instance (MonadIO m) => MonadIO (MaybeT m) where
  liftIO = lift . liftIO

or for StateT s m

instance (MonadIO m) => MonadIO (StateT s m) where
  liftIO = lift . liftIO

they are all the same. Imagine when you have a 4 layer transformer stack then you either need to do lift . lift . lift . lift $ myIOAction or just liftIO myIOAction. If you think about it, every lift . liftIO will take you one layer down in the stack up until it digs all the way down to IO at where liftIO is defined as id and finalizes with the same code like composed lifts above.

So this is basically why regardless of the transformer stack configuration, provided that all underlaying layers are members of MonadIO and MonadTrans a single liftIO is just fine.

Share:
22,436

Related videos on Youtube

Lachlan
Author by

Lachlan

Functional programming in Scala.

Updated on April 13, 2020

Comments

  • Lachlan
    Lachlan about 4 years

    In what situations should liftIO be used? When I'm using ErrorT String IO, the lift function works to lift IO actions into ErrorT, so liftIO seems superfluous.

  • John L
    John L over 13 years
    I generally will use liftIO to lift to the IO layer even if lift is sufficient, because then I can change the monad stack and the code still works.
  • Roman Cheplyaka
    Roman Cheplyaka over 13 years
    @John: good point. And also it makes it obvious that you're lifting IO and not any other monad.
  • Nawaz
    Nawaz almost 4 years
    I'm a newbie: lift in this answer (and the question) is assumed to be from Control.Monad.Trans.Class, I guess? Not the Monadic lifting or the general lifting as described in the first section here?
  • Ben Weaver
    Ben Weaver over 2 years
    This is a great answer that finally helped me to get it: if each monad in the stack is an instance of MonadIO and if IO is included at the bottom of the stack, then the succession of lift . liftIO calls works its way down the stack, each lift .liftIO calling the liftIO of the monad one level down, in a chain, until liftIO for IO is reached, which equals id, itself.