I have been following and expanding on the tutorial Write Yourself A Scheme. I have a type LispVal wrapped up in a couple of layers of monad transformers:
import qualified Data.Map as M
data LispVal = ...
data LispError = ...
type Bindings = M.Map String (IORef LispVal)
data Env = Environment { parent :: Env, bindings :: IORef Bindings }
type IOThrowsError = ErrorT LispError IO
type EvalM = ReaderT Env IOThrowsError
The idea with using ReaderT is that I’ll be able to automatically pass the environment (which maintains variable bindings) around through the evaluator, and it will be obvious where it is used because there will be a call to ask. This seems preferable to explicitly passing the environment around as an extra parameter. When I come to implement continuations I’ll want to do a similar trick with the ContT monad transformer, and avoid passing around an extra argument for the continuation.
However, I haven’t figured out how it’s possible to modify the environment by doing this. For example, to define a new variable or to set the value of an old one.
For a concrete example, let’s say that whenever I evaluate an if statement, I want to bind the variable it to the result of the test clause. My first thought was to modify the environment directly:
evalIf :: [LispVal] -> EvalM LispVal
evalIf [test, consequent, alternate] = do
result <- eval test
bind "it" result
case (truthVal result) of
True -> eval consequent
False -> eval alternate
Here truthVal is a function that assigns a Bool to any LispVal. But I can’t figure out how to write the function bind so that it modifies the environment.
My second thought was to use local:
evalIf :: [LispVal] -> EvalM LispVal
evalIf [test, consequent, alternate] = do
result <- eval test
local (bind "it" result) $ case (truthVal result) of
True -> eval consequent
False -> eval alternate
But here bind needs to have type Env -> Env, and since I’m using IORefs as values in the environment, I can only write a function with the signature Env -> IO Env.
Is this even possible, or do I need to use StateT instead of ReaderT?
ReaderTis used when you have scoped, read-only environments. E.g. in interpreters.Environments in
ReaderTnest lexically, so you can augment the environment each time you encounter a binding. Like so:Here’s an old post of mine with some examples.
If you have such an environment, it shouldn’t be necessary to also use
IORefs, unless you’re playing funny games with dynamic scoping? What is the reason for the IORefs?