如何绘制环境图
利用 EnvDraw 工具和上一课的规则,从零开始学习绘制环境图,覆盖 define、过程调用、set! 与 let。
EnvDraw
EnvDraw 是一个很酷的程序,你可以在课程账号上运行它来辅助绘制环境图。使用方法:
- 在终端输入
envdraw。(如果不行,先 SSH 到 torus,再输入该命令。)这个命令会打开一个 STk 解释器。 - 在解释器里输入
(envdraw)。 - 你会看到一个新的 EnvDraw 窗口打开。在
EnvDraw>提示符下,试着定义square函数。然后观察 EnvDraw 窗口,看看会发生什么!
概述
现在你要开始自己绘制环境图了!我们从基础讲起,然后逐步加深。绘制这些图所需的规则,全都在上一课里。等到本课结束,你必须全部掌握。
本课展示的所有示例环境图都取自 EnvDraw 程序。
定义
我们先在 Scheme 里定义一个变量,看看对应的环境图。具体来说,我们要画下面这行代码的图:
(define x 3)
第一步永远是画出全局环境。别忘了给它标注标签!接下来要弄清 define 怎么处理。看看上一课的规则,你应该已经知道:define 会向当前框架添加一个新绑定。画出来就是这样:
就这样!在全局环境里写下「x」,画一个箭头指向 3,齐活。(注意:在本课程中,你自己的环境图不必写「[其他绑定]」。EnvDraw 加上它是为了完整性。)
接下来我们定义过程。在 STk 解释器里输入下面的代码会发生什么?我们接着之前的图往下画。
(define (square x) (* x x))
第一步,把上面的代码改写成使用 lambda 的形式:
(define square (lambda (x) (* x x)))
注意,这个表达式现在拥有和 (define x 3) 完全一样的基本组成部分。所以我们遵循完全相同的步骤:在全局框架里写下「square」,画一个箭头指向这个 lambda。我们把 lambda 画成双气泡:第一个气泡指向参数和过程体,第二个气泡指向定义环境,也就是看到 lambda 时的当前环境。现在你的图应该变成这样:
现在完成了!回顾一下:先画 lambda,让第一个气泡指向参数和过程体,第二个气泡指向定义环境。接着在全局环境里写下「square」,让它指向这个 lambda。
下一课我们会讲如何真正调用刚定义的 square 函数。
这些例子忽略了一个非常重要的点:define 并不总是往全局环境里加东西。它加东西的地方是当前框架(只不过在上面这些例子里,当前框架恰好是全局环境)。至于如何判断当前框架是哪个,我们后面会讲。
应用基本过程
现在,我们来画下面这行代码的环境图:
(define y (+ 3 4))
它和上一个例子的区别在于:必须先把 + 过程应用到 3 和 4 上,才能给 y 赋值。你可以假设所有基本过程都由魔法完成应用,不需要为它们画任何东西。因此,完整的环境图就是这样的:
应用自定义过程
假设我们现在想真正调用之前定义的 square 函数。调用代码如下:
(square 5)
调用用户定义的过程,按以下步骤进行:
- 创建一个框架,把过程的形式参数绑定到实际参数的值上。
- 用这个新框架扩展过程的定义环境。
- 求值过程体,以新框架作为当前框架。
最后一步其实不涉及修改环境图。相反,到这一步我们才最终得出调用的值。为了求值 square 的过程体,我们先要求出 x 的值。变量总是使用第一个可用的绑定。这意味着我们查当前框架里 x -> 5 的绑定,而不是全局框架里 x -> 3 的绑定。求出 x 的值后,把它和自身相乘(记住,你可以直接假设这是魔法完成的)。现在我们完成了!5 自乘得到 25。
记住,只有复合过程调用才会创建新框架!
原子表达式
求值原子表达式(比如查符号的值)的关键,在于判断哪个框架是当前框架。在讲这个之前,先记住原子表达式的求值规则:
- 数字、字符串、
#t、#f自求值。 - 如果表达式是符号,找第一个可用的绑定。(也就是说:先查当前框架;查不到,就查当前框架「后面」的框架;如此继续,直到全局框架。)
难点全在规则 2 上。回顾一下上一课的环境图:
记住,新框架只在调用用户定义的过程时才会画出。因此,只有当你位于另一个函数的作用域内时,当前框架才可能不同于全局环境。这一点极其重要,但现在不必过于纠结。请确保你理解了目前的所有示例。本课后面我们会引入更复杂的例子。
检验理解
回想一下,我们目前的环境图是这样的:
如果我现在往解释器里输入 x,它的值会是多少?
现在我把 cube 函数这样(不恰当地)定义:
(define (cube x) (* y y y))
请先画出与这个定义对应的环境图,把它添加到我们已经画好的环境图上。
再画出求值下面代码后得到的环境图:
(cube 2)
它输出什么?
无所事事的框架
到现在,我们的环境图已经有三个框架:全局框架、E1 和 E2。E1 和 E2 分别是调用 square 和 cube 时创建的。然而,这些函数一返回(执行完),框架 E1 和 E2 就没用了!它们不再可达,其中的绑定也不再重要。
但并非总是如此。接下来几课我们会看一些代码,让这些框架在过程调用结束后很久仍然有用。
使用 set!
现在来看 set! 怎么处理。你可能记得,set! 改变第一个可用的绑定。记住,找第一个可用的绑定,是先查当前框架,再查它「后面」的框架。
检验理解
动手试试!从一张白纸开始,画出与下面几行代码对应的环境图:
(define x 3)
(define (change x n)
(set! x n))
(change x 5)
x 的值是多少?
怎样才能修正 change 过程,让全局环境里 x 的值发生变化?请指出所有可行的修正方法。
使用 let
let 常常让很多同学头疼。但别绝望!每当被 let 困住时,记住这几条简单规则:
- 把
let改写成lambda语句加一次调用。
例如,可以把
(let ((x 7)
(y 10))
(+ x y))
改写成
((lambda (x y) (+ x y)) 7 10)
- 画出对应的
lambda。记住,lambda就是一个双气泡,箭头要画对。 - 用合适的参数调用这个
lambda。记住,这包括画一个新框架,并把形式参数绑定到实际参数的值上。
只要能记住这几条简单规则,你就不会有任何麻烦!
检验理解
请画出下面代码的环境图:
(let ((x 7)
( y 10))
(+ x y))
现在来点更复杂的。画出求值下面代码后得到的环境图:
(define (make-withdraw initial-amount)
(let ((balance initial-amount))
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))))
(define W1 (make-withdraw 100))
(W1 50)
要点回顾
到这一步,绘制任何环境图所需的知识你已经全部掌握了,无论它多复杂!将来碰到问题时,别忘了回到基础!再复杂的代码,也能归结成几条简单的规则。
如果到目前为止有哪怕一处内容你不理解,就去求助!环境图正是很多同学觉得困难的专题之一。
接下来
利用新学到的求值环境模型知识,下一课我们将讲解如何巧妙地用 lambda 和 let 实现面向对象编程。
关于测验
笔记
参加第 8 课测验(Quiz 8)时,除了双面小抄之外,你还被允许携带一份环境图绘制规则。