【问题标题】:Monad and MonadIO for custom type自定义类型的 Monad 和 MonadIO
【发布时间】:2018-08-18 08:46:40
【问题描述】:

我有一个 * -> * 类型的 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

【问题讨论】:

  • liftIO a = do { b &lt;- liftIO a; ... liftIO 作为参数本身。因此是无限循环。
  • 我认为MonadIO 不像你认为的那样有效。看看all defined instancesMonadIO。看到图案了吗?除了IO 本身之外的所有实例都是转换器,为了成为MonadIO,它们需要转换MonadIO。 IOW 如果您希望您的 monad 成为 MonadIO,则它需要在内部某处直接或间接包含实际的 IO a 值。否则你无法从IO a提升到Logger a,因为你永远无法逃脱IO
  • 但是如果我想从一种 monad 类型转换为另一种呢?这将帮助我从一个单子链接到另一个单子以创建流程?
  • 您的 Logger monad 与 Identity monad 相同。由于Identity 没有MonadIO 实例,您的Logger 也没有。但是,IdentityT 有一个 MonadIO 实例。
  • 您需要首先决定 Logger 将要做什么,而 IO 还没有做。例如,它是否应该携带日志文件的文件句柄?一旦您知道设计将开始变得更加清晰。

标签: haskell monads monad-transformers io-monad


【解决方案1】:

正如 cmets 所述,我认为您误解了 MonadIOliftIO 的作用。

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

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

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

还要注意liftIO :: MonadIO m =&gt; IO a -&gt; m a 的签名在m 上设置了MonadIO 约束,这不仅仅是一个微不足道的约束。它实际上表明 liftIO 仅适用于 已经具有 IO 功能 的 monad m,因此 m IO monad,或者它是具有IO 在它的底部。您的 Logger 示例没有 IO 功能,因此不能有(合理的)MonadIO 实例。

回到您的具体问题,实际上在不知道您要做什么的情况下引导您到这里确实有点困难。如果您只想将基于文件的日志记录添加到现有的 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"

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

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

更通用的解决方案是定义您自己的 monadtransformer(不仅仅是您自己的 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

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

【讨论】:

  • 谢谢@K。 A. Buhr 我会尝试你提到的建议并在这里更新
猜你喜欢
  • 1970-01-01
  • 2019-06-30
  • 1970-01-01
  • 2016-10-28
  • 1970-01-01
  • 2022-06-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多