core-program-0.6.9.2: Opinionated Haskell Interoperability
Safe HaskellSafe-Inferred
LanguageHaskell2010

Core.Program.Unlift

Description

The Program monad is an instance of MonadIO, which makes sense; it's just a wrapper around doing IO and you call it using execute from the top-level main action that is the entrypoint to any program. So when you need to actually do some I/O or interact with other major libraries in the Haskell ecosystem, you need to get back to IO and you use liftIO to do it:

main :: IO ()
main = execute $ do
    -- now in the Program monad
    write "Hello there"

    liftIO $ do
        -- now something in IO
        source <- readFile "hello.c"
        compileSourceCode source

    -- back in Program monad
    write "Finished"

and this is a perfectly reasonable pattern.

Sometimes, however, you want to get to the Program monad from there, and that's tricky; you can't just execute a new program (and don't try: we've already initialized output and logging channels, signal handlers, your application context, etc).

main :: IO ()
main = execute $ do
    -- now in the Program monad
    write "Hello there"

    liftIO $ do
        -- now something in IO
        source <- readFile "hello.c"
        -- log that we're starting compile      ... FIXME how???
        result <- compileSourceCode source
        case result of
            Right object -> linkObjectCode object
            Left err     -> -- debug the error  ... FIXME how???

    -- back in Program monad
    write "Finished"

We have a problem, because what we'd like to do is use, say, debug to log the compiler error, but we have no way to unlift back out of IO to get to the Program monad.

To workaround this, we offer withContext. It gives you a function that you can then use within your lifted IO to run a (sub)Program action:

main :: IO ()
main = execute $ do
    -- now in the Program monad
    write "Hello there"

    withContext $ \runProgram -> do
        -- now lifted to IO
        source <- readFile "hello.c"

        runProgram $ do
            -- now "unlifted" back to Program monad!
            info "Starting compile..."
            info "Nah. Changed our minds"
            info "Ok, fine, compile the thing"

        -- more IO
        result <- compileSourceCode source
        case result of
            Right object -> linkObjectCode object
            Left err     -> runProgram (debugS err)

    -- back in Program monad
    write "Finished"

Sometimes Haskell type inference can give you trouble because it tends to assume you mean what you say with the last statement of do-notation block. If you've got the type wrong you'll get an error, but in an odd place, probably at the top where you have the lambda. This can be confusing. If you're having trouble with the types try putting return () at the end of your subprogram.

Synopsis

Unlifting

withContext :: ((forall β. Program τ β -> IO β) -> IO α) -> Program τ α Source #

This gives you a function that you can use within your lifted IO actions to return to the Program monad.

The type signature of this function is a bit involved, but the example below shows that the lambda gives you a function as its argument (we recommend you name it runProgram for consistency) which gives you a way to run a subprogram, be that a single action like writing to terminal or logging, or a larger action in a do-notation block:

main :: IO ()
main = execute $ do
    withContext $ \runProgram -> do
        -- in IO monad, lifted
        -- (just as if you had used liftIO)

        ...

        runProgram $ do
            -- now unlifted, back to Program monad

        ...

Think of this as liftIO with an escape hatch.

This function is named withContext because it is a convenience around the following pattern:

    context <- getContext
    liftIO $ do
        ...
        subProgram context $ do
            -- now in Program monad
        ...

Note there is a MonadUnliftIO instance which may be useful to you as well.

Internals

getContext :: Program τ (Context τ) Source #

Get the internal Context of the running Program. There is ordinarily no reason to use this; to access your top-level application data τ within the Context use getApplicationState.

subProgram :: Context τ -> Program τ α -> IO α Source #

Run a subprogram from within a lifted IO block.