Skip to content

HW 7: Discovering Monads by Building Interpreters

In this assignment you write four versions of one tiny evaluator. The first one just evaluates. The other three also report errors, count divisions, or keep a log, and each of those jobs adds its own bookkeeping to the evaluator. Then you factor that bookkeeping out into >>= and rewrite the evaluators in monadic style. The language has only integers and division, so nothing distracts from how the evaluator is built.

It's the same move as followedBy in Lab 7, applied to an interpreter.

How you'll get materials and submit this assignment

Getting the starter code and turning in your work follows the same process as every homework this semester. See The Homework Workflow for the full walkthrough. In short: from the hw7 template repo, make your own private copy, clone it to the course server, and submit your test output and code on Gradescope, along with a few short written answers. The rest of this page covers what the assignment actually asks of you.

There isn't much code to write. Most TODOs are one to five lines, and the whole assignment is around 80 lines of Haskell. The real work is understanding what each version has to carry along, and noticing what stays the same.

What You'll Do

  • Tell apart the essential work of an evaluator and the extra plumbing needed for error handling, counting, or logging.
  • Represent each kind of computation with a Haskell datatype of its own.
  • Trace how values, errors, a counter, or log text move through a recursive evaluation.
  • Derive an operation like >>= by factoring out repeated code.
  • Write Functor, Applicative, and Monad instances for your own types.
  • Rewrite the same evaluator using pure, >>=, and do notation.
  • Check the monad laws on concrete examples, and explain what they guarantee.

Checklist

  • Get the assignment materials (see The Homework Workflow)
  • Part 1: evalPlain
  • Part 2: safeDiv and evalSafe (handle errors by hand)
  • Part 3: evalCount (pass the counter along by hand)
  • Part 4: evalTrace (build the log by hand)
  • Part 5: safePure, safeBind, and evalSafeWithBind
  • Part 6: SafeResult instances, evalSafeM, and evalPlainM
  • Part 7: countBind, Counted instances, and evalCountM
  • Part 8: Traced instances and evalTraceM
  • Part 9: write evalWith, compare all four, and check a monad law
  • Turn in the assignment (see Turning It In)

Collaboration

You can work with a partner on this assignment. If you do, you must cooperate on every part, follow the pair-programming rules from the syllabus at all times (even while writing prose), and share roles equally.


Background: Wadler's Evaluators

This assignment walks through the evaluator examples in Philip Wadler's paper Monads for Functional Programming (1995), especially Section 2. There, Wadler takes one tiny evaluator, changes it three ways (exceptions, state, and output), and then shows how a monad lets each change happen in one place.

You don't need to read the paper to do the assignment, because everything you need is on this page. It's worth reading afterward, though, to see how closely your own work matches his. One thing to know if you do: Wadler calls every version eval. Here each version gets its own name, so you can keep all of them in one file and compare them.


The File

Everything happens in HW07.hs. It's split into sections that match the Parts below, with type signatures and undefined placeholders. Work through the Parts in order.

undefined marks a step you haven't done yet. The file still loads with these placeholders in it, but calling an unfinished function gives an error.

Keep every evaluator. Don't delete the by-hand versions after you write the monadic ones. Comparing them is the whole point.

How to Work: GHCi and Tests

GHCi

ghci HW07.hs

After each change, run :reload. Each Part ends with a ✅ Check your work box of expressions to try. At the bottom of HW07.hs there's also a list called checks, made up of named True/False tests. When you've finished everything, this should print True:

all snd checks   -- True

Tests

From the top of the repo, this runs the whole test suite:

runhaskell -itest test/Spec.hs

To run only some tests, add --match and a piece of text. Every test whose name contains that text runs. The tests are grouped by the Part numbers on this page, so:

runhaskell -itest test/Spec.hs --match "Part 3"     # just Part 3
runhaskell -itest test/Spec.hs --match "evalSafe"   # evalSafe, evalSafeWithBind, and evalSafeM

Until you finish a TODO, its tests fail with Prelude.undefined and the line number in HW07.hs. That's expected, and the rest of the tests still run. (A few tests check provided code, so they pass from the start.)

What the tests can't check

The tests check what your functions return. They can't tell whether your monadic versions really let the Monad instance do the plumbing, or whether your by-hand versions still do it by hand. That part is up to you, and it's what the written questions at the end are about.

Checkpoints

Each Part ends with a Checkpoint question. Checkpoints 3, 5, and 8 are graded on Gradescope. The others aren't submitted, but they feed into the Part 9 comparison table, which is. Jot a sentence or two in a scratch file as you go (see Turning It In).


Part 0: The Smallest Possible Language

Every evaluator in this homework works on the same abstract syntax tree (AST):

data Term
  = Num Int
  | Div Term Term
  deriving (Show, Eq)

