I'd like to use a custom application type in place of IO
in my program and use it with functions like race_
from the async
library.
Specifically, I'm keen on passing two computations of type App
to race_
. Since race_
only accepts values of type IO
, I wrapped those computations with return
.
While this type-checks, I can see that neither of the computations is actually executed.
Here's a minimal example¹ illustrating the issue:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
module RaceTest where
import Control.Monad.Reader ( MonadReader
, ReaderT(..)
, runReaderT
)
import Control.Monad.IO.Class ( MonadIO
, liftIO
)
import Control.Concurrent.Async ( race_ )
data Env = Env { val :: !Int }
newtype App a = App
{ unApp :: ReaderT Env IO a
} deriving (Functor, Applicative, Monad, MonadIO, MonadReader Env)
runApp :: Env -> App a -> IO a
runApp env app = runReaderT (unApp app) env
main = runApp (Env 24) simpleApp
where
simpleApp :: App () = do
liftIO $ putStrLn "About to spawn threads"
liftIO $ race_ (return firstAsync) (return secondAsync)
firstAsync :: App () = liftIO $ putStrLn "First async"
secondAsync :: App () = liftIO $ putStrLn "Second async"
How can I run these computations of type App
using race_
?
¹ While it would be simple in this example to just get rid of the App
type, in the application I'm building, I have App
and Env
types that allow logging using co-log
similar to this setup. That's something I don't want to lose.
Your computations are not getting executed, because you're not actually passing them to race_
. Instead, you're passing two IO
computations, which return your App
computations as a result. But not executing them.
In order to get them executed inside IO
, use your function runApp
that you already have. Since you'll need to pass an environment to it, and I'm assuming you'll want to use the same environment as simpleApp
itself has, you can use ask
to obtain it out of the MonadReader
context:
simpleApp :: App () = do
liftIO $ putStrLn "About to spawn threads"
env <- ask
liftIO $ race_ (runApp env firstAsync) (runApp env secondAsync)