haskell - 没有 Cont Monad 的继续传递风格的评估
问题描述
我有一个任务:编写一个函数evalCPS
来评估由下面的 ADT 形式化的表达式,使用 Continuation Passing Style 但没有 Cont Monad 或类似的东西。
data Expr a = Expr a :+: Expr a
| Expr a :/: Expr a
| Num Int
| Var a
| Let a Int (Expr a)
我为前三个构造函数做了它,这并不难:
evalCPS :: Expr a -> (Int -> r) -> r
evalCPS (Num n) k = k n
evalCPS (e1 :+: e2) k =
evalCPS e1 $ \n1 ->
evalCPS e2 $ \n2 ->
k (n1 + n2)
evalCPS (e1 :/: e2) k =
evalCPS e2 $ \n2 ->
if n2 == 0 then error "division by zero" else evalCPS e1 $ \n1 ->
k (n1 `div` n2)
但现在我坚持使用Var
和Let
构造函数。我想我有点理解如何使用 monad 来做到这一点,因为我有绑定运算符,但我需要一个建议如何直接解决它,而不使用 monad。将非常感谢您的帮助!
解决方案
您需要为自己获取某种存储空间来存储通过Let
. 在一般解释器/编译器术语中,这种存储通常称为“环境”。让我们这样定义:
type Env a = ...
每当遇到 aLet
时,都需要向存储中添加一个变量。每当遇到 aVar
时,都需要在存储中查找变量。此外,整个计算应该从一个空存储开始。这意味着在存储上应该有三个操作:
emptyEnv :: Env a
lookupVar :: a -> Env a -> Int
insertVar :: (a, Int) -> Env a -> Env a
现在,您的evalCPS
函数需要接受一个Env
as 参数(否则它将如何查找变量?)。这将是应在其上下文中评估表达式的环境:
evalCPS :: Env a -> Expr a -> (Int -> r) -> r
该:+:
案例不需要查看环境,因此它应该将其通过隧道传递到递归evalCPS
调用:
evalCPS env (e1 :+: e2) k =
evalCPS env e1 $ \n1 ->
evalCPS env e2 $ \n2 ->
k (n1 + n2)
同样的:/:
情况。
该Var
案例将在环境中查找变量值并返回它(通过调用延续):
evalCPS env (Var a) k =
k $ lookupVar a env
很简单。
案例必须通过将Let
新变量添加到旧变量来构造一个新环境,然后在这个新环境的上下文中评估表达式:
evalCPS env (Let a value e) k =
let newEnv = insertVar (a, value) env
in evalCPS newEnv e k
还是很简单的。
现在,我们如何实现环境本身?lookupVar
、insertVar
和的主体应该是什么emptyEnv
样的?
从概念上讲,环境可以看作是一个函数:你给它一个变量标识符,它给你它的Int
值。根据这种理解,最简单的环境实现可能会失败:
type Env a = a -> Int
由此,定义lookupVar
:
lookupVar :: a -> Env a -> Int
lookupVar a env = env a
emptyEnv
有点棘手。让我们想一想:当程序试图引用一个尚未定义的变量时会发生什么?这个问题有很多可能的答案,但我会跟随你的脚步,像处理被零除一样处理这个错误情况:只需调用error
:
emptyEnv :: Env a
emptyEnv _ = error "Undefined variable"
insertVar
仍然更棘手。让我们再想一想:当我在现有环境中添加一个a
带值的变量时,结果应该是一个新环境,这样如果有人试图查找 variable ,结果应该是 value 。让我们把它写下来:v
e
a
v
insertVar :: Eq a => (a, Int) -> Env a -> Env a
insertVar (a, v) oldEnv =
\x -> if x == a then v else ???
否则呢?好吧,如果有人试图查找除 之外的任何变量,结果a
应该与oldEnv
给出的任何变量相同。让我们也写下来:
insertVar :: Eq a => (a, Int) -> Env a -> Env a
insertVar (a, v) oldEnv =
\x -> if x == a then v else oldEnv x
很简单。请注意,因为我使用的是比较运算符==
,所以我必须在类型签名中添加一个Eq a
约束。而且由于evalCPS
尝试insertVar
在某个时候调用,约束也会渗入evalCPS
:
evalCPS :: Eq a => Env a -> Expr a -> (Int -> r) -> r
这是合理的:如果我们希望能够在我们的变量存储中查找变量,就必须有某种方法来判断哪个变量是哪个。
当然,这种实现Env
有一个关键的缺点:它在每次查找时有效地执行线性搜索,当变量很多时性能很差。虽然这对于一个玩具练习来说是可以的,但对于任何严肃的编译器或解释器来说,这都行不通。
如果需要更好的性能,请随意尝试其他存储变量 -> 值映射的方法。例如,您可能希望将存储实现为Map
(提供对数查找时间):
type Env a = Map a Int
我将把 、 和 的实现emptyEnv
留作lookupVar
练习insertVar
。
推荐阅读
- html - CSS父背景颜色未填充子容器
- bash - 有没有办法接受以前的论点?
- python - Django/Python。在对象列表中转换的 QuerySet 数据转换元组中的 str/int 值
- java - 当我们说实例时,我们是什么意思?
- python - firebase=firebase(config) TypeError: 'module' object is not callable
- python - selenium 与 python 列出了一个优秀的列表
- ruby-on-rails - 如何在 Rails 6 中使用 flash 和错误处理程序?
- vbscript - 从 vbscript 子返回导致 ASP 页面上的错误
- spring - Spring security 5:为 OAuth2 认证用户提供角色
- javascript - Javascript 从数组中获取购物清单