(Not updated very recently)
We’ll looks at two patterns of computations that produce “effects”:
producing, or “writing,” something in addition to “real” output; and
“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.
newtype Writer_ w a = Writer_ { runWriter_ :: (a, w) }
deriving (Show)
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
w0 = "Calling doubleSquareDouble"
Writer_ (n1, w1) = applyChattyFunc double n0
Writer_ (n2, w2) = applyChattyFunc square n1
Writer_ (n3, w3) = applyChattyFunc double n2
in
Writer_ (n3, w0 <> 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
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)
instance Monoid w => Applicative (Writer_ w) where
pure = return
(<*>) = ap
instance Monoid w => Functor (Writer_ w) where
fmap f x = pure f <*> x
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_
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
**************************
************
*************
**************
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.
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…