过程和变量绑定
过程和变量绑定是Scheme程序的基本构建快。本章描述了一小部分的语法形式,它们的主要目的是创建过程和操作变量绑定。它从两个最基本的Scheme程序构建块开始:变量引用和lambda表达式,以及变量绑定和赋值的描述,如define、letrec、let值和set!。
你能在第五章找到其他各种形式的变量绑定或赋值的语法,因为它们的其主要目的不是绑定或赋值,如let。
4.1 变量引用
语法: 变量
返回: 变量的值
如果存在标识符绑定了一个可视变量,那么它在程序中作为表达式出现时都将视为变量,例如,标识符出现在由define、lambda、let或其他变量绑定结构的作用域中。
list => #\<procedure>
(define x 'a)
(let ([x 'b])
(list x x)) => (b b)
(let ([let 'let]) let) => let
如果标识符出现在library形式或者顶级程序中时,没有绑定为变量、关键字、record名或其他实体,那么会造成语法冲突。
因为一个库、顶级程序、lambda或者局部内容的定义域是一个整体,所以只要一个引用在定义完成前不立即求值,那么变量的定义可以出现在引用第一次出现之后。因此,举个例子,下面对f的定义中,引用g是允许的。
(define f
(lambda (x)
(g x)))
(define g
(lambda (x)
(+ x x)))
但是,下面的定义q中g的引用是错误的。
(define q (g 3))
(define g
(lambda (x)
(+ x x)))
4.2 Lambda
语法: (lambda formals body1 body2)
返回: 一个过程
库: (rnrs base),(rnrs)
lambda语法形式用于创建过程。任何创建过程或建立局部变量绑定的操作最终都是依照lambda或case-lambda定义的。
在formals中的变量是过程的形式化参数,而子序列body1、body2……是它的主体。
主体可以从一系列定义开始,在这种情况下,由定义创建的绑定是属于主体局部的。如果定义存在,则关键字绑定将在在主体展开时使用和丢弃,主体将被展开成由变量定义和其余表达式组成的 letrec*表达式,如第八章所述。lambda描述的其余部分假定了这个转换已经发生(如果需要的话),因此主体是一系列没有定义的表达式。
在创建过程时,除形式参数外,在主体中自由出现的所有变量的绑定将与过程一起保留。随后,每当将过程应用到一个实际参数序列上时,将形式参数绑定到世纪参数上,恢复保留的绑定,并计算主体。
应用时,由formals定义的形式参数与实际参数绑定如下:
- 如果
formals是一个标准变量列表,例如(x y z),则每个变量都绑定到相应的实际参数。如果提供的实际参数太少或太多,就会引发条件类型&assertion的异常。 - 如果
formals是单个变量(不在列表中),例如z,它就绑定到实际参数的列表。 - 如果
formals是一个以变量结尾的不标准变量列表,例如(x y),除最后一个变量外,每个变量都绑定相应的实际参数。最后一个变量绑定到剩下的实际参数列表。如果提供的实际参数太少,则会引发条件类型&assertion的异常。
计算主体时,主体中的表达式按顺序计算,过程返回最后一个表达式的值。
通常情况下,过程没有一个可打印的输出形式。Scheme系统以不同方式打印程序;本书使用符号#\
(lambda (x) (+ x 3)) => #\<procedure>
((lambda (x) (+ x 3)) 7) => 10
((lambda (x y) (* x (+ x y))) 7 13) => 140
((lambda (f x) (f x x)) + 11) => 22
((lambda () (+ 3 4))) => 7
((lambda (x . y) (list x y))
28 37) => (28 (37))
((lambda (x . y)(list x y))
28 37 47 28) => (28 (37 47 28))
((lambda (x y . z) (list x y z))
1 2 3 4) => (1 2 (3 4))
((lambda x x) 7 13) => (7 13)
4.3 Case-Lambda
Scheme lambda表达式总是生成一个具有固定数量参数的过程,或者具有大于或等于某个数的不定数量参数的过程。
特别是,
(lambda (var1 ... varn) body1 body2 ...)
恰好接受n个参数,
(lambda r body1 body2 ...)
接受零个或多个参数,并且
(lambda (var1 ... varn . r) body1 body2 ...)
接受n个或多个参数。
然而,lambda不能直接生成一个接受两个或三个参数的过程。特别是,lambda不直接支持接受可选参数的过程。上面所示的lambda范例的后一种形式可以与参数长度检测,以及car和cdr的组合一起使用,来实现带有可选参数的过程,尽管这是以清晰和效率为代价的。
case-lambda语法形式直接支持带有可选参数的过程,以及带有固定或不定数量参数的过程。case-lambda是基于文章"A New Approach to Procedures with Variable Arity"中引入的lambda*语法形式。
语法: (case-lambda clause ...)
返回: 一个过程
库: (rnrs control),(rnrs)
case-lambda表达式由一组子句组成,每个子句都类似于lambda表达式。每个子句的形式如下。
[formals body1 body2 ...]
子句的形式参数由formals定义,方式与lambda表达式相同。case-lambda表达式的过程值接受的参数数量由各个子句接受的参数数量决定。
当由case-lambda创建的过程被调用时,这些子句将按顺序判断。第一个接受给定数量的实际参数的子句将被选择,将其formals定义的形式参数绑定到相应的实际参数,并按照上面lambda的描述对主体进行计算。如果子句中的formals是一个正确的标识符列表,那么子句接受的实际参数与formals中的形式参数(标识符)一样多。如同lambda formals,case-lambda子句的formals可能是一个标识符,在这种情况下,子句接受任意数量的参数,或者formals是一个由一个标识符终止的不恰当列表,在这种情况下,子句接受数量大于或等于形式参数的任意数量参数(不包括终止标识符)。如果没有子句符合提供的实际参数的数量,则会引发条件类型&assertion的异常。
以下make-list的定义使用case-lambda支持可选的填充参数。
(define make-list
(case-lambda
[(n) (make-list n #f)]
[(n x)
(do ([n n (- n 1)] [ls '() (cons x ls)])
((zero? n) ls))]))
过程substring可以用case-lambda进行扩展,以接受无end索引(在这种情况下,它默认接受字符串的结尾)或无start和end索引(在这种情况下,substring等价于string-copy):
(define substring1
(case-lambda
[(s) (substring1 s 0 (string-length s))]
[(s start) (substring1 s start (string-length s))]
[(s start end) (substring s start end)]))
当只提供一个索引时,也可以默认start索引而不是end索引:
(define substring2
(case-lambda
[(s) (substring2 s 0 (string-length s))]
[(s end) (substring2 s 0 end)]
[(s start end) (substring s start end)]))
甚至可以仅通过省略中间子句,要求同时提供或不提供start和end索引:
(define substring3
(case-lambda
[(s) (substring3 s 0 (string-length s))]
[(s start end) (substring s start end)]))
4.4 局部绑定
语法: (let ((var expr) ...) body1 body2 ...)
返回: 主体最终表达式的值
库: (rnrs base), (rnrs)
let建立局部变量绑定。每个变量var都与相应表达式expr的值绑定。let的主体(变量都已绑定),是由body1 body2 ...组成的子表单序列,并按照lambda主体的方式处理和计算。
形式let、let*、letrec和letrec*(这些形式在let之后进行描述)是类似,但用途略有不同。与let*、letrec、letrec*相比,let中的表达式expr ...都是在变量var ...的范围以外。同时,同let*和letrec*相比,表达式expr ...的求值顺序是隐式的。根据实现的判断,它们可以从左到右、从右到左或者任意其它顺序。当值独立于变量且求值顺序不重要时,使用let。
(let ([x (* 3.0 3.0)] [y (* 4.0 4.0)])
(sqrt (+ x y))) => 5.0
(let ([x 'a] [y '(b c)])
(cons x y)) => (a b c)
(let ([x 0] [y 1])
(let ([x y] [y x])
(list x y))) => (1 0)
下面的let定义展示了let是lambda的典型派生。
(define-syntax let
(syntax-rules ()
[(_ ((x e) ...) b1 b2 ...)
((lambda (x ...) b1 b2 ...) e ...)]))
另一种形式的let,在5.4节中进行了描述,完整的let的定义可以在第八章中找到。
语法: (let* ((var expr) ...) body1 body2 ...)
返回: 主体最终表达式的值
库: (rnrs base), (rnrs)
let*与let相似,只是表达式expr ...按从左到右的顺序计算,每个表达式都在左边变量的范围内。当值之间存在线性依赖关系或计算顺序很重要时,使用let*。
(let* ([x (* 5.0 5.0)]
[y (- x (* 4.0 4.0))])
(sqrt y)) => 3.0
(let ([x 0] [y 1])
(let* ([x y] [y x])
(list x y))) => (1 1)
任何let*表达式都可以转换为一组嵌套的let表达式。下面的let*定义演示了典型的转换。
(define-syntax let*
(syntax-rules ()
[(_ () e1 e2 ...)
(let () e1 e2 ...)]
[(_ ((x1 v1) (x2 v2) ...) e1 e2 ...)
(let ((x1 v2))
(let* ((x2 v2) ...) e1 e2 ...))]))
语法: (letrec ((var expr) ...) body1 body2 ...)
返回: 主体最终表达式的值
库: (rnrs base), (rnrs)
letrec类似于let和let*,除了所有表达式expr…都在变量var的范围之内。letrec允许定义相互递归的过程。
(letrec ([sum (lambda (x)
(if (zero? x)
0
(+ x (sum (- x 1)))))])
(sum 5)) => 15
表达式expr ...的求值顺序是未指定的,因此在计算所有值之前,程序不能对由letrec表达式绑定的任何变量的引用求值。(lambda表达式中变量的出现不算作引用,除非在计算所有值之前应用结果过程)。如果违反此限制,则会引发条件类型&assertion的异常。
一个expr不应该返回超过一次。也就是说,它不应该正常返回,也不应该通过调用在求值期间获得的继续来返回,并且不应该通过两次调用这种继续来返回两次。实现不需要检测违反此限制的情况,但是如果检测到了,则会引发条件类型&assertion的异常。
当变量及其值之间存在循环依赖关系,且计算顺序不重要时,选择letrec而不是let或let*。当存在循环依赖项并且需要从左到右计算绑定时,选择letrec*而不是letrec。
letrec表达式的形式
(letrec ((var expr) ...) body1 body2 ...)
可以用let和set!来表示
(let ((var #f) ...)
(let ((temp expr) ...)
(set! var temp) ...
(let () body1 body2 ...)))
temp作为临时变量,即,表示尚未出现在letrec表达式中的项,每个项对应一个(var expr)对。外部let表达式建立变量绑定。给定每个变量的初始值并不重要,因此任何值都可以代替#f。首先建立绑定,这样expr...可以包含所有出现d变量的出现,也就是说,表达式在变量的作用域内计算。中间的let计算值并将它们绑定到临时变量,然后set!表达式为每个变量分配相应的值。内部let的出现是为了应对主体包含内部定义的情况。
使用这种转换的letrec的定义见第八章。
这种转换并不强制限定,即expr表达式不是必须计算变量的任何引用或者赋值。更详细的转换来执行此限制并实际生成更有效的代码是可能的。
语法:(letrec* ((var expr) ...) body1 body2 ...)
返回: 主体最终表达式的值
库: (rnrs base),(rnrs)
letrec*与letrec类似,只是letrec*计算expr...按照从左到右的顺序。虽然在对相应的expr进行求值之前,程序仍然不能对任何var的引用求值,但是对var的引用可以在之后的任何时间进行求值,包括在任何后续绑定的expr的求值期间。
letrec*表达式的形式
(letrec* ((var expr) ...) body1 body2...)
可以用let和set的组合来表示
(let ((var #f) ...)
(set! var expr) ...
(let () body1 body2 ...))
外部let表达式创建绑定,每个赋值计算一个表达式,并立即按顺序将相应的变量设置为其值,而内部let计算主体。相比begin,let更适用于接下来的情况,因为主体可能包括内部定义和表达式。
(letrec* ([sum (lambda (x)
(if (zero? x)
0
(+ x (sum (- x 1)))))]
[f (lambda () (cons n n-sum))]
[n 15]
[n-sum (sum n)])
(f)) => (15 . 120)
(letrec* ([f (lambda () (lambda () g))]
[g (f)])
(eq? (g) g)) => #t
(letrec* ([g (f)]
[f (lambda () (lambda () g))])
(eq? (g) g)) => exception: attempt to reference undefined variable f
4.5 多值
语法: (let-values ((formals expr) ...) body1 body2 ...)语法: (let*-values ((formals expr) ...) body1 body2 ...)
返回: 主体最终表达式的值
库: (rnrs base), (rnrs)
let-values是一种可以接收多个值并将它们绑定到变量的方便的方式。它的结构类似于let,但允许在每个左侧都有一个任意的形式参数列表(类似lambda)。let*-value与let*类似,但按从左到右的顺序执行绑定。如果expr返回的值的数量与相应的形式参数不符合,就会引发条件类型&assertion的异常,如上面的lambda条目中所述。let-value的定义见第八章。
(let-value ([(a b) (values 1 2)] [c (values 1 2 3)])
(list a b c)) => (1 2 (1 2 3))
(let*-values ([(a b) (values 1 2)] [(a b) (values b a)])
(list a b)) => (2 1)
4.6 变量定义
语法: (define var expr)语法: (define var)语法: (define (var0 var1 ...) body1 body2 ...)语法: (define (var0 . varr) body1 body2 ...)语法: (define (var0 var1 var2 ... varr) body1 body2 ...)
库: (rnrs base), (rnrs)
在第一种形式中,define创建一个新的var到expr值的绑定。expr不应返回超过一次。也就是说,它不应该正常返回,也不应该通过调用在求值期间获得的继续来返回,并且不应该通过两次调用这种继续来返回两次。实现不需要检测违反此限制的情况,但是如果检测到了,则会引发条件类型&assertion异常。
第二种形式等价于(define var unspecified),其中unspecified是某个未指定的值。其余是将变量绑定到过程的简写形式,它们和下面关于的定义是一样的。
(define var
(lambda formals
body1 body2 ...))
其中,formals为(var1 ...),varr或第三、第四和第五种定义的`(var1 ...var2 ... . varr)。
定义可以出现在library主体的前面,顶级程序主体形式中的任何位置,以及lambda或case-lambda主体的前面,或者lambda派生的任何形式的前面,例如let或letrec*。在宏展开过程中,任何以定义的序列开头的主体都将转换为letrec*表达式,如第八章所述。
在变量定义可能出现的地方,语法定义可能与变量定义一起出现,见第八章。
(define x 3)
x => 3
(define f
(lambda (x y)
(* (+ x y) 2)))
(f 5 4) => 18
(define (sum-of-squares x y)
(+ (* x x) (* y y)))
(sum-of-squares 3 4) => 25
(define f
(lambda (x)
(+ x 1)))
(let ([x 2])
(define f
(lambda (y)
(+ y x)))
(f 3)) => 5
(f 3) => 4
一组定义可以通过将它们封装在begin形式中来分组。以这种方式分组的定义可能出现在普通变量和语法定义可能出现的任何地方。它们被视为单独编写的,即,不包含begin形式的封装。该特性允许语法扩展扩展为定义组。
(define-syntax multi-define-syntax
(syntax-rules ()
[(_ (var expr) ...)
(begin
(define-syntax var expr)
...)]))
(let ()
(define plus
(lambda (x y)
(if (zero? x)
y
(plus (sub1 x) (add1 y)))))
(multi-define-syntax
(add1 (syntax-rules () [(_ e) (+ e 1)]))
(sub1 (syntax-rules () [(_ e) (- e 1)])))
(plus 7 8)) => 15
许多实现支持交互式的“顶层”,其中可以交互式地输入变量和其他定义,或者从文件中加载它们。这些顶级定义的行为超出了Revised6 Report的范围,但是顶级变量定义在在对它们的任何引用或赋值的求值之前,这些行为在大多数实现中都是一致的。因此,例如,如果g还没有定义,下面f的顶级定义中对g的引用是可以的,并且g被假设为一个变量的命名,该变量将在稍后的某个点定义。
(define f
(lambda (x)
(g x)))
如果在计算f之前先定义g,那么g被定义为变量的假设就被证明是正确的,对f的调用就像预期的那样工作。
(define g
(lambda (x)
(+ x x)))
(f 3) => 6
如果g被定义为语法扩展的关键字,那么将g绑定为变量的假设被证明是错误的,如果在调用f之前没有重新定义f,那么实现很可能会引发一个异常。
4.7 赋值
语法: (set! var expr)
返回: 未指明
库: (rnrs base), (rnrs)
set!不为var建立新的绑定,而是更改现有绑定的值。它首先计算expr,然后将expr的值赋值给var。在修改后的绑定范围内对var的任何后续引用都计算为新值。
Scheme中使用赋值的频率不像大多数其他语言那样高,但是它们对于实现状态更改非常有用。
(define flip-flop
(let ([state #f])
(lambda ()
(set! state (not state))
state)))
(flip-flop) => #t
(flip-flop) => #f
(flip-flop) => #t
赋值对于缓存值也很有用。下面的示例使用了一种称为memoization的技术,在这种技术中,一个过程记录与旧输入值相关的值,因此它不需要重新计算这些值,从而实现另外一个Fibonacci函数的双递归指数定义的快速版本。
(define memoize
(lambda (proc)
(let ([cache '()])
(lambda (x)
(cond
[(assq x cache) => cdr]
[else
(let ([ans (proc x)])
(set! cache (cons (cons x ans) cache))
ans)])))))
(define fibonacci
(memoize
(lambda (n)
(if (< n 2)
1
(+ (fibonacci (- n 1)) (fibonacci (- n 2)))))))
(fibonacci 100) => 573147844013817084101