0

我有一种 Logger 类型,* -> *它可以采用任何类型并将值记录在文件中。我试图以一种单子的方式实现这一点,以便我登录并继续工作。我的代码看起来像

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)

liftIO 函数在调用自身时会进行无限循环。我也不能做 b <- a 。有人可以帮助正确实施 MonadIO 吗?

4

1 回答 1

2

如评论中所述,我认为您误解了做什么MonadIOliftIO做什么。

这些类型类和函数来自mtl库。不幸的是,它mtl代表“monad 转换器库”,但mtl 不是 monad 转换器库。相反,mtl是一组类型类,允许您采用一个 monad --- 这很重要 ---已经具有特定类型的功能,并为该 monad 提供围绕该功能的一致接口。这最终对于使用实际的 monad 转换器非常有用。这是因为mtl允许您以一致的方式使用tell和访问 monad 转换器堆栈的、和ask功能。putWriterReaderState

与这个转换器业务分开,如果你已经有一个自定义的 monad,比如说支持任意 IO 并具有State功能,那么你可以定义一个MonadState实例来使标准状态操作(state, get, gets, put, modify)可用于你的自定义 monad,你可以定义一个MonadIO实例以允许在您的自定义 monad 中执行任意 IO 操作,使用liftIO. 但是,这些类型类中没有一个能够向 monad 添加它还没有的功能。特别是,您不能将任意一元操作m a转换为IO a使用MonadIO实例。

请注意,该transformers包包含的类型能够向 monad 添加它还没有的功能(例如,添加读取器或写入器功能),但是没有转换器可以添加IO到任意 monad。这样的变压器是不可能的(没有不安全或非终止操作)。

另请注意, for 的签名对liftIO :: MonadIO m => IO a -> m a施加了MonadIO约束m,这不仅仅是一个微不足道的约束。它实际上表明liftIO它只适用于已经具有 IO 功能m的 monad ,所以要么IO monad,要么是一个以它为基础的 monad 堆栈。您的示例没有 IO 功能,因此没有(明智的)实例。m IOLoggerMonadIO

回到您的具体问题,实际上在不知道您要做什么的情况下将您引导到这里实际上有点困难。如果您只想将基于文件的日志记录添加到现有的 IO 计算中,那么定义一个新的转换器堆栈可能会解决问题:

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

你可以写这样的东西:

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"

liftIO在 monad 中使用 IO 操作时添加调用的需要LogIO很丑陋,但在很大程度上是不可避免的。

此解决方案也适用于将基于文件的日志记录添加到纯计算中,但如果您想安全地登录到文件,则无论如何都必须将它们转换为 IO 计算。

更通用的解决方案是定义您自己的 monad转换器(不仅仅是您自己的 monad),例如LoggerT m,以及一个关联的MonadLogger类型类,它将基于文件的日志记录添加到任何支持 IO 的 monad 堆栈。这个想法是您可以创建任意自定义 monad 堆栈:

type MyMonad = StateT Int (LoggerT IO)

然后编写混合来自不同层的单子计算的代码(如混合状态计算和基于文件的日志记录):

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

这就是你想要做的吗?如果没有,也许您可​​以在此处或在新问题中描述您如何尝试将日志记录添加到某些示例代码中。

于 2018-08-18T21:26:19.473 回答