4.6 局部绑定
尽管 define 内部定义可用于局部绑定,然而 Racket 还提供了三种形式, 赋予了程序员比绑定更多的控制能力:let、let* 和 letrec。
4.6.1 平行绑定:let
The Racket Reference的Local Binding: let, let*, letrec, ...一节中也提供了let的文档。
let 形式将多个标识符绑定到一些表达式的结果上以便在 let 的主体中使用:
(let ([id expr] ...) body ...+)
id 是“平行绑定”的,即没有任何 id 会作为右边的 expr 被绑定到某个 id 上。不过所有的 id 在 body 中均可用。 id 之间必须互不相同。
> (let ([me "Bob"]) me) "Bob"
> (let ([me "Bob"] [myself "Robert"] [I "Bobby"]) (list me myself I)) '("Bob" "Robert" "Bobby")
> (let ([me "Bob"] [me "Robert"]) me) eval:3:0: let: duplicate identifier
at: me
in: (let ((me "Bob") (me "Robert")) me)
id 的 expr 无法看到其自己的绑定这一事实, 通常对于必须引用旧有值的包裹体来说很有用:
> (let ([+ (lambda (x y) (if (string? x) (string-append x y) (+ x y)))]) ; 使用原始的 + (list (+ 1 2) (+ "see" "saw"))) '(3 "seesaw")
有时,let 绑定的平行性质对于交换或重排一些绑定来说也很方便:
> (let ([me "Tarzan"] [you "Jane"]) (let ([me you] [you me]) (list me you))) '("Jane" "Tarzan")
let 绑定的“平行性”并不意味着它隐含了并发求值。expr 会按照顺序求值,即便绑定过程会推迟到所有 expr 求值完毕。
4.6.2 顺序绑定:let*
The Racket Reference的Local Binding: let, let*, letrec, ...一节中也提供了let*的文档。
(let* ([id expr] ...) body ...+)
不同之处在于每个 id 都可以在后续的 expr 中使用,body 亦同。此外,id 可以相同,其中最近的绑定是可见的。
> (let* ([x (list "Burroughs")] [y (cons "Rice" x)] [z (cons "Edgar" y)]) (list x y z)) '(("Burroughs") ("Rice" "Burroughs") ("Edgar" "Rice" "Burroughs"))
> (let* ([name (list "Burroughs")] [name (cons "Rice" name)] [name (cons "Edgar" name)]) name) '("Edgar" "Rice" "Burroughs")
换句话说,let* 形式等价于嵌套的 let 形式,每层都只有一个绑定:
> (let ([name (list "Burroughs")]) (let ([name (cons "Rice" name)]) (let ([name (cons "Edgar" name)]) name))) '("Edgar" "Rice" "Burroughs")
4.6.3 递归绑定:letrec
The Racket Reference的Local Binding: let, let*, letrec, ...一节中也提供了letrec的文档。
(letrec ([id expr] ...) body ...+)
let 的绑定只在 body 中可用,let* 的绑定可在任何之后的 expr 绑定中可用,而 letrec 的绑定对于所有其它 expr 来说均可用,即便前面的也是。换句话说, letrec 绑定是递归的。
letrec 形式中的 expr 最常用 lambda 形式来表示递归或互递归函数。
> (letrec ([swing (lambda (t) (if (eq? (car t) 'tarzan) (cons 'vine (cons 'tarzan (cddr t))) (cons (car t) (swing (cdr t)))))]) (swing '(vine tarzan vine vine))) '(vine vine tarzan vine)
> (letrec ([tarzan-near-top-of-tree? (lambda (name path depth) (or (equal? name "tarzan") (and (directory-exists? path) (tarzan-in-directory? path depth))))] [tarzan-in-directory? (lambda (dir depth) (cond [(zero? depth) #f] [else (ormap (λ (elem) (tarzan-near-top-of-tree? (path-element->string elem) (build-path dir elem) (- depth 1))) (directory-list dir))]))]) (tarzan-near-top-of-tree? "tmp" (find-system-path 'temp-dir) 4)) directory-list: could not open directory
path: /var/tmp/systemd-private-5d92c99a9c9040dfa92e10b2e68
d4ce7-systemd-timesyncd.service-7iYVOn
system error: Permission denied; errno=13
尽管 letrec 中的 expr 通常为 lambda 表达式, 然而它们实际上可以是任何表达式。表达式按顺序求值,在得到所有的值后, 它们会立即被关联到与其对应的 id。若某个 id 在其值就绪前被引用,那么就会触发一个错误,就像内部定义中的那样。
> (letrec ([quicksand quicksand]) quicksand) quicksand: undefined;
cannot use before initialization
4.6.4 命名的 let
命名的 let 是一种递归或迭代的形式。它使用相同的语法关键字 let 进行局部绑定,但 let 之后的标识符(而非紧跟着的开括号)会触发不同的解析过程。
(let proc-id ([arg-id init-expr] ...) body ...+)
命名的 let 形式等价于
(letrec ([proc-id (lambda (arg-id ...) body ...+)]) (proc-id init-expr ...))
也就是说,命名的 let 只绑定在函数体中可见的函数标识符, 且它隐式地以某些初始表达式的值调用了函数。
(define (duplicate pos lst) (let dup ([i 0] [lst lst]) (cond [(= i pos) (cons (car lst) lst)] [else (cons (car lst) (dup (+ i 1) (cdr lst)))]))) > (duplicate 1 (list "apple" "cheese burger!" "banana")) '("apple" "cheese burger!" "cheese burger!" "banana")
4.6.5 多值绑定:let-values、let*-values 与 letrec-values
The Racket Reference的Local Binding: let, let*, letrec, ...一节中也提供了多值绑定形式的文档。
和 define-values 在定义中绑定多个结果(见 多值与 define-values) 一样,let-values、let*-values 和 letrec-values 也按照同样的方式局部地绑定多个结果。
(let-values ([(id ...) expr] ...) body ...+)
(let*-values ([(id ...) expr] ...) body ...+)
(letrec-values ([(id ...) expr] ...) body ...+)
每个 expr 产生的值的数量都必须与 id 相对应。 这些形式的绑定规则与不带 -values 的形式相同: let-values 的 id 只在 body 中绑定, let*-values 的 id 可在后面从句的 expr 中绑定,而 letrec-value 的 id 可在所有的 expr 中绑定。
> (let-values ([(q r) (quotient/remainder 14 3)]) (list q r)) '(4 2)