我一直在关注并扩展教程Write Yourself A Scheme。我有一种LispVal
包裹在几层单子变压器中的类型:
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
using 的想法ReaderT
是,我将能够通过评估器自动传递环境(它维护变量绑定),并且它的使用位置很明显,因为将调用ask
. 这似乎比将环境作为额外参数显式传递更可取。当我开始实现延续时,我会想用ContT
monad 转换器做一个类似的技巧,并避免为延续传递额外的参数。
但是,我还没有弄清楚如何通过这样做来修改环境。例如,定义一个新变量或设置一个旧变量的值。
举一个具体的例子,假设每当我评估一个 if 语句时,我想将变量绑定it
到 test 子句的结果。我的第一个想法是直接修改环境:
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
这truthVal
是一个将 a 分配Bool
给 any的函数LispVal
。但我不知道如何编写函数bind
以修改环境。
我的第二个想法是使用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
但是这里bind
需要有 type Env -> Env
,而且由于我IORef
在环境中使用 s 作为值,所以我只能写一个带有签名的函数Env -> IO Env
。
这甚至可能吗,还是我需要使用StateT
而不是ReaderT
?