Search code examples
haskellmonad-transformersio-monad

How to use custom App type in place of IO?


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.


Solution

  • 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)