Writer and Reader

We'll looks at two patterns of computations that produce "effects":

  1. producing, or "writing," something in addition to "real" output; and

  2. "reading" from "environment" variables separate from "real" arguments.

Though the programming examples we'll use are small, but they will put us in a favorable state of mind for things to come.

Writer_

newtype Writer_ w a = Writer_ { runWriter_ :: (a, w) }
  deriving (Show)

Example: Function Calls with Logging

Let's have the computation (2*) . (^2) . (2*) $ 3 explain its work.

We'll start by defining types to represent chatty functions.

data ChattyFunc a b =
  ChattyFunc { name :: String, call :: a -> b }

type Chatty b =
  Writer_ [String] b

applyChattyFunc :: (Show a, Show b) => ChattyFunc a b -> a -> Chatty b
applyChattyFunc chattyFunc a =
  let
    b = call chattyFunc a
    s = name chattyFunc ++ " " ++ show a ++ " = " ++ show b
  in
    Writer_ (b, [s])

printLog :: Chatty a -> IO ()
printLog = putStrLn . unlines . snd . runWriter_

Now we can construct and apply chatty functions.

square = ChattyFunc "square" (^2)
double = ChattyFunc "double" (2*)

doubleSquareDouble :: Int -> Chatty Int
doubleSquareDouble n0 =
  let
    Writer_ (n1, w1) = applyChattyFunc double n0
    Writer_ (n2, w2) = applyChattyFunc double n1
    Writer_ (n3, w3) = applyChattyFunc double n2
  in
    Writer_ (n3, w1 <> w2 <> w3)

Chat log:

> doubleSquareDouble 3
Writer_ (72,["Calling doubleSquareDouble","double 3 = 6","square 6 = 36","double 36 = 72"])

> printLog $ doubleSquareDouble 3
Calling doubleSquareDouble
double 3 = 6
square 6 = 36
double 36 = 72

Monad Instance

instance Monoid w => Monad (Writer_ w) where
 -- return :: a -> Writer_ a w
    return a = Writer_ (a, mempty)

 -- (>>=) :: Writer_ a w -> (a -> Writer_ w b) -> Writer_ w b
    Writer_ (a, w1) >>= f =
      let Writer_ (b, w2) = f a in
      Writer_ (b, w1 <> w2)

Free instances:

instance Monoid w => Applicative (Writer_ w) where
    pure  = return
    (<*>) = ap

instance Monoid w => Functor (Writer_ w) where
    fmap f x = pure f <*> x

Helper Functions

tell :: w -> Writer_ w ()
tell w = Writer_ ((), w)

censor :: (w -> w) -> Writer_ w a -> Writer_ w a
censor f (Writer_ (a, w)) = Writer_ $ (a, f w)
censor f                  = Writer_ . fmap f . runWriter_

More Chatting

doubleSquareDouble :: Int -> Chatty Int
doubleSquareDouble n0 = do
  tell ["Calling doubleSquareDouble"]
  n1 <- applyChattyFunc double n0
  n2 <- applyChattyFunc square n1
  n3 <- applyChattyFunc double n2
  pure n3

 

> printLog $ doubleSquareDouble 3
Calling doubleSquareDouble
double 3 = 6
square 6 = 36
double 36 = 72

> printLog $ censor (const []) $ doubleSquareDouble 3


> printLog $ censor (map (const "BLEEP")) $ doubleSquareDouble 3
BLEEP
BLEEP
BLEEP
BLEEP

> printLog $ censor (map (map (const '*'))) $ doubleSquareDouble 3
**************************
************
*************
**************

Reader (or Environment)

We've seen Functor and Applicative instances for ((->) t). There's a Monad instance too. This type is often called the "reader" — or "environment" — monad. We'll use the type variable env below to remind us of this name.

instance Monad ((->) env) where
 -- (>>=) :: (env -> a) -> (a -> env -> b) -> (env -> b)
    f >>= g = \env -> g (f env) env

The key is the type of "actions" (a -> env -> b) that are going to get stitched together. They're binary functions, where the second argument is an "environment." More precisely, it is the environment — the functions f and g do not produce new values of type env, so the environment "does not change."

It's hard to demonstrate the utility with a very small example. But as programs grow larger and many functions take the same set of environment (or configuration) parameters, the reader monad can make passing around these parameters implicit. The following contrived example...

applyWithMax f n max
  | f n > max = Nothing
  | otherwise = Just $ f n

squareWithMax = applyWithMax (^2)

foo :: (Int,Int,Int) -> Int -> Maybe Int
foo (n1,n2,n3) max =
  squareWithMax n1 max
    <|> squareWithMax n2 max
    <|> squareWithMax n3 max

suffices to demonstrate how the environment can be "hidden"...

foo' :: (Int,Int,Int) -> Int -> Maybe Int
foo' (n1,n2,n3) = do
  mn1 <- squareWithMax n1
  mn2 <- squareWithMax n2
  mn3 <- squareWithMax n3
  pure $ mn1 <|> mn2 <|> mn3

 

> foo' (10, 5, 3) 50
Just 25
> foo' (10, 5, 3) 50
Just 25

Note that this example actually depends only on the Applicative interface of ((->) env), not Monad.

foo'' :: (Int,Int,Int) -> Int -> Maybe Int
foo'' (n1,n2,n3) =
  pure (\mn1 mn2 mn3 -> mn1 <|> mn2 <|> mn3)
    <*> squareWithMax n1
    <*> squareWithMax n2
    <*> squareWithMax n3

We'll have more to say about Applicative and Monad later.

From Writer_ and Reader_ to Writer and Reader

These will pop out of the library in due course, as a result of rather more exotic features...

Source Files