Apply:过程应用
剖析元循环求值器中 apply 的工作原理,包括过程表示、原语过程以及环境操作的实现。
apply
apply 接受两个实参:一个过程,以及该过程所要应用的实参表。它把过程分为两类:对原语过程,调用 apply-primitive-procedure 应用;对复合过程,则按顺序求值构成过程体的各个表达式。复合过程体的求值环境是这样构造的:把过程所携带的基础环境扩展,纳入一个新框架,其中把过程的形参与该过程被应用时的实参绑定。下面是 apply 的定义:
(define (apply procedure arguments)
(cond ((primitive-procedure? procedure)
(apply-primitive-procedure procedure arguments))
((compound-procedure? procedure)
(eval-sequence
(procedure-body procedure)
(extend-environment
(procedure-parameters procedure)
arguments
(procedure-environment procedure))))
(else
(error
"Unknown procedure type -- APPLY" procedure))))
下面逐个讲解这个定义中用到的过程。
过程的表示
为了处理原语,我们假定已有以下过程可用:
-
(apply-primitive-procedure proc args)把给定的原语过程应用于表args中的各个实参值,返回应用的结果。 -
(primitive-procedure? proc)检验proc是否为原语过程。
复合过程用构造函数 make-procedure 从形参、过程体和环境构造:
(define (make-procedure parameters body env)
(list 'procedure parameters body env))
(define (compound-procedure? p)
(tagged-list? p 'procedure))
(define (procedure-parameters p) (cadr p))
(define (procedure-body p) (caddr p))
(define (procedure-environment p) (cadddr p))
原语过程
此刻,也许很长一段时间内,你可能会好奇原语过程在 Scheme 里是怎么表示的。其实原语过程的表示没有唯一正确的方式,只要 apply 能用 primitive-procedure? 和 apply-primitive-procedure 识别并应用它们即可。
Scheme 的创造者们决定把原语过程表示为一个表:以符号 primitive 开头,其中包含底层 Lisp 中实现该原语的过程。
(define (primitive-procedure? proc)
(tagged-list? proc 'primitive))
(define (primitive-implementation proc) (cadr proc))
(define primitive-procedures
(list (list 'car car)
(list 'cdr cdr)
(list 'cons cons)
(list 'null? null?)
<更多原语>
))
(define (primitive-procedure-names)
(map car
primitive-procedures))
(define (primitive-procedure-objects)
(map (lambda (proc) (list 'primitive (cadr proc)))
primitive-procedures))
应用原语过程时,我们只需用底层 Lisp 系统把实现过程应用于各个实参:
(define (apply-primitive-procedure proc args)
(apply-in-underlying-scheme
(primitive-implementation proc) args))
环境上的操作
当然,求值器需要操作环境的功能。再说一遍,什么是环境?它是框架的序列,每个框架是一张绑定表,把变量和对应的值关联起来。我们用以下操作来操作环境:
-
(lookup-variable-value <var> <env>)返回环境中与该符号绑定的值,如果变量未绑定则报错。 -
(extend-environment <variables> <values> <base-env>)返回一个新环境,它包含一个新框架,其中符号表中的各符号与值表中相应元素绑定,其外围环境为<base-env>。 -
(define-variable! <var> <value> <env>)在环境的第一个框架中新增一个绑定,把该变量与该值关联。 -
(set-variable-value! <var> <value> <env>)改变环境中该变量的绑定,使该变量现在绑定到这个值;如果变量未绑定则报错。
为了实现这些操作,我们用框架的表来表示环境。某个环境的外围环境就是这个表的 cdr。空环境就是空表。
(define (enclosing-environment env) (cdr env))
(define (first-frame env) (car env))
(define the-empty-environment '())
环境的每个框架表示为一对表:一个表列出该框架中绑定的变量,另一个表列出相关联的值。
(define (make-frame variables values)
(cons variables values))
(define (frame-variables frame) (car frame))
(define (frame-values frame) (cdr frame))
(define (add-binding-to-frame! var val frame)
(set-car! frame (cons var (car frame)))
(set-cdr! frame (cons val (cdr frame))))
要用一个新框架扩展环境,把变量与值关联起来:我们先构造一个由变量表和值表组成的框架,再把它拼接到环境前面。如果变量个数与值个数不匹配,我们报错。
(define (extend-environment vars vals base-env)
(if (= (length vars) (length vals))
(cons (make-frame vars vals) base-env)
(if (< (length vars) (length vals))
(error "Too many arguments supplied" vars vals)
(error "Too few arguments supplied" vars vals))))
在环境中查找变量时,我们扫描第一个框架中的变量表。如果找到目标变量,就返回值表中相应的元素。如果在当前框架中没找到,就接着搜索外围环境,依此类推。如果搜到空环境仍未找到,就报「未绑定变量」错误。
(define (lookup-variable-value var env)
(define (env-loop env)
(define (scan vars vals)
(cond ((null? vars)
(env-loop (enclosing-environment env)))
((eq? var (car vars))
(car vals))
(else (scan (cdr vars) (cdr vals)))))
(if (eq? env the-empty-environment)
(error "Unbound variable" var)
(let ((frame (first-frame env)))
(scan (frame-variables frame)
(frame-values frame)))))
(env-loop env))
要在指定环境中把变量改为新值,我们像 lookup-variable-value 一样扫描查找该变量,找到后修改相应的值。
(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-variable-value! 一样)。如果不存在这样的绑定,就在第一个框架中新增一个。
(define (define-variable! var val env)
(let ((frame (first-frame env)))
(define (scan vars vals)
(cond ((null? vars)
(add-binding-to-frame! var val frame))
((eq? var (car vars))
(set-car! vals val))
(else (scan (cdr vars) (cdr vals)))))
(scan (frame-variables frame)
(frame-values frame))))
再访 apply
我们再看一遍 apply 的定义。这次明白了吗?
(define (apply procedure arguments)
(cond ((primitive-procedure? procedure)
(apply-primitive-procedure procedure arguments))
((compound-procedure? procedure)
(eval-sequence
(procedure-body procedure)
(extend-environment
(procedure-parameters procedure)
arguments
(procedure-environment procedure))))
(else
(error
"Unknown procedure type -- APPLY" procedure))))
要点
在本小节中,你学到了:
apply是如何定义的- 原语过程是如何定义和应用的
- 环境操作是如何定义的
接下来?
我们将了解求值器是如何作为一个程序运行起来的。