I have been learning about monads in the book learnyouahaskell. After reading about the writer monad, I decided to check the documentation of the Control.Monad.Writer.Class.
There I saw that they also implemented the listen
and pass
functions but I was not able to understand what they are used for. Could someone give me a good example so I can understand how to use listen
and pass
?
A monad is an algebraic structure in category theory, and in Haskell it is used to describe computations as sequences of steps, and to handle side effects such as state and IO. Monads are abstract, and they have many useful concrete instances. Monads provide a way to structure a program.
The Maybe sum type is a useful data type that forms a functor. Like many other useful functors, it also forms a monad.
To create a monad, it is not enough just to declare a Haskell instance of the Monad class with the correct type signatures. To be a proper monad, the return and >>= functions must work together according to three laws: (return x) >>= f ==== f x.
Lists are a fundamental part of Haskell, and we've used them extensively before getting to this chapter. The novel insight is that the list type is a monad too! As monads, lists are used to model nondeterministic computations which may return an arbitrary number of results.
Here is code from Control.Monad.Trans.Writer.Strict
that defined listen
and pass
:
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
is a monad transformer which provides Writer
functionality to other monads, the simple Writer
type is defined as type Writer w = WriterT w Identity
, in Identity
monad the bind function (<-
and >>=
) is just variable binding, so if we break down the monadic part of the code above:
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)
It's now clear that listen
gives you access to the log produced by a Writer action inside the Writer monad, and pass
gives you a way to alter the log inside the Writer monad.
Assume you have a Writer [String]
and want to log an action only if the log produced by it satisfies some condition:
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"])
-}
Inside a Writer
you can't inspect what has been written, until you run (or "unwrap") the monad, using execWriter
or runWriter
. However, you can use listen
to inspect what some sub-action wrote to the writer, before the value is appended to the writer state, and you can use pass
to modify what is written.
Consider this example of an application that uses WriterT
for logging:
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
Now, for some reason, inside app
, I want to know what messages getInt
wrote to the log. I can do that with listen
:
app :: App Int
app = do
(n, logs) <- listen getInt
let numLogLines = length logs
logMsg $ "getInt logged " ++ show numLogLines ++ " lines"
return $ n + 1
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With