将值绑定到环境模型中的框架

时间:2018-12-08 18:45:27

标签: scheme parameter-passing lisp sicp lexical-scope

我对评估的环境模型如何工作感到有些困惑,希望有人能解释一下。

SICP说:

  

环境模型指定:要将过程应用于参数,   创建一个包含绑定参数的框架的新环境   到参数的值。这个的封闭环境   frame是过程指定的环境。现在,在这个   新环境,请评估程序主体。

第一个示例:

如果我是

(define y 5)

在全球环境中,然后致电

(f y)

其中

(define (f x) (set! x 1))

我们构建了一个新环境(e1)。在e1内,x将绑定到y(5)的值。在体内,x的值现在为1。我发现y仍为5。我相信这样做的原因是因为x和y位于不同的帧中。也就是说,我完全替换了x的值。我修改了x绑定的框架,而不仅仅是其值。正确吗?

第二个示例:

如果我们在全球环境中生存:

(define (cons x y)
  (define (set-x! v) (set! x v))
  (define (set-y! v) (set! y v))
  (define (dispatch m)
    (cond ((eq? m 'car) x)
          ((eq? m 'cdr) y)
          ((eq? m 'set-car!) set-x!)
          ((eq? m 'set-cdr!) set-y!)
          (else (error "Undefined 
                 operation: CONS" m))))
  dispatch)

(define (set-car! z new-value)
  ((z 'set-car!) new-value)
  z)

现在我说:

(定义z2(cons 1 2))

假设z2在名为e2的环境中具有一个值,即调度过程,我调用:

(set-car! z2 3)

定车!创建一个新的环境e3。就像我的第一个示例一样,在e3中,参数z绑定到z2的值(e2中的调度过程)。执行主体后,z2现在为'(3 2)。我觉得定车!之所以能做到这一点,是因为我正在更改z(在全局中也被z2引用)所持有的对象的状态,而不是替换它。也就是说,我没有修改绑定z的框架。

在第二个示例中,似乎全局中的z2和e3中的z是共享的。我不确定我的第一个例子。根据环境模型中应用过程的规则,尽管x和y完全不可检测,因为5没有局部状态,但看起来x和y是共享的。

我说的一切正确吗?我误解了报价吗?

3 个答案:

答案 0 :(得分:2)

要回答第一个问题:假设您打算在第一个问题中写(f y)而不是(f 5),则y未被修改的原因是,球拍(像大多数语言一样)是“按价值致电”语言。即,将值传递给过程调用。在这种情况下,则在调用y之前,将参数5评估为f。更改x绑定不会影响y绑定。

要回答第二个问题:在第二个示例中,存在共享环境。也就是说,z是在环境(您称为e2)上关闭的函数。每次对z的调用都会创建一个新环境,该环境链接到现有的e2环境。在此环境中对xy进行突变会影响以后对e2环境的所有引用。

摘要:传递变量的值不同于传递包含该变量的闭包。如果我说

(f y)

...调用完成后,“ y”仍将引用相同的值[*]。如果我写

f (lambda (...) ... y ...)

(也就是说,传递一个引用了y的闭包,那么在调用f之后,y可能会绑定到另一个值。

如果您感到困惑,那么您并不孤单。关键是:不要停止使用闭包。相反,请停止使用变异。

[*]如果y是一个可变值,它可能会被突变,但仍然是“相同”值。请参阅上面有关混乱的注释。

答案 1 :(得分:1)

TL; DR: Scheme中的简单值是不可变的,当作为参数传递给函数时将被完整复制。复合值是可变的,作为指针的副本传递,而复制的指针与原始指针指向的内存位置相同。


您要解决的问题称为“变异”。像 5 这样的简单值是不可变的。此后在程序中没有set-int!可以更改5来保持值 42 。很好,没有。

但是变量的值是可变的。变量是函数调用框架中的绑定,可以使用set!进行更改。如果有

(define y 5)
(define (foo x) (set! x 42) (display (list x x)))
(foo 5)
   --> foo is entered
       foo invocation environment frame is created as { x : {int 5} }
       x's binding's value is changed: the frame is now { x : {int 42} }
       (42 42)    is displayed
       y still refers to 5 in the global environment

但是如果foo接收到一个本身持有可变引用的值,该值可以被更改,即“就地”更改,那么尽管foo的框架本身没有变化,但绑定到该值的值是指可以。

(define y (cons 5 6))     ; Scheme's standard cons
   --> a cons cell is created in memory, at {memory-address : 123}, as
                   {cons-cell {car : 5} {cdr : 6} } 
(define (foo x) (set-car! x 42) (display (list x x)))
(foo y)
   --> foo is entered
       foo invocation environment frame is created as 
             { x : {cons-cell-reference {memory-address : 123}} }
       x's binding's value is *mutated*: the frame is still
             { x : {cons-cell-reference {memory-address : 123}} }
           but the cons cell at {memory-address : 123} is now
                   {cons-cell {car : 42} {cdr : 6} } 
       ((42 . 6) (42 . 6))    is displayed
       y still refers to the same binding in the global environment
         which still refers to the same memory location, which has now 
         been altered in-place: at {memory-address : 123} is now
                   {cons-cell {car : 42} {cdr : 6} } 

在Scheme中,cons是一个原始变量,它创建可变的cons单元格,这些变量可以使用set-car!set-cdr!随地更改

这些SICP练习打算表明的是,没有必要将其作为原始的内置过程;即使不是Scheme中内置的,它也可以由用户实现。拥有set!就足够了。


另一个术语是说“装箱”值。如果我将 5 传递给某个函数,则当该函数返回时,我保证我的 5 仍然有效,因为它是通过复制其值来传递的,设置函数调用框架的绑定以引用值 5 的副本(当然,它也是整数 5 )。这就是所谓的“传递值”。

但是,如果我将其“装箱”并传递(list 5)到某个函数中,则复制的值(在 Lisp 中)是指向该“箱”的指针。这称为“传递指针值”或其他内容。

如果函数用(set-car! ... 42)更改了该框,则将其更改就位,此后我将在该框(list 42)中使用 42 -在同一内存下位置和以前一样。我的环境框架的绑定将保持不变-它仍将引用内存中的同一对象-但该值本身将被更改,就地更改,变异。

这是有效的,因为盒子是复合基准。无论我在其中放置简单值还是复合值,框本身(即可变的cons单元格)都不简单,因此将通过指针值传递-仅复制指针,而不复制其指向的内容。

答案 2 :(得分:1)

绑定到x的值的

y意味着x是一个新绑定,它接收与y包含的相同值的副本。 xy不是共享内存位置的别名。

尽管由于优化问题,绑定并不完全是内存位置,但是您可以用这种方式对它们的行为进行建模。也就是说,您可以将环境视为一袋用符号命名的存储位置。

实际上,

教育计划中的计划评估者使用关联列表表示环境。因此(let ((x 1) (y 2)) ...)创建了一个看起来像((y . 1) (x . 2))的环境。存储位置是此列表中cdr对的cons字段,它们的标签是car字段中的符号。细胞本身就是结合物。符号和位置通过相同的cons结构而被绑定在一起。

如果此let周围有外部环境,则可以使用缺点将这些关联对推入其中:

(let ((z 3))
  ;; env is now ((z . 3))
  (let ((x 1) (y 2))
     ;; env is now ((y . 2) (x . 1) (z . 3))

环境只是我们推送到的绑定的堆栈。当我们捕获一个词法闭包时,我们只需获取当前指针并将其存储到闭包对象中即可。

(let ((z 3))
  ;; env is now ((z . 3))
  (let ((x 1) (y 2))
     ;; env is now ((y . 2) (x . 1) (z . 3))
     (lambda (a) (+ x y z a))
     ;; lambda is an object with these three pices:
     ;; - the environment ((y . 2) (x . 1) (z . 3))
     ;; - the code (+ x y z a)
     ;; - the parameter list (a)
     )
  ;; after this let is done, the environment is again ((z . 3))
  ;; but the above closure maintains the captured one
)

因此,假设我们用一个参数10来调用lambda。lambda获取参数列表(a)并将其绑定到参数列表以创建一个新环境:

((a . 1))

这种新环境不是在真空中制造的;它被创建为捕获环境的扩展。所以,真的:

((a . 1) (y . 2) (x . 1) (z . 3))

现在,在这种有效的环境中,执行正文(+ x y z a)

您需要了解的所有有关环境的知识都可以参考此约束对模型。

分配给变量?这只是基于cons的绑定上的set-cdr!

什么是“扩展环境”?只是将基于cons的绑定推到了前面。

什么是变量的“新鲜绑定”?这就是使用(cons variable-symbol value)分配新单元格并通过推入来扩展环境。

什么是变量的“影子”?如果一个环境包含(... ((a . 2)) ...),并且我们将一个新的绑定(a . 3)推到该环境上,那么现在a是可见的,而(a . 2)是隐藏的,这仅仅是因为{{1 }}函数线性搜索并首先找到assoc(a . 2)完美地建模了内部到外部环境。内部绑定显示在外部绑定的左侧,更靠近列表的顶部,并且首先找到。

共享所有内容的语义来自这些单元格列表的语义。在assoc列表模型中,当两个环境assoc列表共享同一尾巴时,就会发生环境共享。例如,每次我们在上面调用lambda时,都会创建一个新的assoc自变量环境,但它会扩展相同的捕获环境尾部。如果lambda更改(a . whatever),则其他调用看不到,但是如果更改a,则其他调用将看到它。 x是lambda调用的私有对象,但axy在lambda的外部(在其捕获的环境中)。

如果您从心理上退回到这个assoc列表模型,那么就算不出环境的行为,包括任意复杂的情况,都不会出错。

真正的实现基本上就是围绕这一点进行优化。例如,从z之类的常量初始化并且从未分配过的变量根本不必作为实际环境条目存在;称为“恒定传播”的优化可以用42代替该变量的出现,就好像它是一个宏一样。实际的实现可能会针对环境级别使用哈希表或其他结构,而不是使用asso列表。可以编译实际的实现:可以根据各种策略(例如“闭包转换”)来编译词法环境。基本上,整个词法范围可以展平为单个类似矢量的对象。在运行时进行关闭时,将复制并初始化整个向量。编译后的代码不是引用可变符号,而是引用闭包向量中的偏移量,这要快得多:不需要通过assoc列表进行线性搜索。