Monad and MonadIO for custom type

2019-08-24 07:42发布

问题:

I have a Logger type of kind * -> * which can take any type and log the value in a file. I am trying to implement this in a monadic way so that I log and keep working the same. My code looks like

import Control.Applicative
import Control.Monad
import System.IO
import Control.Monad.IO.Class

instance Functor Logger where
  fmap = liftM

instance Applicative Logger where
  pure = return
  (<*>) = ap

newtype Logger a = Logger a deriving (Show)

instance Monad (Logger) where
  return  = Logger
  Logger logStr >>= f = f logStr

instance MonadIO (Logger) where
  liftIO a = do
    b <- liftIO a
    return b


logContent :: (Show a) => a -> Logger a
logContent a = do
  b  <- liftIO $ logContent2 a
  return b


logContent2 :: (Show a) => a -> IO a
logContent2 a = do
    fHandle <- openFile "test.log" AppendMode
    hPrint fHandle a
    hClose fHandle
    return (a)

The liftIO function goes on endless loop as it calls itself. I am not able to do b <- a either. Can someone help on getting MonadIO implementation right ?

回答1:

As noted in the comments, I think you've misunderstood what MonadIO and liftIO do.

These typeclasses and functions come from mtl library. Rather unfortunately, mtl stands for "monad transformer library", but mtl is not a monad transformer library. Rather, mtl is a set of typeclasses that allow you to take a monad that --- and this is important --- already has a particular type of functionality and provide that monad with a consistent interface around that functionality. This ends up being really useful for working with actual monad transformers. That's because mtl allows you to use tell and ask and put to access the Writer, Reader, and State functionality of your monad transformer stack in a consistent way.

Separately from this transformer business, if you already have a custom monad, say that supports arbitrary IO and has State functionality, then you can define a MonadState instance to make the standard state operations (state, get, gets, put, modify) available for your custom monad, and you can define a MonadIO instance to allow an arbitrary IO action to be executed in your custom monad using liftIO. However, none of these typeclasses are capable of adding functionality to a monad that it doesn't already have. In particular, you can't transform an arbitrary monadic action m a into an IO a using a MonadIO instance.

Note that the transformers package contains types that are capable of adding functionality to a monad that it doesn't already have (e.g., adding reader or writer functionality), but there is no transformer to add IO to an arbitrary monad. Such a transformer would be impossible (without unsafe or nonterminating operations).

Also note that the signature for liftIO :: MonadIO m => IO a -> m a puts a MonadIO constraint on m, and this isn't just a trivial constraint. It actually indicates that liftIO only works for monads m that already have IO functionality, so either m is the IO monad, or it's a monad stack with IO at its base. Your Logger example doesn't have IO functionality and so can't have a (sensible) MonadIO instance.

Getting back to your specific problem, it's actually a little bit hard to steer you right here without knowing exactly what you're trying to do. If you just want to add file-based logging to an existing IO computation, then defining a new transformer stack will probably do the trick:

type LogIO = ReaderT Handle IO

logger :: (Show a) => a -> LogIO ()
logger a = do
  h <- ask
  liftIO $ hPrint h a

runLogIO :: LogIO a -> FilePath -> IO a
runLogIO act fp = withFile fp AppendMode $ \h -> runReaderT act h

and you can write things like:

main :: IO ()
main = runLogIO start "test.log"

start :: LogIO ()
start = do
  logger "Starting program"
  liftIO . putStrLn $ "Please enter your name:"
  n <- liftIO $ getLine
  logger n
  liftIO . putStrLn $ "Hello, " ++ n
  logger "Ending program"

The need to add liftIO calls when using IO actions within the LogIO monad is ugly but largely unavoidable.

This solution would also work for adding file-based logging to pure computations, with the understanding that you have to convert them to IO computations anyway if you want to safely log to a file.

The more general solution is to define your own monad transformer (not merely your own monad), like LoggerT m, together with an associated MonadLogger type class that will add file-based logging to to any IO-capable monad stack. The idea would be that you could then create arbitrary custom monad stacks:

type MyMonad = StateT Int (LoggerT IO)

and then write code that mixes monadic computations from different layers (like mixing state computations and file-based logging):

newSym :: String -> MyMonad String
newSym pfx = do
  n <- get
  logger (pfx, n)
  put (n+1)
  return $ pfx ++ show n

Is this what you what you're trying to do? If not, maybe you could describe, either here or in a new question, how you're trying to add logging to some example code.