自定义类型的Monad和MonadIO

时间:2018-08-18 08:46:41

标签: haskell monads monad-transformers io-monad

我有* -> *类型的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实施吗?

1 个答案:

答案 0 :(得分:1)

如评论中所述,我认为您误解了MonadIOliftIO的作用。

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

与此变压器业务分开,如果您已经有一个自定义monad,例如说它支持任意IO并且具有State功能,则可以定义一个MonadState实例以进行标准状态操作({ {1}},stategetgetsput)可用于您的自定义单子,并且您可以定义一个modify实例以允许任意使用MonadIO在您的自定义monad中执行的IO操作。但是,这些类型类都不具备向Monad尚不具备的功能添加功能的能力。特别是,您无法使用liftIO实例将任意单子动作m a转换为IO a

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

还请注意,IO的签名在liftIO :: MonadIO m => IO a -> m a上施加了MonadIO约束,这不仅仅是一个琐碎的约束。它实际上表明m仅适用于已经具有IO功能的单子liftIO,因此m 是 IO单子,或者它是一个以m为基础的monad堆栈。您的IO示例没有IO功能,因此也没有一个(明智的)Logger实例。

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

MonadIO

您可以编写如下内容:

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" monad中使用IO操作时,添加liftIO调用是很丑陋的,但在很大程度上是不可避免的。

该解决方案还可以将基于文件的日志记录添加到纯计算中,但要理解,如果要安全地记录到文件,则必须将其转换为IO计算。

更通用的解决方案是定义您自己的monad transformer (不仅仅是您自己的monad),例如LogIO,以及关联的LoggerT m类型类,该类将添加基于文件的日志记录到任何具有IO功能的monad堆栈。这个想法是,您然后可以创建任意的自定义monad堆栈:

MonadLogger

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

type MyMonad = StateT Int (LoggerT IO)

这就是您要执行的操作吗?如果没有,也许您可​​以在这里或在一个新问题中描述如何尝试将日志添加到示例代码中。