没有因使用“告诉”而产生的 (MonadWriter [Log] IO) 实例
No instance for (MonadWriter [Log] IO) arising from a use of ‘tell’
考虑 Writer
和 WriterT
上的这个玩具 exercise:我们需要根据一组预定义的规则过滤数据包列表。我们还需要根据另一组规则记录一些数据包。现在考虑两个增强功能:
- 如果有重复的连续数据包(符合日志记录标准),我们应该只创建 1 个日志条目,并打印重复计数。 (目标是教授所谓的 'delayed logging' 技巧。)
- 我们需要 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
。希望对您有所帮助!
考虑 Writer
和 WriterT
上的这个玩具 exercise:我们需要根据一组预定义的规则过滤数据包列表。我们还需要根据另一组规则记录一些数据包。现在考虑两个增强功能:
- 如果有重复的连续数据包(符合日志记录标准),我们应该只创建 1 个日志条目,并打印重复计数。 (目标是教授所谓的 'delayed logging' 技巧。)
- 我们需要 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
。希望对您有所帮助!