函数和变量绑定是scheme程序的重要组成部分,本章重点讲述与其相关的语法表达式。 我们先从最基础的变量引用和lambda定义开始,然后再讨论变量绑定和赋值(defineletreclet-valuesset!)。

其它也会绑定变量或者对变量进行赋值但主要目的不在于此的语法表达式(如命名let)会在第五章中讨论。

变量引用

语法:variable
返回:variable的值

凡是出现在绑定语句有效范围内的被绑定标识符都被scheme认为引用了某个变量,这样的绑定语句有definelambdalet等。

list => #<procedure>
(define x 'a)
(list x x) => (a a)
(let ([x 'b])
  (list x x)) => (b b)
(let ([let 'let]) let) => let

任何未被绑定的标识符都不能出现在库或者程序顶层中,否则scheme会报语法错误。 变量引用的定义语句不一定要出现在变量使用语句之前,只需保证变量引用在被正式求值之前定义已经结束即可。举例来讲:

(define f
  (lambda (x)
    (g x)))
(define g
  (lambda (x)
    (+ x x)))

函数f使用了之后定义的函数g,由于函数f在没被执行前不会对g求值,以上代码不会有任何问题。 反之,下面代码中的函数g在定义结束之前被定义语句define调用了,因此scheme会报错。

(define q (g 3))
(define g
  (lambda (x)
    (+ x x)))

lambda

语法:(lambda formals body1 body2 ...)
返回:子程序
库:(rnrs base), (rnrs)

lambda表达式的作用是创建子程序。 scheme中任何涉及到创建子程序或局部变量的操作最终都通过某种形式的lambdacase-lambda语句完成。

formals字句表示子程序的形参,body1 body2 ...表示子程序的函数体。

可以在函数体的起始位置添加多个定义语句,其有效范围被局限在函数体内部。 当函数体的起始处出现定义语句时,函数体被扩展为letrec*表达式,局部定义通过其绑定语句完成,执行部分是其它函数执行语句。 如此一来,函数体中的定义表达式就被清理掉了。

创建子程序时,函数体中使用到的形参以外的所有外部变量都会被作为子程序的一部分保留下来。 当子程序被调用时,函数体中的形参被绑定至实参,其它的变量则会被恢复至原先的绑定,最后再对函数体进行求值。

子程序的实参被绑定至形参的具体过程如下:

  • formalslist时(如(x y z)),实参和形参一一对应。如果实参和形参的数目不一致,scheme会抛出异常&assertion
  • formals为单个变量时(如z),该变量被绑定至所有实参组成的list
  • formals为结尾非空的非法list时(如(x y . z)),最后一个形参除外,其它形参和实参一一对应,最后一个形参被绑定至剩下的其它实参组成的list。 当实参的数目过少时,scheme会抛出异常&assertion

在对函数体求值时,scheme会依次对函数体的每条表达式进行求值计算,最后一个表达式的求值结果被作为函数的结果返回。

函数或者子程序没有统一的打印输出格式,本书使用#<procedure>作为输出格式。

(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)

case-lambda

如前所述,lambda表达式创建的子程序只支持两种参数风格:固定长度的参数列表和大于等于某个长度的参数列表。

(lambda (var1 ... varn) body1 body2 ...)

以上形式的lambda表达式只接受n个参数。

(lambda (var1 ... varn . r) body1 body2 ...)

以上形式的lambda表达式可接受大于等于n个参数。

由此可见,lambda表达式灵活性相对有限(仅就参数列表这一点来讲),现代编程语言中常出现的参数默认值也没有被直接支持。 显而易见,通过对以上第二种形式的lambda表达式的入参进行分析,结合参数列表的长度检查和carcdr函数,我们还是能够 实现参数默认值的,代价是更为晦涩的代码和性能的降低。

case-lambda语法表达式就是为针对这种情况而引入的语法扩展。

语法:(case-lambda clause ...)
返回:子程序
库:(rnrs control), (rnrs)

case-lambda由多个cluase组成,每个clause的定义形式如下:

[formals body1 body2 ...]

上面的formals部分的含义和lambda表达式中的形参部分一模一样,它表示了当前函数所能接受的其中一种参数风格,多个clause组合起来 为函数提供了多种风格的参数列表和对应的函数体。

case-lambda定义的函数被调用时,scheme会对所有的clause进行逐个分析,当遇到第一个能够接受当前参数的clause时,该clauseformals中的所有形参被绑定至对应的实参,然后再对对应的函数体进行求值。如果没有任何clause能够和实参匹配成功,scheme会抛出异常 &assertion

下面的函数make-list就是用case-lambda定义的,调用方不输入初始值的情况下,make-list默认将#f作为初始值:

(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-lambdaend参数变为可选项,默认不输入表示字符串的结尾, 也可以startend参数都不输入,这时substring的功能和string-copy一模一样:

(define substring1
  (case-lambda
    [(s) (substring1 s 0 (string-length s))]
    [(s start) (substring1 s start (string-layout s))]
    [(s start end) (substring s start end)]))

还可以在只有一个参数时将start默认为字符串起始位置:

(define substring2
  (case-lambda
    [(s) (substring2 s 0 (string-length s))]
    [(s end) (substring2 s 0 end)]
    [(s start end) (substring s start end)]))

我们甚至可以让startend参数要么同时出现,要么都使用默认值:

(define substring3
  (case-lambda
    [(s) (substring3 s 0 (string-length s))]
    [(s start end) (substring s start end)]))

本地绑定

语法:(let ((var expr) ...) body1 body2 ...)
返回:绑定语句块的最后一个表达式的求值结果
库:(rnrs base), (rnrs)

let表达式被用于创建本地绑定,绑定语句中的每个var标识符均被绑定至对应的expr的求值结果。 let的语句部分(body1 body2 …)的求值方式跟lambda的函数体部分一样。

let还有其它几种功能相近的“近亲”表达式:let*letrecletrec*letlet*letrecletrec*的不同之处在于绑定语句的求值表达式expr无法使用任何被绑定的变量varletlet*letrec*的不同之处在于每个var的绑定顺序是不可预知。

(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)

下面的代码利用lambdalet表达式定义为语法扩展:

(define-syntax let
  (syntax-rules ()
    [(_ ((x e) ...) b1 b2 ...)
      ((lambda (x ...) b1 b2 ...) e ...)]))

带命名的let表达式将会在5.4节中再描述。

语法:(let* ((var expr) ...) body1 body2 ...)
返回:绑定语句块的最后一个表达式的求值结果
库:(rnrs base), (rnrs)

let*的作用与let类似,区别在与let*的绑定语句的求值顺序是固定的从左至右。 因此,当绑定语句的求值顺序对最终的求值结果有影响时,可以使用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表达式,具体如下:

(define-syntax let*
  (syntax-rules ()
    [(_ () e1 e2 ...)
      (let () e1 e2 ...)]
    [(_ ((x1 v1) (x2 v2) ...) e1 e2 ...)
      (let ((x1 v1))
        (let* ((x2 v2) ...) e1 e2 ...))]))
语法:(letrec ((var expr) ...) body1 body2 ...)
返回:绑定语句块的最后一个表达式的求值结果
库:(rnrs base), (rnrs)

letrec的作用和letlet*类似,区别是letrec的绑定语句的表达式可任意访问其它绑定变量var。 因此letrec可被用于定义多个互相调用的递归函数。

(letrec ([sum (lambda (x)
                (if (zero? x)
                    0
                    (+ x (sum (- x 1)))))])
  (sum 5)) => 15

由于letrec的绑定语句的执行顺序是不确定的,在绑定语句块结束前不能对绑定变量进行求值,否则scheme会抛出&assertion异常。 (注意,定义lambda语句时不会对其中出现的外部变量马上进行求值,而是等到之后函数被调用时才会进行求值)。

当绑定变量之间有依赖,并且其求值顺序无所谓时,优先选择letrec表达式。 当绑定变量之间有依赖,并且其求值顺序需要从左至右依次执行时,优先选择letrec*表达式。

形如(letrec ((var expr) ...) body1 body2 ...)letrec表达式可通过letset!来定义:

(let ((var #f) ...)
  (let ((temp expr) ...)
    (set! var temp)
    ...
    (let ()
      body1 body2 ...)))

上面的temp ...是一组新的变量,每个var变量都存在一个对应的temp变量。 最外层的let语句首先创建所有的var变量,初始值在这时并不重要,因此全部初始为#f。 这样做的目的是为了保证变量var ...可以在后面的expr ...语句中出现。 中间的let语句对expr ...进行求值,并且将求值结果绑定至每个对应的temp变量。 最内层又加了一个let语句是为了应对body1 body2 ...包含内部定义的情况(还记得吗,内部定义只能出现在语句块的首部)。

语法:(letrec* ((var expr) ...) body1 body2 ...)
返回:绑定语句块的最后一个表达式的求值结果
库:(rnrs base), (rnrs)

letrec*的作用和letrec同样类似,区别是letrec*expr ...的求值严格按照从左至右的顺序进行。 因此,letrec*中后面的绑定语句可以正常对前面出现的绑定变量进行求值计算。

形如(letrec* ((var expr) ...) body1 body2 ...)letrec表达式同样可通过letset!来定义:

(let ((var #f) ...)
  (set! var expr) ...
  (let () body1 body2 ...))

最外层的let表达式创建了var ...变量,然后依次对每个expr进行求值后再赋给对应的var变量。 最后,使用let而不是begin语句来封装body1 body2 ...的原因同样是为了应对body1 body2 ...包含内部定义的情况。

(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

多变量绑定