1

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 ?

nnnmmm
  • 7,964
  • 4
  • 22
  • 41
DBS
  • 794
  • 2
  • 9
  • 21
  • `liftIO a = do { b <- liftIO a; ...` <- here you call `liftIO` recursively for argument itself. Hence the infinite loop. – Shersh Aug 18 '18 at 09:46
  • I don't think `MonadIO` works like you think it works. Look at [all defined instances](https://hackage.haskell.org/package/transformers-0.3.0.0/docs/Control-Monad-IO-Class.html#t:MonadIO) of `MonadIO`. See the pattern? All the instances apart from `IO` itself are transformers, and in order to be a `MonadIO` they need to transform a `MonadIO`. IOW if you want your monad to be a `MonadIO`, it needs to contain an actual `IO a` value somewhere inside, directly or indirectly. Otherwise you cannot lift from `IO a` to `Logger a`, because you can never escape the `IO`. – n. m. could be an AI Aug 18 '18 at 09:48
  • But what if I want to convert from one monad type to another ? This will help me in chaining from one monad to another to create a flow ? – DBS Aug 18 '18 at 10:22
  • 4
    Your `Logger` monad is identical to the [`Identity`](http://hackage.haskell.org/package/base-4.11.1.0/docs/Data-Functor-Identity.html#t:Identity) monad. Since `Identity` doesn't have a `MonadIO` instance, neither can your `Logger`. However, there is a `MonadIO` instance for [`IdentityT`](https://hackage.haskell.org/package/transformers-0.3.0.0/docs/Control-Monad-Trans-Identity.html#t:IdentityT). – 4castle Aug 18 '18 at 11:11
  • You need to start by deciding what Logger is going to do that IO doesn't already do. For instance, should it carry a file handle for the log file? Once you know that the design will start to become clearer. – Paul Johnson Aug 18 '18 at 11:24
  • With liftIO :: IO a -> m a, I should be able to pass any IO monad and convert to another monad type 'm' right ? – DBS Aug 18 '18 at 13:02
  • No, not in general. The signature is actually `liftIO :: MonadIO m => IO a -> m a`, and the `MonadIO m` bit isn't just a trivial constraint. If a monad `m` has a `liftIO` instance, that's an indication that it **already has IO functionality**, either because it **is** the IO monad or because it's part of a monad stack with `IO` at its base. Your `Logger` monad can't have a `liftIO` instance (except one that's either unsafe or doesn't terminate) because your `Logger` monad has no existing IO functionality. – K. A. Buhr Aug 18 '18 at 19:56

1 Answers1

3

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.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • Thanks @K. A. Buhr I will try out the suggestions you mentioned and update here – DBS Aug 20 '18 at 03:55