没有因使用“告诉”而产生的 (MonadWriter [Log] IO) 实例

No instance for (MonadWriter [Log] IO) arising from a use of ‘tell’

考虑 WriterWriterT 上的这个玩具 exercise:我们需要根据一组预定义的规则过滤数据包列表。我们还需要根据另一组规则记录一些数据包。现在考虑两个增强功能:

  1. 如果有重复的连续数据包(符合日志记录标准),我们应该只创建 1 个日志条目,并打印重复计数。 (目标是教授所谓的 'delayed logging' 技巧。)
  2. 我们需要 attach timestamp 每个日志条目。 (即使用 WriterT w IO

我已经实现了 1,但我坚持将它扩展到 2。首先,下面是 1 的代码。merge 函数处理当前数据包,但将潜在的日志条目传递给下一个决定是打印还是合并的步骤:

import Control.Monad
import Data.List
import Control.Monad.Writer

type Packet = Int
data Log = Log {
    packet :: Packet,
    acceptance :: Bool,
    bulk :: Int
  } deriving Show

instance Eq Log where
  (==) a b = packet a == packet b

incr :: Log -> Log
incr x = x {bulk = 1 + bulk x}

shouldAccept :: Packet -> Bool
shouldAccept = even

shouldLog :: Packet -> Bool
shouldLog p = p `mod` 4 < 2

type WriterP = Writer [Log] [Packet]

merge ::  (WriterP, [Log]) -> Packet -> (WriterP, [Log])
merge (prevWriter,prevLog) p = (newWriter,curLogFinal) where
  acc = shouldAccept p
  curLog = [Log p acc 1 | shouldLog p]
  curLogFinal = if null prevLog || prevLog /= curLog then curLog else incr <$> prevLog
  shouldTell = not (null prevLog) && prevLog /= curLog
  newWriter = do
            packets <- prevWriter
            when shouldTell $ tell prevLog
            return $ [p | acc] ++ packets

processPackets ::  [Packet] -> WriterP
processPackets packets = fst $ foldl' merge (return [],[]) packets

main :: IO ()
main = do
  let packets = [1,2,3,4,4,4,5,5,6,6] -- Ideally, read from a file
      (result,logged) = runWriter $ processPackets packets
      accepted = reverse result
  putStrLn "ACCEPTED PACKETS"
  forM_ accepted print
  putStrLn "\nFIREWALL LOG"
  forM_ logged print

对于 2,最初我想将待处理的日志条目作为 Writer 计算的一部分。类似于 WriterT [Log] IO ([Packet],[Log])。但是我不喜欢它,因为这两个增强原则上是无关的,如果日志要与计算混合,为什么还要使用 monad?

然后我(天真地)试图用IO包裹整个(WriterP, [Log])。当我继续修复类型错误时,事情似乎自行解决了(哈哈),但后来我遇到了这个 No instance for (MonadWriter [Log] IO) 障碍。 (请参阅下面的代码。)那是什么?一些自定义实例化有帮助吗,或者这条路是死胡同吗?


data Log = Log {
    -- ...
    timestamp :: UTCTime
  } deriving Show

type WriterPT = WriterT [Log] IO [Packet]

merge ::  IO (WriterPT, [Log]) -> Packet -> IO (WriterPT, [Log])
merge prevWriter_prevLog p = do
  t <- getCurrentTime
  (prevWriter,prevLog) <- prevWriter_prevLog
  let
    acc = shouldAccept p
    curLog = [Log p acc 1 t | shouldLog p]
    curLogFinal = if null prevLog || prevLog /= curLog then curLog else incr <$> prevLog
    shouldTell = not (null prevLog) && prevLog /= curLog
    newWriter = do
            packets <- prevWriter
            lift $ when shouldTell $ tell prevLog -- Error: No instance for (MonadWriter [Log] IO)
            return $ [p | acc] ++ packets
  return (newWriter,curLogFinal)

processPacketsMerged ::  [Packet] -> IO WriterPT
processPacketsMerged packets = fst <$> foldl' merge (return (return [],[])) packets

诚然它很丑,更因为我在 IO.

中嵌套 WriterT

那么..添加时间戳功能的巧妙方法是什么,

也欢迎其他评论:)

查看类型可能会有很大帮助:在您的第二个代码片段中,在函数 merge 中,newWriter 应该具有类型 WriterT [Log] IO [Packet].

问题出在 lift $ when shouldTell $ tell prevLog 行:您想要实现的行为是记录先前的日志,前提是 shouldTell 为真。如果您尝试在没有任何辅助函数的情况下编写此代码,您最终可能会得到如下代码:

do ...
   if shouldTell
     then (tell prevLog) -- log the previous log
     else (return ())    -- do nothing   
   ...

现在让我们看看when是如何实现的,是否可以用它来以更好的方式重写这段代码:

when :: (Applicative f) => Bool -> f () -> f ()
when p s  = if p then s else pure ()

这正是我们想要做的,如果条件为假,它会自动默认为默认操作。 所以我们可以把代码改成:

do ...
   when shouldTell $ tell oldLog
   ...

无需使用 lift,因为类型已经正确,关于缺少实例的错误现在消失了。

要调试此类错误,让类型检查器使用类型化孔来帮助您非常有用:

do ... 
   lift $ when shouldTell $ _ -- Found hole: _ :: IO ()
   ...

但是,您要执行的操作(即 tell prevLog)不是 IO 操作。至少这是帮助我理解 lift 不是必需的,您可以简单地使用 when。希望对您有所帮助!