SICP 的箭头表示法有点过载。我将引用文本的相关部分来理解该图。
过程对象是一对,其代码指定过程有一个形式参数,即 x 和过程体 (* x x)。该过程的环境部分是指向全局环境的指针,因为这是计算 lambda 表达式以生成该过程的环境。将过程对象与符号方块关联起来的新绑定已添加到全局框架中。一般来说,define 通过向框架添加绑定来创建定义。
那么,让我们分析一下每个箭头。
“全局环境”→ 正方形。这个箭头看起来只是labeling广场作为地球环境的象征。值得注意的是,此环境是自此以来唯一存活的堆栈帧define
在全局环境中被称为。
“正方形”→ 两个点。这个箭头似乎表明这两个点代表的是stored在名字上"square"
这是在全球环境中发现的。
左点→“参数”/“主体”。该箭头表示左边的点是一个“对象”,被认为存储着两个数据:“形式参数列表”和“过程主体”。
右点 → 正方形。这个箭头表明右边的点包含一个返回全局环境的“指针”。
这张图给出了关于 Lisp 中符号如何派生含义的高度可操作的观点。特别是,符号在特定的“上下文”中被“评估”。上下文是“环境框架”的链接列表,每个环境框架都包含一些名称→值映射的集合。为了评估符号,需要遵循该链接列表并返回从符号名称映射的第一个值。直观地看,一个例子是
"foo" → { "bar" : 3 → { "foo" : 8 } → { "foo" : 10 }
, "baz" : 4 }
在哪里评估foo
回报8
通过“跳过”第一帧并找到值8
在第二帧中ignoring第三帧。这种忽略功能很重要——它表明某些上下文可能具有隐藏来自更大上下文的值的名称。
所以这里的整个图片表明以下内容:
- Calling
define
在全局上下文中添加一个新的名称→值映射到全局框架。
-
存储 lambda 对象存储两条信息(两个点)
最后,我们应该谈谈计算 lambda 的含义。要计算 lambda,您必须向其传递一个列表values。它使用该输入值列表并将它们与它存储的形式参数列表进行匹配,以便生成一个新的环境框架它将形式参数映射到输入值。然后,它使用该新框架作为主框架和linked框架作为后续上下文。用图表来说,我们可以说square
看起来像
+--- Formal parameter list
/ +--- Body of function
| |
(left: (x) (* x x)) (right: {global frame})
然后当我们评估它时(square 3)
我们使用创建一个新框架3
和形式参数列表
{ "x" : 3 }
并评估身体。首先我们查一下名字*
。由于它不在我们新的本地框架中,我们必须在全局框架中找到它。
"*" → { "x" : 3 } → { global frame }
事实证明它存在,并且是乘法的定义。因此,我们需要向它传递一些值,以便我们查找“x”
"x" → { "x" : 3 } → { global frame }
since x
is存储在本地框架中,我们在那里找到它并通过3
and 3
作为我们找到的乘法函数的参数。
重要的部分是局部框架shadows全球框架。这意味着如果x
在全球框架中也有意义,我们会override它在评估身体的背景下square
.
最后,当我被要求在有关“变量”含义的问题的背景下回答这个问题时,需要注意的是,以上是一个非常特殊的问题执行变量的非常特殊的语义。从表面上看,你总是可以说“lisp 中的变量恰好意味着这个过程的发生”。不过,这可能有点具有挑战性。
“变量”一词的另一种语义(我和许多数学爱好者都喜欢)是上下文中的变量代表域中特定的、固定的但未知的值。如果我们检查lambda在体内square
(lambda (x) (* x x))
我们看到这或多或少是这个短语的预期语义——在解释时(* x x)
we see x
作为某种价值(例如数字),但我们对此一无所知。在口译中(lambda (x) (* x x))
我们看到,为了理解 lambda 内部短语的含义,我们必须为其提供以下含义:x
。这大致是到处使用的变量和函数的标准语义。
挑战在于这里描述的堆栈帧实现也可以轻松设置violate这种语义——事实上,在这个例子中它的作用非常巧妙。具体来说:define
破坏语义。原因在下面的代码片段中很明显
(define foo 3)
foo
(define foo 4)
foo
在此片段中,我们按顺序评估每个短语,并看到变量的(据称“固定但未知”)值foo
从第 2 行更改为第 4 行。这是因为define
让我们能够edit堆栈帧位于上下文中,而不是仅仅创建一个新的上下文来遮蔽旧的上下文,例如lambda
做。这意味着我们必须将变量视为不是“固定但未知”,而是一系列可变槽,不能保证随着时间的推移保持其值——这是一种更复杂的语义,也许应该迫使我们调用foo
“插槽”或“可分配”。
我们也可以将其视为有漏洞的抽象。我们希望变量具有标准的“固定但未知”语义,但由于堆栈帧的机制和define
我们并不完全遵循这个含义。
最后一点,Lisps 经常给你一个叫做let
它可用于复制前面的示例,而不会丢弃变量语义:
(let ((foo 3))
foo
(let ((foo 4))
foo)
foo)
在这种情况下,foo
第 2 行取值3
, the foo
4号线存在于不同的变量上下文中因此只有shadows the foo
在第 2 行...因此采用不同的固定值4
,最后foo
第 5 行再次与foo
在第 2 行并取相同的值。
换句话说,let
允许我们创建任意本地上下文(巧合的是,正如您所期望的那样,通过在幕后创建新的堆栈帧)。不幸的是,让我们知道这些语义是安全的黄金法则被称为 α 转换。该规则规定,如果您重命名变量到处 and 均匀地在单一上下文中,程序的含义不会改变。
因此,通过 α 转换,前面的例子与这个例子的含义相同
(let ((foo 3))
foo
(let ((bar 4))
bar)
foo)
也许稍微不那么混乱,因为我们不再需要担心阴影的影响foo
.
那么我们可以制作 Lisp 的吗define
语义更安全?有点儿。您可能会想象以下转变:
- 禁止定义集中的循环依赖,例如
(define x y) (define y x)
不允许同时(define x 3) (define y x)
isn't.
- 全部移动
define
直到任何给定上下文(堆栈帧)的最开头,并将它们按依赖顺序放置。
- 将“重新”设置为错误
define
“任何变量
事实证明,这种转换有点棘手(代码移动很困难,因此可能会出现循环依赖),但如果您解决了一些小问题,您会发现在任何上下文中,变量只能接受一个固定但未知的值价值。
您还可以找到以下内容——具有以下转换形式的任何程序
(define x ... definition of x ...)
(define y ... definition of y ...)
(define z ... definition of z ...)
... body ...
相当于下面的
(let ((x ... definition of x ...))
(let ((y ... definition of y ...))
(let ((z ... definition of z ...))
... body ...)))
这是表明我们美好、简单的“变量是固定但未知数量”语义的另一种方式。