Num n is an integer literal. Div t u means: evaluate t and u, then divide the first result by the second using integer division (Haskell's div).

HW07.hs includes some sample terms:

tOk :: Term
tOk = Div (Div (Num 1999) (Num 2)) (Num 23)

tBad :: Term
tBad = Div (Num 1) (Num 0)

tNested :: Term
tNested = Div (Div (Num 8) (Num 2)) (Num 2)

In ordinary arithmetic, tOk is:

  (1999 `div` 2) `div` 23
= 999 `div` 23
= 43

You'll use these terms throughout. tBad is the interesting one: it separates evaluators that crash on division by zero from evaluators that represent failure as data. For every evaluator you write, try it on all three: tOk, tBad, and tNested.


Part 1: The Plain Evaluator, evalPlain

Start with the simplest possible interpretation:

evalPlain :: Term -> Int

There are exactly two cases:

  1. A number evaluates to that number.
  2. A division evaluates each child recursively, then divides the results.

Implement both equations. This should take only a few lines.

✅ Check your work (Part 1)

evalPlain (Num 7)   -- 7
evalPlain tOk       -- 43
evalPlain tBad      -- *** Exception: divide by zero
evalPlain tNested   -- 2

Tests: runhaskell -itest test/Spec.hs --match "Part 1"

evalPlain tBad raises Haskell's divide-by-zero exception. For now that's expected.

Checkpoint 1

Suppose you wanted evalPlain tBad to report the problem instead of crashing, without changing the type Term -> Int. Could you? What would you have to change?


Part 2: Error Reporting, evalSafe

Now let's make the evaluator handle division by zero without crashing. It should return an ordinary value that describes either success or failure:

data SafeResult a
  = Exception String
  | Value a
  deriving (Show, Eq)

SafeResult Int is either an integer or an explanatory error message: Value 43, or Exception "division by zero".

Step 2.1: The Division Check

Implement:

safeDiv :: Int -> Int -> SafeResult Int

It returns Exception "division by zero" for a zero denominator, and otherwise Value holding the integer quotient.

Step 2.2: An Evaluator That Handles Errors Explicitly

Implement:

evalSafe :: Term -> SafeResult Int

For Num n, return Value n. For Div t u:

  1. Call evalSafe t.
  2. If it fails, return the same Exception immediately.
  3. If it succeeds, call evalSafe u.
  4. If that fails, return its Exception immediately.
  5. If both succeeded, call safeDiv on the two integers.

Use nested case expressions. Don't use >>=, do, or any other monadic shortcut yet. The point is to see the repetition before you hide it. Here is the shape (HW07.hs has it too):

evalSafe (Div t u) =
  case evalSafe t of
    Exception msg -> ...
    Value a ->
      case evalSafe u of
        Exception msg -> ...
        Value b -> ...

✅ Check your work (Part 2)

safeDiv 10 2       -- Value 5
safeDiv 10 0       -- Exception "division by zero"
evalSafe tOk       -- Value 43
evalSafe tBad      -- Exception "division by zero"
evalSafe tNested   -- Value 2

Tests: runhaskell -itest test/Spec.hs --match "Part 2"

Checkpoint 2

Compare evalSafe with evalPlain. Which lines are actually about evaluating division? Which are only about checking and passing along failures? How much longer is the recursive case?


Part 3: Counting Divisions, evalCount

This version ignores errors. Instead, it keeps track of how many successful divisions it performs.

A program could use a mutable global counter, but we can also pass an immutable counter explicitly from one computation to the next. A counted computation:

  • receives the current counter,
  • returns its ordinary result,
  • and also returns the updated counter.
newtype Counted a = Counted (Int -> (a, Int))

Read Counted a as "a computation that produces an a while carrying an integer counter." A newtype is like a data declaration with exactly one constructor holding exactly one thing, here a function (see the ParserBase reference, where Parser is built the same way). It gives Counted a type of its own, so later we can make it an instance of Monad. A plain type synonym couldn't do this: type Counted a = Int -> (a, Int) would just be another name for a function type, not a new type, so it couldn't have its own Monad instance.

HW07.hs provides a function that takes the function back out of a Counted and runs it on a starting counter:

runCounted :: Counted a -> Int -> (a, Int)
runCounted (Counted f) = f

A computation that produces 7 without changing the counter is Counted (\count -> (7, count)). One that produces "hi" and adds 1 to the counter is Counted (\count -> ("hi", count + 1)). A Counted value is a function waiting for a starting counter. Run one with runCounted by supplying that counter:

runCounted (Counted (\count -> (7, count))) 0          -- (7,0)
runCounted (Counted (\count -> ("hi", count + 1))) 0   -- ("hi",1)
runCounted (Counted (\count -> ("hi", count + 1))) 5   -- ("hi",6)

The same computation gives a different final count depending on where it starts.

HW07.hs also provides runFromZero computation = runCounted computation 0.

Step 3.1: Thread the Counter by Hand

Implement:

evalCount :: Term -> Counted Int

Each case builds a Counted computation, a function waiting for the current count. For Num n, that function should produce the pair of n and the count it was given, leaving the counter unchanged. For Div t u, when the computation runs starting at count0, it must:

Four boxes stacked top to bottom, connected by downward arrows. An arrow labeled count0 enters "run the left child, starting at count0". The arrow below it is labeled count1 and "left value", leading to "run the right child, starting at count1". The next arrow is labeled count2 and "right value", leading to "calculate the division: left value div right value". The last arrow is labeled count2 and "result", leading to a green box "produce the pair (result, count2 + 1)". A key at the bottom marks counter labels in orange and value labels in blue.

The right child gets the counter produced by the left child, not the original one. HW07.hs provides the let skeleton for the recursive case. Fill it in using runCounted and Counted.

✅ Check your work (Part 3)

runFromZero (evalCount (Num 7))   -- (7,0)
runFromZero (evalCount tOk)       -- (43,2)
runFromZero (evalCount tBad)      -- (*** Exception: divide by zero
runFromZero (evalCount tNested)   -- (2,2)

tOk has two Div nodes, so the counter ends at 2. This version is only about counting, so it doesn't handle division by zero.

Tests: runhaskell -itest test/Spec.hs --match "Part 3"

Checkpoint 3

Trace the values of count0, count1, and count2 in evalCount (Div t u). Why can't we just evaluate the left and right subtrees independently, both starting from the original count? Give a concrete term where that would produce the wrong count.


Part 4: Recording a Trace, evalTrace

This version ignores errors and counting, and keeps a written record of its work instead. It doesn't print as it goes. It returns the trace as a string, which keeps the evaluator pure:

data Traced a = Traced String a
  deriving (Show, Eq)

A traced result holds (the accumulated text, the computed value). HW07.hs provides a helper:

traceLine :: Term -> Int -> String
traceLine term n =
  "eval(" ++ show term ++ ") = " ++ show n ++ "\n"

traceLine only formats a line of text. It doesn't evaluate anything, so you pass in the term and the value it should report:

traceLine (Num 7) 7                 -- "eval(Num 7) = 7\n"
traceLine (Div (Num 8) (Num 2)) 4   -- "eval(Div (Num 8) (Num 2)) = 4\n"
putStr (traceLine (Div (Num 8) (Num 2)) 4)
-- eval(Div (Num 8) (Num 2)) = 4

GHCi shows a String with quotes and \n. putStr prints it as a line of text.

Step 4.1: Concatenate the Log by Hand

Implement:

evalTrace :: Term -> Traced Int

Make every log line with traceLine, so the format matches the tests. For Num n, return n and the line traceLine (Num n) n. For Div t u:

  1. Evaluate the left child, giving a value and some log text.
  2. Evaluate the right child, giving another value and more log text.
  3. Compute the quotient.
  4. Concatenate the left trace, the right trace, and a line for this division, in that order.
  5. Return that trace and the quotient together.

HW07.hs gives you a let skeleton for this case too.

✅ Check your work (Part 4)

evalTrace (Num 7)
-- Traced "eval(Num 7) = 7\n" 7

traceSummary (evalTrace tOk)       -- (43,5)
traceSummary (evalTrace tBad)      -- (*** Exception: divide by zero
traceSummary (evalTrace tNested)   -- (2,5)

traceSummary (provided) gives the value and the number of trace lines. The five lines are three number leaves and two divisions. To see the whole trace, try let Traced text _ = evalTrace tOk in putStr text. Using putStr shows each \n as a real line break. Like evalCount, this version doesn't handle division by zero.

Tests: runhaskell -itest test/Spec.hs --match "Part 4"

Checkpoint 4

Put the Div cases of evalPlain, evalSafe, evalCount, and evalTrace side by side. What do all four have in common? For each of the last three, which lines are there only to carry the extra information along?

Puzzle (optional): a crash that isn't one

evalCount and evalTrace don't handle division by zero, so you'd expect tBad to crash them right away. Try snd (runFromZero (evalCount tBad)) and let Traced text _ = evalTrace tBad in putStr text. You get a count, and most of a trace, before anything goes wrong. Why? (Hint: Haskell is lazy. When does a `div` b actually get computed?)


Part 5: Discovering the Abstraction with SafeResult

Start with the easiest kind of bookkeeping: failure. Look again at the shape that kept repeating in Part 2:

case earlierResult of
  Exception msg -> Exception msg
  Value x       -> nextStep x

None of the error-handling logic depends on which computation comes next. So it can go in a helper that takes the earlier result and a function for the next step.

Before you read on, write down what you think that helper's type should be. Then compare your guess with Step 5.2.

Step 5.1: Put an Ordinary Value into a Result

Implement safePure :: a -> SafeResult a, which represents an ordinary successful result: safePure 5 is Value 5.

Step 5.2: Connect One Result to the Next

Implement:

safeBind :: SafeResult a
         -> (a -> SafeResult b)
         -> SafeResult b

Here's how the pieces of the type line up:

SafeResult a  ->  (a -> SafeResult b)  ->  SafeResult b
    ^                     ^                    ^
 earlier work      function describing      combined result
                   what to do next

The next function expects a plain a, not a SafeResult a. safeBind resolves that mismatch:

  • On Exception msg, pass the error along and don't call the next function.
  • On Value x, call the next function with x.
safeBind (Value 7) (\x -> Value (x + 1))           -- Value 8
safeBind (Exception "oops") (\x -> Value (x + 1))  -- Exception "oops"

In the second example, the lambda is never called.

Step 5.3: Refactor the Evaluator, Before Any Typeclass

Implement:

evalSafeWithBind :: Term -> SafeResult Int

Use safePure for Num, and two calls to safeBind for Div. No nested case expressions. Use safeDiv for the final step, the one that might fail. The recipe:

evaluate left
  THEN take its successful value a
  evaluate right
    THEN take its successful value b
    safely divide a by b

Hint: two lambdas, one inside the other

Div has two children to evaluate, and each result goes through its own safeBind. Each safeBind needs a function that says what to do next, so you'll write two lambdas: one that receives a and one that receives b. The second lambda goes inside the body of the first. That's the only way a is still in scope when you finally divide a by b. Each THEN in the recipe above is a safeBind, and the indentation shows the nesting.

Compare the result to evalSafe.

✅ Check your work (Part 5)

safePure 5                                         -- Value 5
safeBind (Value 7) (\x -> Value (x + 1))           -- Value 8
safeBind (Exception "oops") (\x -> Value (x + 1))  -- Exception "oops"
evalSafeWithBind tOk                               -- Value 43
evalSafeWithBind tBad                              -- Exception "division by zero"
evalSafeWithBind tNested                           -- Value 2

Tests: runhaskell -itest test/Spec.hs --match "Part 5"

Checkpoint 5

Explain why safePure (a `div` b) is not a substitute for safeDiv a b. Where does the actual division-by-zero check belong?


Part 6: Making SafeResult a Real Monad

You've now invented two operations with very general-looking types:

safePure :: a -> SafeResult a
safeBind :: SafeResult a -> (a -> SafeResult b) -> SafeResult b

Haskell's standard names for these are:

pure  :: Applicative m => a -> m a
(>>=) :: Monad m => m a -> (a -> m b) -> m b

>>= is pronounced "bind". (Wadler calls pure "unit", and older code often uses return. This assignment always uses pure.)

Why is pure in Applicative and not Monad? Haskell organizes these interfaces in a hierarchy:

Functor           fmap
    |
Applicative       pure, (<*>)
    |
Monad             (>>=)

(<*>) applies a wrapped function to a wrapped value. For example, Value (+1) <*> Value 2 is Value 3. In this assignment you mostly need it because Haskell won't accept a Monad instance unless there's an Applicative instance too. You met Functor in Module 7.1.

Step 6.1: Three Instances for SafeResult

Complete the Functor, Applicative, and Monad instances in HW07.hs:

  • Functor: the same idea as fmap on Maybe. Transform the value in Value x, and leave an Exception unchanged.
  • Applicative: pure behaves like safePure. Exception msg <*> anything keeps that error. Value f <*> result applies f to the value inside result, if there is one. You can use fmap for this.
  • Monad: reuse exactly the two cases you discovered in safeBind. It should look almost identical.

These instances are for your own type

You can define instances for SafeResult because you created the type. Don't try to redefine Haskell's existing Monad Maybe instance.

Once your instances compile, try these in GHCi:

fmap (+1) (Value 2)                        -- Value 3
fmap (+1) (Exception "oops")               -- Exception "oops"
pure 5 :: SafeResult Int                   -- Value 5
Value (+1) <*> Value 2                     -- Value 3
Value (+1) <*> Exception "oops"            -- Exception "oops"
Value 7 >>= \x -> Value (x + 1)            -- Value 8
Exception "oops" >>= \x -> Value (x + 1)   -- Exception "oops"
Value 7 >>= \x -> safeDiv 84 x             -- Value 12
Value 0 >>= \x -> safeDiv 84 x             -- Exception "division by zero"

Step 6.2: Rewrite the Evaluator a Third Time

Implement:

evalSafeM :: Term -> SafeResult Int

This time use pure and >>=, or the equivalent do notation, and finish with safeDiv. The evaluator should never inspect Exception or Value directly.

Once it's done, try:

evalSafeM tOk       -- Value 43
evalSafeM tBad      -- Exception "division by zero"
evalSafeM tNested   -- Value 2

Step 6.3: Check a Monad Law

The right identity law says:

m >>= pure  ==  m

Try it for both constructors:

(Value 7 >>= pure) == Value 7                                         -- True
(Exception "oops" >>= pure) == (Exception "oops" :: SafeResult Int)   -- True

Step 6.4: A Tiny Bridge Back to the Plain Evaluator

Wadler also treats the simplest computation, one that carries no extra information, as a monad. HW07.hs provides it, instances included:

newtype Plain a = Plain a
  deriving (Show, Eq)

unPlain :: Plain a -> a
unPlain (Plain x) = x

-- its bind just passes the value along:
--   Plain x >>= next = next x

These work already:

fmap (+1) (Plain 2)                -- Plain 3
pure 5 :: Plain Int                -- Plain 5
Plain (+1) <*> Plain 2             -- Plain 3
Plain 7 >>= \x -> Plain (x * 2)    -- Plain 14

Your job is just to write:

evalPlainM :: Term -> Plain Int

using pure and do notation. It should mirror evalSafeM almost exactly, except that its last step can use ordinary div, because it has no error reporting. This version adds almost no behavior. It's there so the comparison of all four monadic evaluators is complete.

✅ Check your work (Part 6)

evalSafeM tOk                  -- Value 43
evalSafeM tBad                 -- Exception "division by zero"
evalSafeM tNested              -- Value 2
unPlain (evalPlainM tOk)       -- 43
unPlain (evalPlainM tBad)      -- *** Exception: divide by zero
unPlain (evalPlainM tNested)   -- 2

Tests: runhaskell -itest test/Spec.hs --match "Part 6", and --match "right identity" for the law.

Checkpoint 6

Explain by considering both constructors why right identity holds for your SafeResult. Which branch of your bind handles each case?


Part 7: The Same Idea for the Counter

Now return to the counter threading from Part 3. A Counted a is, underneath, an Int -> (a, Int). The first counter goes to the computation that runs first, and the next computation must receive its updated counter.

Step 7.1: One Useful Counted Operation

HW07.hs provides:

tick :: Counted ()
tick = Counted (\count -> ((), count + 1))

tick increments the counter without producing an interesting value. The value () means "nothing useful to report." Its type is also ().

Like any Counted computation, tick does nothing until you run it with a starting counter:

runCounted tick 0   -- ((),1)
runCounted tick 1   -- ((),2)
runCounted tick 2   -- ((),3)
runCounted tick 3   -- ((),4)

Step 7.2: Factor Out the Counter Threading

Do for Counted what Part 5 did for SafeResult. Implement:

countBind :: Counted a -> (a -> Counted b) -> Counted b

countBind first next is a counted computation. Given a starting counter, it runs first, passes the resulting value to next, and runs the computation next returns on the updated counter. Look at the let you wrote in evalCount. If this feels familiar, it's because followedBy in Lab 7 did the same thing with a stack.

countBind builds a computation. It doesn't run one. Calling countBind first next runs nothing. It returns a Counted b, which is a function waiting for a starting counter. The running happens later, when runCounted hands that function a count. So your answer will wrap a function of the starting counter in Counted, the same shape as the Div case of your evalCount.

Inside that function, the pieces line up like this:

Name Type Where it comes from
count0 Int the starting counter your function receives
first Counted a an argument. It's wrapped, so you can't apply it to a number directly. runCounted unwraps it.
x, count1 a, Int what running first from count0 gives back
next x Counted b the next computation, chosen using x (not run yet)
the answer (b, Int) what running next x from count1 gives back

Here's a worked run with two small computations. five produces 5 and counts one step. double x produces twice x and counts one step:

let five = Counted (\count -> (5, count + 1))
let double x = Counted (\count -> (x * 2, count + 1))

runCounted (countBind five double) 3 should go:

count0 = 3
run five from 3        ->  x = 5, count1 = 4
next x = double 5      ->  a computation, not run yet
run double 5 from 4    ->  (10,5)

Try these once countBind is written:

runCounted (countBind five double) 3         -- (10,5)
runCounted (countBind five double) 0         -- (10,2)
runCounted (countBind tick (\_ -> five)) 0   -- (5,2)
runCounted (countBind tick (\_ -> tick)) 0   -- ((),2)

If the first one gives (10,4), you ran double 5 from count0 instead of count1, so the step five counted was lost.

Step 7.3: Finish the Instances for Counted

The provided code includes fmap and <*>. You implement pure (in the Applicative instance) and >>= (in the Monad instance). >>= should do exactly what countBind does, so first >>= next = countBind first next is a fine answer. pure x produces x without changing the counter (compare the 7 example in Part 3).

Once your instances compile, try these in GHCi (five is from Step 7.2):

runCounted (pure 5) 3                          -- (5,3)
runCounted (fmap (* 2) five) 0                 -- (10,1)
runCounted (pure (+1) <*> five) 0              -- (6,1)
runCounted (five >>= \x -> pure (x * 2)) 0     -- (10,1)
runCounted (tick >>= \_ -> five) 10            -- (5,12)

fmap and pure change only the value. The count goes up only when a computation like five or tick runs.

Step 7.4: Rewrite the Evaluator

Implement:

evalCountM :: Term -> Counted Int

using do notation, pure for the ordinary result, and tick to count the division. Start from your evalSafeM. The only new ingredient is tick.

✅ Check your work (Part 7)

runCounted (countBind tick (\_ -> tick)) 0   -- ((),2)
runFromZero (evalCount tOk)                  -- (43,2)
runFromZero (evalCountM tOk)                 -- (43,2)
runFromZero (evalCountM tBad)                -- (*** Exception: divide by zero
runFromZero (evalCountM tNested)             -- (2,2)
runCounted (evalCountM tOk) 10               -- (43,12)

Tests: runhaskell -itest test/Spec.hs --match "Part 7"

Checkpoint 7

Compare evalCountM and evalSafeM. What is structurally the same in the recursive case? What differs? evalCountM never mentions a counter variable. Where did count0, count1, and count2 go?


Part 8: The Same Idea for Traces

The third variation produced both a value and some log text, Traced "some log text" 42 :: Traced Int.

Step 8.1: Finish the Instances for Traced

The provided code includes fmap and <*>. You implement pure and >>=:

pure produces the value with an empty log. Your bind should:

  1. Start with the first traced result, which has text text1 and value x.
  2. Call the next function with x.
  3. Get back text text2 and the next value.
  4. Return the combined trace text1 ++ text2 with the next value.

It's the same type pattern as error handling and counting, but the bookkeeping is different.

Once your instances compile, try these in GHCi:

pure 5 :: Traced Int                                -- Traced "" 5
fmap (+1) (Traced "a\n" 2)                          -- Traced "a\n" 3
Traced "f\n" (+1) <*> Traced "x\n" 2                -- Traced "f\nx\n" 3
Traced "one\n" 7 >>= \x -> Traced "two\n" (x * 2)   -- Traced "one\ntwo\n" 14
record "hi\n" >>= \_ -> pure 5                      -- Traced "hi\n" 5

Step 8.2: One Useful Traced Operation

HW07.hs provides:

record :: String -> Traced ()
record message = Traced message ()

record is a small computation whose only job is to add a message to the trace.

record on its own gives a Traced with your message and the useless value (). It becomes useful when you chain it with >>= to whatever comes next. Bind keeps the message and throws away the ():

record "hi\n"                                          -- Traced "hi\n" ()
record "one\n" >>= \_ -> Traced "two\n" 42             -- Traced "one\ntwo\n" 42
record "one\n" >>= \_ -> pure 42                       -- Traced "one\n" 42
record "a\n" >>= \_ -> record "b\n" >>= \_ -> pure 7   -- Traced "a\nb\n" 7

In do notation, a line with no <- means exactly >>= \_ ->: run it, keep its text, ignore its value. So the third example is the same as:

do record "one\n"
   pure 42

Step 8.3: Rewrite the Evaluator

Implement:

evalTraceM :: Term -> Traced Int

Use do notation in both cases:

  • Num n: a two-line do block. Use record to add the line for the number (made with traceLine, as in evalTrace), then pure n. This is the shape of the last example in Step 8.2.
  • Div t u: a do block that evaluates both children, uses record for the division's line, then returns the quotient. It should look a lot like evalCountM, with record doing the job tick did there. Record after both children, so the trace comes out in the same order as evalTrace's.

You'll need the quotient twice, once in the log line and once as the result. To name it, use let inside do. It has no in:

do a <- ...
   b <- ...
   let q = a `div` b
   ...

✅ Check your work (Part 8)

evalTraceM (Num 7)                  -- Traced "eval(Num 7) = 7\n" 7
traceSummary (evalTraceM tOk)       -- (43,5)
traceSummary (evalTraceM tBad)      -- (*** Exception: divide by zero
traceSummary (evalTraceM tNested)   -- (2,5)
evalTraceM tOk == evalTrace tOk     -- True

Tests: runhaskell -itest test/Spec.hs --match "Part 8"

Checkpoint 8

Change your bind to concatenate text2 ++ text1 instead, reload, and look at the trace for tOk (let Traced text _ = evalTraceM tOk in putStr text). What changed, and what didn't? Explain why, using your bind. Then change it back.


Part 9: Seeing the Common Computation Again

Put your four monadic division cases side by side: evalPlainM, evalSafeM, evalCountM, and evalTraceM. Each should have this recognizable shape:

do
  a <- eval... t
  b <- eval... u
  -- possibly one piece of variation-specific work
  -- produce a result

Step 9.1: One Evaluator for All Four

The four monadic evaluators differ only in two places: what happens after a node is evaluated, and how to divide. So you can write the evaluator once, for any monad, and pass those two pieces in:

evalWith :: Monad m
         => (Term -> Int -> m ())    -- afterNode
         -> (Int -> Int -> m Int)    -- divide
         -> Term -> m Int

The first argument, afterNode, is a function that evalWith calls after evaluating each node, numbers included. It gets the node and the value that node evaluated to: afterNode (Num n) n for a number, and afterNode (Div t u) v for a division that came out to v. It returns m (), a step that produces no useful value but can do this monad's extra work. Most evaluators don't need it and pass \_ _ -> pure (). The trace evaluator uses it to record a line.

The second argument, divide, gets the two evaluated children and produces the quotient however this monad needs. For example, the safe evaluator would pass safeDiv, which reports division by zero.

For Num n, call afterNode and return n. For Div t u, evaluate left, evaluate right, divide, call afterNode on the result, and return it.

Tip: start from an evaluator you already wrote

evalWith is one of your …M evaluators with its specific parts swapped out for the hooks. Copy your evalTraceM into evalWith and change it in three places. The record lines become calls to afterNode. The division becomes a call to divide. The recursive calls become calls to evalWith.

Tip: three things that go wrong in evalWith

  • The recursive calls need the hooks too. evalWith takes the two hooks before the term, so the children are evaluated with the same afterNode and divide you were given.
  • divide is a step, not a plain value. It returns m Int, so get its result with <-, not let. (In evalTraceM you used let, because div is an ordinary function.)
  • afterNode runs on numbers too. If you forget it in the Num case, the trace loses its leaf lines.

Then implement evalPlainW, evalSafeW, evalCountW, and evalTraceW, each with a single call to evalWith. Each should agree with its …M version. Think about which hook does the extra work in each case. For some evaluators, one of the hooks does nothing (\_ _ -> pure ()).

Tip: fill in this table first

For each evaluator, answer two questions. What extra work happens after every node? What extra work happens when dividing?

Evaluator afterNode (every node) divide (each division)
evalSafeW nothing safeDiv, which may fail
evalPlainW ? ?
evalCountW ? ?
evalTraceW ? ?

A hook that does nothing still has to be a step in the monad. That's why "nothing" is written with pure.

Tip: name a hook, test it, then plug it in

A hook is just a function. You can define it at the top level of HW07.hs with its own type signature, and try it in GHCi before passing it to evalWith. For example, if you write a counting divide countDivide :: Int -> Int -> Counted Int, then runCounted (countDivide 7 2) 0 should give (3,1). A trace hook traceAfter :: Term -> Int -> Traced () should give traceAfter (Num 7) 7 ⟹ Traced "eval(Num 7) = 7\n" (). Once each hook works on its own, each …W definition is one line.

✅ Check your work (Step 9.1)

unPlain (evalPlainW tOk)         -- 43
evalSafeW tBad                   -- Exception "division by zero"
runFromZero (evalCountW tOk)     -- (43,2)
evalTraceW tOk == evalTraceM tOk -- True

Each ...W evaluator should give exactly what its Part 6–8 version gives on tOk, tBad, and tNested.

Tests: runhaskell -itest test/Spec.hs --match "evalWith"

Now adding a new kind of evaluator means writing an instance and two small hooks. The recursion never changes.

Step 9.2: The Comparison Table

You'll fill in this table on Gradescope (see Turning It In):

Evaluator What extra information does it carry? Where is the bookkeeping in the manual version? Where does it live in the monadic version? Which evalWith argument does the extra work?
evalPlain / evalPlainM ... ... ... ...
evalSafe / evalSafeM ... ... ... ...
evalCount / evalCountM ... ... ... ...
evalTrace / evalTraceM ... ... ... ...

Step 9.3: Three Laws, Intuitively

You checked right identity for SafeResult. A monad must satisfy three laws:

Left identity. Wrapping an ordinary value with pure and then passing it to f is the same as just calling f:

pure x >>= f  =  f x

Right identity. Taking the ordinary result and re-wrapping it changes nothing:

m >>= pure  =  m

Associativity. Changing the grouping of a chain doesn't change its behavior. It does not mean you can reorder the computations:

(m >>= f) >>= g  =  m >>= (\x -> f x >>= g)

The tests check examples of all three laws (--match "Monad law"), but on Gradescope you'll show one check you did yourself.

Checkpoint 9: a law check

Choose one law other than right identity. Test it with concrete values for SafeResult or Traced, and explain why both sides agree. For Counted, run both sides from the same starting counter and compare: Counted wraps a function, so it has no Eq instance.

Step 9.4: What This All Shows

All four evaluators use the same two operations, pure and >>=, but each type's instance decides what "and then" means. Changing what an evaluator carries is a change to one instance, not to every recursive case. That's how Wadler makes changes to an evaluator modular.


Turning It In

Submit on Gradescope, under HW 07: Discovering Monads by Building Interpreters:

  1. From the top of the repo, run the full test suite one last time: runhaskell -itest test/Spec.hs > testing.log. Paste the entire contents of testing.log into the test-output text box, even if some tests still fail.
  2. Upload HW07.hs, the only file you edit.

Gradescope also asks a few short questions. You'll fill in the Part 9 comparison table, answer Checkpoints 3, 5, and 8, and show one monad-law check you ran in GHCi (Checkpoint 9). Your Checkpoint notes will come in handy here. The other Checkpoints aren't submitted.

The Homework Workflow covers the submission step in context. Make sure your final code is committed and pushed to your repo as well.

Final Self-Check

  • HW07.hs loads in GHCi, and nothing you were asked to write is still undefined.
  • All the by-hand evaluators (evalPlain, evalSafe, evalCount, evalTrace) are still there and still work.
  • evalSafeWithBind uses safeBind, not nested cases.
  • evalSafeM, evalCountM, and evalTraceM let >>= (or do) do the plumbing: no matching on Exception/Value, no counter variables, no ++.
  • evalPlainW, evalSafeW, evalCountW, and evalTraceW are each one call to evalWith.
  • all snd checks prints True.

Optional Bonuses

These are for fun and practice. They're not part of the required assignment: the tests don't check them, and you don't submit them on Gradescope.

Bonus 1: Errors and Counting Together

Every result type so far added one extra behavior. Real programs often want several. Try combining safe division and counting in a single type. One starting point:

newtype SafeCount a = SafeCount (Int -> (SafeResult a, Int))

Define a combined pure and bind (as named helpers or as full instances), and write evalSafeCount :: Term -> SafeCount Int.

We recommend counting successful divisions. When a division fails, keep the error, stop evaluating later steps, and keep the count of the divisions that succeeded before it. HW07.hs defines:

bonusExample :: Term
bonusExample = Div (Div (Num 12) (Num 3)) (Num 0)

The inner division succeeds and the outer one fails, so running from counter 0 should give something equivalent to (Exception "division by zero", 1).

Questions to think about:

  • Which pieces of your earlier code can you reuse?
  • What should bind do if the first computation returned an Exception?
  • Should a failed division count as an attempted division? In evalCountM, it didn't matter whether tick came before or after the children. Here, where you put the tick relative to the division decides which one you're counting. Why the difference?
  • You don't have to write fmap and <*> by hand. Once you have >>=, fmap = liftM and (<*>) = ap work for any monad. They come from Control.Monad, so add this line right under module HW07 where:

    import Control.Monad (ap, liftM)
    
    - Does your combined bind satisfy right identity and associativity?

This bonus points toward the broader problem of combining computational behaviors.

Bonus 2: Variables, Environments, and Simplifiers

The language so far has only numbers and division. If you want to keep going, here are some directions to try. Do as much or as little as you like, in a new file (say Bonus2.hs) so HW07.hs stays as you submitted it.

Grow the language. Make a new expression type with addition, subtraction, and multiplication alongside division. Then add variables, and a let form that gives a name to a value inside a smaller expression. To evaluate variables you'll need an environment, a record of which value each name has, and something to do when a name isn't in it. Your SafeResult monad is a natural fit for that. A list of pairs and the Prelude's lookup are enough to start. (If you import HW07, write import HW07 (SafeResult (..)) so your new constructors don't clash with Term's.)

Notice that almost every case of the new evaluator just passes the environment along unchanged. That's plumbing again. Could a monad hide it, the way Counted hid the counter?

Write simplifiers. A simplifier is a function that rewrites an expression into a simpler one, like turning x + 0 into x, x * 1 into x, x * 0 into 0, or 2 + 3 into 5. Try writing each rule as its own small function of type Expr -> Expr. Because they're just functions, you can combine them with .. Some things worth playing with:

  • Does the order you compose them in matter?
  • A rule that only looks at the top of an expression misses anything nested. How would you apply a rule everywhere in an expression?
  • When should you stop simplifying? Is one pass ever not enough?

Ask whether it's correct. A simplifier should never change what a program means. Evaluate a few expressions before and after simplifying and compare. Then think about 0 * (1 / 0), or 0 * y when y isn't defined. Is your simplifier still correct? What would you do about it?


Where to Go From Here

Now's a good time to read Wadler's Monads for Functional Programming. Section 2 will feel familiar. Section 3 states the monad laws properly. Section 5 builds a parser as a monad, which is the same idea behind the parser combinators you used in HW 6: a parser is a computation that carries "the rest of the input" and might fail. If Bonus 1 made you wonder how to combine several behaviors without writing a new monad by hand every time, look up monad transformers.