HW 7: Discovering Monads by Building Interpreters¶
Here's the question for this assignment: what happens when you take a simple evaluator and ask it to do just one more thing, like report errors, count operations, or keep a log of what it did? Why does so much of the code suddenly turn into bookkeeping?
You'll write four versions of one tiny evaluator, each doing one extra job, and then rewrite all four in monadic style. Along the way, the same pattern shows up in three problems that look completely different. The language has only integers and division, so there's nothing to distract from how the evaluator is built.
Like Lab 7, this assignment is a staircase. This time the steps are evaluators instead of small functions, and the payoff is the same: by the end, >>= should feel like something you invented to get rid of repeated code, not something handed to you.
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 60 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, andMonadinstances for your own types. - Rewrite the same evaluator using
pure,>>=, anddonotation. - 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:
safeDivandevalSafe(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, andevalSafeWithBind - Part 6:
SafeResultinstances,evalSafeM, andevalPlainM - Part 7:
Countedinstances andevalCountM - Part 8:
Tracedinstances andevalTraceM - Part 9: 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.
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 are thinking prompts
Each Part ends with a Checkpoint question. You don't submit these one by one. They're there to make you stop and compare what the evaluator means with the extra work its result type demands. Jot a sentence or two in a scratch file as you go, because you'll use those notes when you answer a few questions on Gradescope at the end (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).
The starter file 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.
Part 1: The Plain Evaluator, evalPlain¶
Start with the simplest possible interpretation:
evalPlain :: Term -> Int
There are exactly two cases:
- A number evaluates to that number.
- 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 tNested -- 2
Tests: runhaskell -itest test/Spec.hs --match "Part 1"
What about evalPlain tBad? It raises a Haskell divide-by-zero exception, and for now that's expected. This evaluator has type Term -> Int, and no Int means "evaluation failed."
Look for the algorithm we'll preserve
Focus on the recursive case: evaluate left, evaluate right, combine. This short pattern will stay recognizable even after the result types get much more complicated.
Checkpoint 1
In one or two sentences, describe the essential work of evaluating Div t u. Don't mention monads yet.
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:
- Call
evalSafe t. - If it fails, return the same
Exceptionimmediately. - If it succeeds, call
evalSafe u. - If that fails, return its
Exceptionimmediately. - If both succeeded, call
safeDivon 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 (the starter 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"
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¶
Forget errors for a moment. This time, the evaluator should keep 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
{ runCounted :: Int -> (a, Int) }
Read Counted a as "a computation that produces an a while carrying an integer counter." The newtype gives it a type of its own, so later we can make it an instance of Monad. (A plain type synonym isn't a distinct type.)
A computation that produces 7 without changing the counter is Counted (\count -> (7, count)). You run one by supplying the starting counter:
runCounted (Counted (\count -> (7, count))) 0 -- (7,0)
The starter also provides runFromZero computation = runCounted computation 0.
Step 3.1: Thread the Counter by Hand¶
Implement:
evalCount :: Term -> Counted Int
For Num n, return n without incrementing the counter. For Div t u, the sequence must be:
starting count (count0)
|
v
run the left child
| returns left value AND count1
v
run the right child, starting at count1
| returns right value AND count2
v
calculate the division
|
v
return result AND count2 + 1
The key rule: the right child gets the counter produced by the left child, not the original one. The starter 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 tNested) -- (2,2)
tOk has two Div nodes, so the counter ends at 2. As with evalPlain, division by zero still crashes. This version is only about counting.
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?
Part 4: Recording a Trace, evalTrace¶
Now forget counting. This time, the evaluator should keep a written record of its work. Instead of printing 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). The starter provides a helper:
traceLine :: Term -> Int -> String
traceLine term n =
"eval(" ++ show term ++ ") = " ++ show n ++ "\n"
Step 4.1: Concatenate the Log by Hand¶
Implement:
evalTrace :: Term -> Traced Int
For Num n, return n and a line saying it was evaluated. For Div t u:
- Evaluate the left child, giving a value and some log text.
- Evaluate the right child, giving another value and more log text.
- Compute the quotient.
- Concatenate the left trace, the right trace, and a line for this division, in that order.
- Return that trace and the quotient together.
The starter gives you a let skeleton for this case too.
Why does order matter? String concatenation isn't commutative. Swapping left and right changes the trace even when the final number stays the same.
✅ Check your work (Part 4)
evalTrace (Num 7)
-- Traced "eval(Num 7) = 7\n" 7
traceSummary (evalTrace tOk)
-- (43,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. This version still crashes on division by zero too, because we're adding one extra behavior at a time.
Tests: runhaskell -itest test/Spec.hs --match "Part 4"
Checkpoint 4
Compare evalPlain, evalSafe, evalCount, and evalTrace. Find the common "evaluate left, evaluate right, divide" structure. For each of the last three, write down the extra plumbing that makes its recursive case more complicated.
Stop and compare before continuing
This is the most important observation in the assignment, so don't rush past it.
The essential recursive algorithm has barely changed. But depending on the result type, the code has turned into a nest of error checks, a chain of hand-threaded counters, or a tangle of log concatenations.
What we want is to write the common computation once, and put each kind of extra behavior in a reusable operation.
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.
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
The type is worth reading slowly:
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 withx.
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
Compare the result to evalSafe. The evaluator is about what the language means again, and safeBind owns the failure handling.
✅ 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"
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, and this homework doesn't turn into an Applicative tutorial.
Step 6.1: Three Instances for SafeResult¶
Complete the Functor, Applicative, and Monad instances in the starter:
Functor: the same idea asfmaponMaybe. Transform the value inValue x, and leave anExceptionunchanged.Applicative:purebehaves likesafePure.Exception msg <*> anythingkeeps that error.Value f <*> resultappliesfto the value insideresult, if there is one. You can usefmapfor this.Monad: reuse exactly the two cases you discovered insafeBind. 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.
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.
Value 7 >>= \x -> Value (x + 1) -- Value 8
Exception "oops" >>= \x -> Value (x + 1) -- Exception "oops"
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. The starter provides it, instances included:
newtype Plain a = Plain { unPlain :: a }
deriving (Show, Eq)
-- its bind just passes the value along:
-- Plain x >>= next = next x
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"
unPlain (evalPlainM tOk) -- 43
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: Finish the Instances for Counted¶
To keep the focus on sequencing, the starter provides fmap and <*>. You implement pure (in the Applicative instance) and >>= (in the Monad instance). Your bind should:
- Run
firston the incoming counter. - Get back an ordinary result
aand the updated counter. - Call
next ato get the next counted computation. - Run that computation on the updated counter.
- Return its result and final counter.
The key point: bind handles passing the counter from one computation to the next. The evaluator no longer needs to name count0, count1, and count2.
Step 7.2: One Useful Counted Operation¶
The starter 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 ().
Step 7.3: Rewrite the Evaluator¶
Implement:
evalCountM :: Term -> Counted Int
using do notation, pure for the ordinary result, and tick to count the division:
a <- evaluate left
b <- evaluate right
tick
pure the quotient
✅ Check your work (Part 7)
runFromZero (evalCount tOk) -- (43,2)
runFromZero (evalCountM tOk) -- (43,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? Where did the explicit counter plumbing 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 starter provides fmap and <*>. You implement pure and >>=:
pure produces the value with an empty log. Your bind should:
- Start with the first traced result, which has text
text1and valuex. - Call the next function with
x. - Get back text
text2and the next value. - Return the combined trace
text1 ++ text2with the next value.
It's the same type pattern as error handling and counting, but the bookkeeping is different.
Step 8.2: One Useful Traced Operation¶
The starter provides:
record :: String -> Traced ()
record message = Traced message ()
record is a small computation whose only job is to add a message to the trace.
Step 8.3: Rewrite the Evaluator¶
Implement:
evalTraceM :: Term -> Traced Int
For Num n, record a line for the number and return n. For Div t u, use do notation:
a <- evaluate left
b <- evaluate right
let result = a `div` b
record a line about this division
pure result
✅ Check your work (Part 8)
evalTraceM (Num 7) -- Traced "eval(Num 7) = 7\n" 7
traceSummary (evalTraceM tOk) -- (43,5)
evalTraceM tOk == evalTrace tOk -- True
Tests: runhaskell -itest test/Spec.hs --match "Part 8"
Checkpoint 8
Where did text1 ++ text2 ++ ... go? If you reversed the concatenation in your bind, which part of the evaluator's visible behavior would change?
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
This is the point of the homework. Your original evaluators had to spell out how to carry the extra information through every recursive call. In the monadic versions, the Monad instance does that repeated plumbing, and the evaluator can focus on the essential sequence again: evaluate left, then evaluate right, then divide.
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? |
|---|---|---|---|
evalPlain / evalPlainM |
... | ... | ... |
evalSafe / evalSafeM |
... | ... | ... |
evalCount / evalCountM |
... | ... | ... |
evalTrace / evalTraceM |
... | ... | ... |
Step 9.1: 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.2: What This All Shows¶
The common interface is just two operations:
pure :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
But pure and >>= do different things for different ms:
SafeResult: keep errors, and skip later steps after a failure.Counted: carry the updated counter into the next step.Traced: accumulate log text, in order.
Monads don't automatically mean mutable state, exceptions, or logging. Each monad's instance defines how its computations are connected. That's the abstraction Wadler uses to make changes to an evaluator modular.
Optional Bonus: 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
{ runSafeCount :: 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. The starter 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? What would you change if that were the spec?
- Does your combined bind satisfy right identity and associativity?
This is a stretch goal, not part of the required assignment, and the test suite doesn't cover it. It points toward the broader problem of combining computational behaviors.
Turning It In¶
Submit on Gradescope, under HW 07: Discovering Monads by Building Interpreters:
- 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 oftesting.loginto the test-output text box, even if some tests still fail. - Upload
HW07.hs, the only file you edit.
Gradescope also asks a few short questions. You'll fill in the Part 9 comparison table, write a short reflection on the by-hand versions vs. the monadic ones, show one monad-law check you ran in GHCi, and paste a few GHCi results. Your Checkpoint notes will come in handy here. You don't need to answer the nine Checkpoints separately.
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.hsloads in GHCi, and nothing you were asked to write is stillundefined. - All the by-hand evaluators (
evalPlain,evalSafe,evalCount,evalTrace) are still there and still work. -
evalSafeWithBindusessafeBind, not nestedcases. -
evalSafeM,evalCountM, andevalTraceMlet>>=(ordo) do the plumbing: no matching onException/Value, no counter variables, no++. -
all snd checksprintsTrue.
Where to Go From Here¶
The most important takeaway: we started with an evaluator whose real algorithm was a few lines long. Adding useful features buried it under repeated bookkeeping. By discovering pure and >>=, we made that bookkeeping reusable and got the simple evaluator back.
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 the bonus made you wonder how to combine several behaviors without writing a new monad by hand every time, look up monad transformers.