Skip to content

Instantly share code, notes, and snippets.

@cideM
Created May 5, 2019 15:45
Show Gist options
  • Save cideM/654e24dd076353e2da763278b7ff763f to your computer and use it in GitHub Desktop.
Save cideM/654e24dd076353e2da763278b7ff763f to your computer and use it in GitHub Desktop.
Understanding `unliftio`

Understanding unliftio

Try to understand the ReaderT instance.

withRunInIO inner =
    ReaderT $ \r ->
    withRunInIO $ \run ->
    inner (run . flip runReaderT r)
  • r is the environment.
  • Why is there a 2nd withRunInIo?
  • What's run?

According to the docs there's only one method in the type class

The type class has only one method -- askUnliftIO:

even though there are two: askUnliftIO and withRunInIO. It seems they don't consider the latter a distinct method, rather it's just a convenience wrapper around askUnliftIO. At least that's my hypothesis.

So let's focus on askUnliftIO. Here's how it can be used.

(u :: UnliftIO m) <- askUnliftIO
  liftIO $ System.Timeout.timeout x $ unliftIO u y

It gives us an unlifted thing (a newtype record). We get the content of the record through unliftIO u. The result of that is a function m a -> IO a. In the above example, we call the function to get the IO a and then that's passed to timeout and ultimately liftIO. So... how?

It's actually really easy! You just look at the source, where askUnliftIO is defined as a really straight forward... nope this is Haskell.

askUnliftIO uses withRunInIO. Talk about single type class method. Seriously I should have known. Never get your hopes up with Haskell. Never.

askUnliftIO :: m (UnliftIO m)
askUnliftIO = withRunInIO (\run -> return (UnliftIO run))

Maybe I can make things easier by renaming and making up fantasy syntax.

-- Without the newtype
askUnliftIO :: monad (monad a -> IO a)

askUnliftIO basically just gives us a function.

The default implementation (at least I think that's what it is?) is actually understandable now.

 withRunInIO inner = askUnliftIO >>= \u -> liftIO (inner (unliftIO u))

Get the unlift newtype thingie u, get the function from inside that thing unliftIO u, then pass that to inner (which is (m a -> IO a) -> IO b). It now needs only the IO b part to give you the m b but something something happened in IO or whatever.

ReaderT now.

withRunInIO func =
    IdentityT $
    withRunInIO $ \run ->
    func (run . runIdentityT)

The whole

\run -> func (run . runIdentityT)

is the inner in

 withRunInIO inner = 
    askUnliftIO >>= 
    \u -> liftIO (
        inner (unliftIO u)
    )
withRunInIO = 
   askUnliftIO >>= 
   \u -> liftIO (
       (\run -> func (run . runIdentityT)) (unliftIO u)
   )
-- | `func` here is kind of undefined because I just copy pasted one snippet into another. In the real world `func` would be something concrete.

So the run function is always (?) the function we get out of our unlift thingie! Cool... ? Sounds like a profound realization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment