core-program-0.2.5.0: lib/Core/Program/Unlift.hs
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
{-# OPTIONS_HADDOCK prune #-}
-- |
-- 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!
-- 'event' \"Starting compile...\"
-- 'event' \"Nah. Changed our minds\"
-- 'event' \"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.
module Core.Program.Unlift
( -- * Unlifting
withContext,
-- * Internals
getContext,
subProgram,
)
where
import Core.Program.Context
import Core.Program.Execute
import Core.Program.Logging
import Core.System.Base
-- |
-- 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
-- ...
-- @
-- I think I just discovered the same pattern as **unliftio**? Certainly
-- the signature is similar. I'm not sure if there is any benefit to
-- restating this as a `withRunInIO` action; we're deliberately trying to
-- constrain the types.
withContext ::
((forall β. Program τ β -> IO β) -> IO α) ->
Program τ α
withContext action = do
context <- getContext
let runThing = subProgram context
liftIO (action runThing)