set!
您将如何在 Scheme 中实现自己的功能?函数是一种set!
破坏性过程,它会更改考虑到先前值而定义的值。
6 回答
如评论中所述,set!
是 Scheme 中的一个原语,必须由实现提供。同样,您无法=
在大多数编程语言中实现赋值运算符 。在 Common Lisp 中,setf
可以扩展(使用setf
-expanders)以允许(setf form value)
处理新类型的表单。
因为Schemeset!
只修改变量绑定(如Common Lisp 的setq
),所以仍然值得询问我们如何实现类似set-car!
和其他结构修饰符的函数。在某种意义上,这可以看作是对变量赋值的一种概括,但是因为词法变量(连同闭包)足以表示任意复杂的结构,所以它也可以看作是一种更特殊的情况。在 Scheme 中(除了像数组这样的内置原语),对象字段的变异是一种特殊化,因为对象可以通过词法闭包来实现,并根据以下方式实现set!
. 这是一个典型的练习,用于展示如何单独使用词法闭包来实现结构,例如 cons 单元。这是一个显示单值可变单元格实现的示例:
(define (make-cell value)
(lambda (op)
(case op
((update)
(lambda (new-value)
(set! value new-value)))
((retrieve)
(lambda ()
value)))))
(define (set-value! cell new-value)
((cell 'update) new-value))
(define (get-value cell)
((cell 'retrieve)))
给定这些定义,我们可以创建一个以 value 开头的单元格,将 value4
更新为8
使用 our set-value!
,然后检索新值:
(let ((c (make-cell 4)))
(set-value! c 8)
(get-value c))
=> 8
如前所述,set!
它是一个原语,不能作为一个过程来实现。要真正理解它是如何工作的,我建议你看一下 Lisp 解释器的内部工作原理。这是一个很好的开始:SICP 中的元循环评估器,特别是标题为“分配和定义”的部分。以下是与问题相关部分的摘录:
(define (eval exp env)
(cond ...
((assignment? exp) (eval-assignment exp env))
...
(else (error "Unknown expression type -- EVAL" exp))))
(define (assignment? exp)
(tagged-list? exp 'set!))
(define (eval-assignment exp env)
(set-variable-value! (assignment-variable exp)
(eval (assignment-value exp) env)
env)
'ok)
(define (set-variable-value! var val env)
(define (env-loop env)
(define (scan vars vals)
(cond ((null? vars)
(env-loop (enclosing-environment env)))
((eq? var (car vars))
(set-car! vals val))
(else (scan (cdr vars) (cdr vals)))))
(if (eq? env the-empty-environment)
(error "Unbound variable -- SET!" var)
(let ((frame (first-frame env)))
(scan (frame-variables frame)
(frame-values frame)))))
(env-loop env))
最后,set!
操作只是环境中绑定值的突变。因为这个级别的修改对于“正常”程序是禁止的,所以它必须作为一种特殊的形式来实现。
不能,不能,不能。大家都很消极!你绝对可以在 Racket 中做到这一点。您需要做的就是定义自己的“lambda”宏,它引入可变单元格代替所有参数,并为所有这些参数引入标识符宏,以便当它们用作常规 varrefs 时,它们可以正常工作。还有一套!防止这些标识符宏扩展的宏,以便它们可以被变异。
小菜一碟!
放!修改符号和位置之间的绑定(真的)。编译器和解释器会处理 set!不同。
解释器将有一个环境,它是符号和值之间的映射。严格规定!将符号第一次出现的值更改为指向第二个操作数的计算结果。在许多实现中,您无法设置!尚未绑定的东西。在 Scheme 中,预计变量已经绑定。阅读SICP或Lisp 小片段并使用示例将使您成为解释器的主要实现者。
在编译器情况下,您实际上并不需要符号表。您可以将评估的操作数保存在堆栈中并设置!需要更改堆栈位置指向的内容,或者如果它是闭包中的自由变量,则可以使用赋值转换。例如,它可以装箱到一个盒子或一个缺点。
(define (gen-counter start delta)
(lambda ()
(let ((cur start))
(set! start (+ cur delta))
cur)))
可以翻译成:
(define (gen-counter start delta)
(let ((start (cons start '()))
(lambda ()
(let ((cur (car start)))
(set-car! start (+ cur delta))
cur)))))
您可能想阅读高阶语言的控制流分析,其中此方法与大量有关编译器技术的信息一起使用。
如果您正在实现一个简单的解释器,那么这并不难。您的环境将从标识符映射到它们的值(或语法关键字到它们的转换器)。标识符-> 值映射需要考虑可能的值变化。像这样:
(define (make-cell value)
`(CELL ,value))
(define cell-value cadr)
(define (cell-value-set! cell value)
(set-car! (cdr cell) value))
...
((set-stmt? e)
(let ((name (cadr e))
(value (interpret (caddr e) env)))
(let ((cell (env-lookup name)))
(assert (cell? cell))
(cell-value-set! cell value)
'return-value-for-set!)))
...
另外两个更改是,当您将标识符绑定到值(例如在 alet
或lambda
应用程序中)时,您需要使用以下内容扩展环境:
(env-extend name (cell value) env)
并且在检索值时您需要使用cell-value
.
当然,只有可变标识符需要一个单元格,但是对于一个简单的解释器来说,为所有标识符值分配一个单元格就可以了。
在方案中有 2 个“全局”环境。
有一个环境可以存储最高定义的符号,还有另一个环境可以存储原始函数和表示系统参数的全局变量。
当你写类似的东西(set! VAR VAL)
时,解释器会寻找VAR
.
你不能使用设置!在未绑定的变量上。绑定是通过定义或通过 lambda 或由系统为原始运算符完成的。
绑定意味着在某个环境中分配一个位置。所以绑定函数有签名symbol -> address
。
回到set!
,解释器将在VAR
绑定的环境中查找。首先,它在本地环境(由 创建lambda
)中查找,然后在其本地父环境中查找,依此类推,然后在全局环境中,然后在系统环境中(按此顺序),直到找到包含 的绑定的环境框架VAR
。