考慮這個關于and的玩具練習:我們需要根據一組預定義的規則過濾資料包串列。我們還需要根據另一組規則記錄一些資料包。現在考慮兩個增強:WriterWriterT
- 如果出現重復的連續資料包(符合日志記錄標準),我們應該只創建 1 個日志條目,同時列印重復計數。(目標是教授所謂的“延遲記錄”技巧。)
- 我們需要為每個日志條目附加時間戳。(即使用
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?
然后我(天真地)試圖(WriterP, [Log])用IO. 當我繼續修復型別錯誤(哈哈)時,事情似乎自行解決了,但后來我遇到了這個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
誠然,它很丑,更因為我嵌套WriterT在IO.
那么..添加時間戳功能有哪些巧妙的方法,
- 對我的第一個代碼片段幾乎沒有什么改動?
- 否則?
也歡迎其他評論:)
uj5u.com熱心網友回復:
查看型別可能會有很大幫助:在您的第二個代碼片段中,函式merge中newWriter應該有 type 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. 我希望這能有所幫助!
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/487884.html
下一篇:如何將串列中的串列轉換為字串
