在 Writer monad 中传递和收听有什么意义?

What is the point of pass and listen in Writer monad?

我一直在 learnyouahaskell 书中学习有关 monad 的知识。 在阅读了 writer monad 之后,我决定查看 Control.Monad.Writer.Class.

的文档

在那里我看到他们还实现了 listenpass 功能,但我无法理解它们的用途。谁能给我一个很好的例子,这样我就可以理解如何使用 listenpass?

这是来自 Control.Monad.Trans.Writer.Strict 的代码,它定义了 listenpass:

listen :: (Monoid w, Monad m) => WriterT w m a -> WriterT w m (a, w)
listen m = WriterT $ do
    (a, w) <- runWriterT m
    return ((a, w), w)

pass :: (Monoid w, Monad m) => WriterT w m (a, w -> w) -> WriterT w m a
pass m = WriterT $ do
    ((a, f), w) <- runWriterT m
    return (a, f w)

WriterT 是一个 monad 转换器,它为其他 monad 提供 Writer 功能,简单的 Writer 类型定义为 type Writer w = WriterT w Identity,在 Identity monad绑定函数(<->>=)只是变量绑定,所以如果我们分解上面代码的单子部分:

listen :: (Monoid w) => Writer w a -> Writer w (a, w)
listen m = Writer $
    let (a, w) = runWriter m
    in ((a, w), w)

pass :: (Monoid w) => Writer w (a, w -> w) -> Writer w a
pass m = Writer $
    let ((a, f), w) = runWriter m
    in (a, f w)

现在很清楚,listen 使您可以访问 Writer monad 中的 Writer 操作生成的日志,pass 为您提供了一种在 Writer monad 中更改日志的方法。

假设您有一个 Writer [String] 并希望仅当它生成的日志满足某些条件时才记录操作:

deleteOn :: (Monoid w) => (w -> Bool) -> Writer w a -> Writer w a
deleteOn p m = pass $ do
    (a, w) <- listen m
    if p w
        then return (a, id)
        else return (a, const mempty)

-- Or pass alone
deleteOn' :: (Monoid w) => (w -> Bool) -> Writer w a -> Writer w a
deleteOn' p m = pass $ do
    a <- m
    return (a, (\w -> if p w then mempty else w))

logTwo :: Writer [String] ()
logTwo = do
    deleteOn ((> 5) . length . head) $ tell ["foo"]
    deleteOn ((> 5) . length . head) $ tell ["foobar"]
{-
*Main> runWriter logTwo
((),["foo"])
-}

Writer 中,您无法检查写入的内容,直到您使用 运行(或 "unwrap")monad,使用 execWriterrunWriter.但是,您可以使用 listen 检查某些 sub-action 写入写入器的内容,然后再将值附加到写入器状态,并且您可以使用 pass 修改写入的内容。

考虑这个使用 WriterT 进行日志记录的应用程序示例:

import Control.Monad.Writer

-- Monad stack for the application uses IO, wrapped in a logging WriterT
type App a = WriterT [String] IO a

-- utility to write to the log
logMsg :: String -> App ()
logMsg msg = tell [msg]

-- gets an Int from user input (and logs what it does)
getInt :: App Int
getInt = do
    logMsg "getting data"
    n <- liftIO getLine
    logMsg $ "got line: " ++ show n
    return . read $ n

-- application logic that uses getInt and increments the result by 1
app :: App Int
app = do
    n <- getInt
    return $ n + 1

-- main code runs the application and prints the log
main = do
    (res, logs) <- runWriterT app
    print $ "Result = " ++ show res
    putStrLn "Log: "
    mapM_ putStrLn logs

现在,出于某种原因,在 app 中,我想知道 getInt 向日志写入了哪些消息。我可以用 listen:

app :: App Int
app = do
    (n, logs) <- listen getInt
    let numLogLines = length logs
    logMsg $ "getInt logged " ++ show numLogLines ++ " lines"
    return $ n + 1