This is the assignment for weeks 8-9, on Reader and State monads.
Jacobson's reader monad only allows for establishing a single binding relationship at a time. It requires considerable cleverness to deploy her combinators in a way that establishes multiple binding relationships, as in
John_x thinks Mary_y said he_x likes her_y.
See her 1999 paper for details. Essentially, she ends up layering several Reader monads over each other.
Here is code for the arithmetic tree Chris presented in week 8. It computes
\n. (+ 1 (* (/ 6 n) 4))
. Your task is to modify it to compute\n m. (+ 1 (* (/ 6 n) m))
. You will need to modify five lines. The first one is the type of a boxed int. Instead oftype num = int -> int
, you'll needtype num = int -> int -> int
The second and third are the definitions of
mid
andmap2
. The fourth is the one that encodes the variablen
, the line that begins(Leaf (Num (fun n -> ...
. The fifth line you need to modify is the one that replaces "4" with "m". When you have these lines modified, you should be able to execute the following expression:# match eval t2 with Leaf (Num f) -> f 2 4;; - : int = 13
Based on the evaluator code from assignment 7, and what you've learned about the Reader monad, enhance the arithmetic tree code to handle an arbitrary set of free variables. Don't use Juli8 libraries for this; just do it by hand. Return to the original code (that is, before the modifications required by the previous problem).
Start like this:
type env = string -> int type num = env -> int let my_env = fun var -> match var with "x" -> 2 | "y" -> 4 | _ -> 0;;
When you have it working, try
# match eval t2 with Leaf (Num f) -> f my_env;; - : int = 13
For this problem you don't need to demonstrate how to implement binding expressions like
let x = 3 in ...
. You just need to compute the value of possibly open expressions, relative to the suppliedenv
.Okay, now what changes do you need to make to add in expressions like
let x = 3 in ...
Add in the Option/Maybe monad. Start here:
type num = env -> int option
Show that your code handles division by zero gracefully.
Consider the following code which uses the Juli8 libraries for OCaml.
module S = Monad.State(struct type store = int end);; let xx = S.(mid 1 >>= fun x -> put 20 >> modify succ >> get >>= fun y -> mid [x;y]) in S.run xx 0
Recall that
xx >> yy
is short forxx >>= fun _ -> yy
. The equivalent Haskell code is:import Control.Monad.State let { xx :: State Int [Int]; xx = return 1 >>= \x -> put 20 >> modify succ >> get >>= \y -> return [x,y] } in runState xx 0
Or:
import Control.Monad.State let { xx :: State Int [Int]; xx = do { x <- return 1; put 20; modify succ; y <- get; return [x,y] } } in runState xx 0
Don't try running the code yet. Instead, get yourself into a position to predict what it will do, by reading the past few discussions about the State monad. After you've made a prediction, then run the code and see if you got it right.
Here's another one:
(* start with module S = ... as before *) let yy = S.(let xx = modify succ >> get in xx >>= fun x1 -> xx >>= fun x2 -> xx >>= fun x3 -> mid [x1;x2;x3]) in S.run yy 0
The equivalent Haskell code is:
import Control.Monad.State let { xx :: State Int Int; xx = modify succ >> get; yy = xx >>= \x1 -> xx >>= \x2 -> xx >>= \x3 -> return [x1,x2,x3] } in runState yy 0
What is your prediction? What did OCaml or Haskell actually evaluate this to?
Suppose you're trying to use the State monad to keep a running tally of how often certain arithmetic operations have been used in computing a complex expression. You've come upon the design plan of using the same State monad module
S
from the previous problems, and defining a function like this:let counting_plus xx yy = S.(tick >> map2 (+) xx yy)
How should you define the operation
tick
to make this work? The intended behavior is that after running:let zz = counting_plus (S.mid 1) (counting_plus (S.mid 2) (S.mid 3)) in S.run zz 0
you should get a payload of
6
(1+(2+3)
) and a finalstore
of2
(because+
was used twice).Instead of the design in the previous problem, suppose you had instead chosen to do things this way:
let counting_plus xx yy = S.(map2 (+) xx yy >>= tock)
How should you define the operation
tock
to make this work, with the same behavior as before?Here is how to create a monadic stack of a Reader monad transformer wrapped around an underlying Option monad:
module O = Monad.Option (* not really necessary *) module R = Monad.Reader(struct type env = (* whatever *) end) module RO = R.T(O) (* wrap R's Transformer around O *)
You can inspect the types that result by saying
#show RO.result
(in OCaml version >= 4.02), or by running:let env0 = (* some appropriate env, depending on how you defined R *) in let xx = RO.(mid 1) in RO.run xx env0
and inspecting the type of the result. In Haskell:
import Control.Monad.Reader -- substitute your own choices for the type Env and value env0 let { xx :: ReaderT Env Maybe Int; xx = return 1 } in runReaderT xx env0
Okay, here are some questions about various monad transformers. Use OCaml or Haskell to help you answer them. Which combined monad has the type of an optional list (that is, either
None
orSome [...]
): an Option transformer wrapped around an underlying List monad, or a List transformer wrapped around an underlying Option monad? Which combined monad has the type of a function fromstore
s to a pair('a list, store)
: a List transformer wrapped around an underlying State monad or a State transformer wrapped around an underlying List monad?
The last two problems are non-monadic.
This is a question about native mutation mechanisms in languages that have them, like OCaml or Scheme. What an expression like this:
let cell = ref 0 in let incr c = (let old = !cell in let () = cell := old + 1 in ()) in (incr cell, !cell, incr cell, incr cell)
will evaluate to will be
((), n, (), ())
for some numbern
between0
and3
. But what number is sensitive to the details of OCaml's evaluation strategy for evaluating tuple expressions. How can you avoid that dependence? That is, how can you rewrite such code to force it that the values in the 4-tuple have been evaluated left-to-right? Show us a strategy that works no matter what the expressions in the tuple are, not just these particular ones. (But you can assume that the expressions all terminate.)In the evaluator code for Week 7 homework, we left the
LetRec
portions unimplemented. How might we implement these for the second,env
-using interpreter? One strategy would be to interpret expressions like:letrec f = \x. BODY in TERM
as though they really read:
let f = FIX (\f x. BODY) in TERM
for some fixed-point combinator
FIX
. And that would work, supposing you use some fixed point combinator like the "primed" ones we showed you earlier that work with eager/call-by-value evaluation strategies. But for this problem, we want you to approach the task a different way.Begin by deleting all the
module VA = ...
code that implements the substitute-and-repeat interpreter. Next, change the type ofenv
to be an(identifier * bound) list
. Add a line after the definition of that type that saysand bound = Plain of result | Recursive of identifier * identifier * term * env
. The idea here is that some variables will be bound to ordinaryresult
s, and others will be bound to special structures we've made to keep track of the recursive definitions. These special structures are akin to theClosure of identifier * term * env
we already added to theterm
(or really more properlyresult
) datatype. ForClosure
s, the singleidentifier
is the bound variable, theterm
is the body of the lambda abstract, and theenv
is the environment that is in place when some variable is bound to this lambda abstract. Those same parameters make up the last three arguments of ourRecursive
structure. The first argument in theRecursive
structure is to hold the variable that ourletrec
construction binds to the lambda abstract. That is, in:letrec f = \x. BODY in TERM
both of the variables
f
andx
need to be interpreted specially when we evaluateBODY
, and this is how we keep track of which variable isf
.Just making those changes will require you to change some other parts of the interpreter to make it still work. Before trying to do anything further with
letrec
, try finding what parts of the code need to be changed to accommodate these modifications to our types. See if you can get the interpreter working again as well as it was before.OK, once you've done that, then add an extra line:
| LetRec of identifier * term * term
to the definition of the
term
datatype. (Forletrec IDENT1 = TERM1 in TERM2
. You can assume thatTERM1
is always aLambda
term.) Now what will you need to add to theeval
function to get it to interpret these terms properly? This will take some thought, and a good understanding of how the other clauses in theeval
function are working.Here's a conceptual question: why did we point you in the direction of complicating the type that environments associate variables with, rather than just adding a new clause to the
result
type, as we did with Closures?