To build on ertes's answer, I think the desired signature for printFile
is printFile :: (MonadIO m, MonadLogger m) => FilePath -> m ()
, which I read as "I will print the given file. To do so, I need to do some IO and some logging."
I am no expert, but here's my attempt at this solution. I will be grateful for comments and suggestions on how to improve this.
{-# LANGUAGE FlexibleInstances #-}
module DependencyInjection where
import Prelude hiding (log)
import Control.Monad.IO.Class
import Control.Monad.Identity
import System.IO
import Control.Monad.State
-- |Any function that can turn a string into an action is considered a Logger.
type Logger m = String -> m ()
-- |Logger that does nothing, for testing.
noLogger :: (Monad m) => Logger m
noLogger _ = return ()
-- |Logger that prints to STDERR.
stderrLogger :: (MonadIO m) => Logger m
stderrLogger x = liftIO $ hPutStrLn stderr x
-- |Logger that appends messages to a given file.
fileLogger :: (MonadIO m) => FilePath -> Logger m
fileLogger filePath value = liftIO logToFile
where
logToFile :: IO ()
logToFile = withFile filePath AppendMode $ flip hPutStrLn value
-- |Programs have to provide a way to the get the logger to use.
class (Monad m) => MonadLogger m where
getLogger :: m (Logger m)
-- |Logs a given string using the logger obtained from the environment.
log :: (MonadLogger m) => String -> m ()
log value = do logger <- getLogger
logger value
-- |Example function that we want to run in different contexts, like
-- skip logging during testing.
printFile :: (MonadIO m, MonadLogger m) => FilePath -> m ()
printFile fp = do
log ("Printing file: " ++ fp)
liftIO (readFile fp >>= putStr)
log "Done printing."
-- |Let's say this is the real program: it keeps the log file name using StateT.
type RealProgram = StateT String IO
-- |To get the logger, build the right fileLogger.
instance MonadLogger RealProgram where
getLogger = do filePath <- get
return $ fileLogger filePath
-- |And this is how you run printFile "for real".
realMain :: IO ()
realMain = evalStateT (printFile "file-to-print.txt") "log.out"
-- |This is a fake program for testing: it will not do any logging.
type FakeProgramForTesting = IO
-- |Use noLogger.
instance MonadLogger FakeProgramForTesting where
getLogger = return noLogger
-- |The program doesn't do any logging, but still does IO.
fakeMain :: IO ()
fakeMain = printFile "file-to-print.txt"