虽然内部define可用于局部绑定,Racket提供了三种表,它们给予程序员在绑定方面的更多控制:let、let*和letrec。
在《Racket参考》的“(let)”部分也有关于let的文档。
一个let表绑定一组标识,每个对应某个表达式的结果,以在let主体中使用:
(let ([id expr] ...) body ...+)
id绑定”在并行(parallel)状态中”。也就是说,在右手边的expr里面没有id被绑定于任何id,但在body中所有的都能找到。id必须不同于其它彼此。
Examples:
> (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是按顺序求值的。
在《Racket参考》的“(let)”部分也有关于let*的文档。
let*的语法和let的一样:
(let* ([id expr] ...) body ...+)
不同的是,每个id可在以后的expr使用中以及body中找到。此外,id不需要有区别,并且最近的绑定是可见的一个。
Examples:
> (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")
在《Racket参考》的“(let)”部分也有关于letrec的文档。
letrec的语法也和let相同:
(letrec ([id expr] ...) body ...+)
而let使其绑定仅在body内被找到,let*使其绑定在任何后面的绑定expr内被找到,letrec使其绑定在所有其它expr——甚至更早的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-601deb1a4a46441cae24498fbda
3c772-ModemManager.service-SoWuoP
system error: 权限不够; errno=13
当一个letrec表的expr是典型的lambda表达式时,它们可以是任何表达式。表达式按顺序求值,而且在每个值被获取后,它立即用相应的id关联。如果一个id在其值准备就绪之前被引用,一个错误被引发,正如内部定义一样。
> (letrec ([quicksand quicksand]) quicksand) quicksand: undefined;
cannot use before initialization
一个命名let是一个迭代和递归表。它使用与局部绑定相同的语法关键字let,但在let之后的一个标识(而不是一个最近的开括号)触发一个不同的解析。
(let proc-id ([arg-id init-expr] ...) body ...+)
一个命名let表等效于
(letrec ([proc-id (lambda (arg-id ...) body ...+)]) (proc-id init-expr ...))
也就是说,一个命名let绑定一个只在函数主体中可见的函数标识,并且用一些初始表达式的值隐式调用函数。
Examples:
(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
在《Racket参考》的“(let)”部分也有关于多值绑定表的文档。
以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。
Example:
> (let-values ([(q r) (quotient/remainder 14 3)]) (list q r)) '(4 2)
大多数函数都可用于分支,如<和string?,产生#t或#f。无论什么情况,Racket的分支表以任何非#f值为真。我们说一个真值(true value)意味着#f值之外的任何值。
这个对“真值(true value)”的约定在#f能够代替故障或表明不提供一个可选的值的地方与协议完全吻合 。(谨防过度使用这一技巧,记住一个异常通常对报告故障是一个更好的机制。)
例如,member函数具有双重职责;它可以用来查找一个从一个特定条目开始的列表的尾部,或者它可以用来简单地检查一个项目是否存在于一个列表中:
> (member "Groucho" '("Harpo" "Zeppo")) #f
> (member "Groucho" '("Harpo" "Groucho" "Zeppo")) '("Groucho" "Zeppo")
> (if (member "Groucho" '("Harpo" "Zeppo")) 'yep 'nope) 'nope
> (if (member "Groucho" '("Harpo" "Groucho" "Zeppo")) 'yep 'nope) 'yep
在《Racket参考》里的“(if)”部分有关于if的文档。
在一个if表里:
(if test-expr then-expr else-expr)
test-expr总是被求值。如果它产生任何非#f值,那么then-expr被求值。否则,else-expr被求值。
一个if表必须既有一个then-expr也有一个else-expr;后者不是可选的。执行(或跳过)基于一个test-expr的副作用,使用when或unless,对此我们将在后边《定序》部分描述。
在《Racket参考》的“(if)”部分有关于and和or的文档。
Racket的and和or是语法表,而不是函数。不像一个函数,如果前边的一个求值确定了答案,and和or表会忽略后边表达式的求值。
(and expr ...)
如果其所有expr产生#f,一个and表产生#f。否则,它从它最后的expr产生值。作为一个特殊的情况,(and)产生#t。
(or expr ...)
如果其所有的expr产生#f,and表产生#f。否则,它从它的expr第一个非#f值产生值。作为一个特殊的情况,(or)产生#f。
Examples:
> (define (got-milk? lst) (and (not (null? lst)) (or (eq? 'milk (car lst)) (got-milk? (cdr lst))))) ; 仅在需要时再发生。 > (got-milk? '(apple banana)) #f
> (got-milk? '(apple milk banana)) #t
如果求值达到一个and或or}表的最后的expr,那么expr的值直接决定and或or}的结果。因此,最后的expr是在尾部的位置,这意味着上面的got-milk?函数在固定空间中运行。
《尾递归》介绍尾部调用和尾部位置。
cond表编链了一系列的测试以选择一个结果表达式。对于一个初步近式,cond语法如下:
在《Racket参考》里的“(if)”部分也有关于cond的文档。
(cond [test-expr body ...+] ...)
每个test-expr被按顺序求值。如果它产生#f,相应的body被忽略,并且求值进行到下一个test-expr。一旦一个test-expr产生一个真值,它的body被求值以产生作为cond表的结果。并不再进一步对test-expr求值。
在一个cond里最后的test-expr可用else代替。就求值而言,else作为一个#t的同义词提供,但它阐明了最后的从句意味着捕获所有剩余的实例。如果else没有被使用,那么可能没有test-expr产生一个真值;在这种情况下,该cond表达式的结果是#<void>。
Examples:
> (cond [(= 2 3) (error "wrong!")] [(= 2 2) 'ok]) 'ok
> (cond [(= 2 3) (error "wrong!")])
> (cond [(= 2 3) (error "wrong!")] [else 'ok]) 'ok
(define (got-milk? lst) (cond [(null? lst) #f] [(eq? 'milk (car lst)) #t] [else (got-milk? (cdr lst))]))
> (got-milk? '(apple banana)) #f
> (got-milk? '(apple milk banana)) #t
cond的完整语法包括另外两种从句:
(cond cond-clause ...)
cond-clause = [test-expr then-body ...+] | [else then-body ...+] | [test-expr => proc-expr] | [test-expr]
=>变体获取其test-expr的真值结果并且传递给proc-expr的结果,proc-expr必须是有一个参数的一个函数。
Examples:
> (define (after-groucho lst) (cond [(member "Groucho" lst) => cdr] [else (error "not there")])) > (after-groucho '("Harpo" "Groucho" "Zeppo")) '("Zeppo")
> (after-groucho '("Harpo" "Zeppo")) not there
一个从句只包括一个test-expr是很少使用的。它捕获test-expr的真值结果,并简单地返回这个结果给整个cond表达式。
Racket程序员喜欢编写尽可能少副作用的程序,因为纯粹的函数式代码更容易测试及组成更大的程序。然而,与外部环境的交互需要定序,例如写入一个显示器、打开一个图形窗口或在磁盘上操作一个文件时。
在《Racket参考》的“(begin)”中也有关于begin的文档。
一个begin表达式定序表达式:
(begin expr ...+)
expr被顺序求值,并且除最后的expr结果外所有结果都被忽略。来自最后的expr结果作为begin表的结果,并且它是相对于begin表来说位于尾部位置。
Examples:
(define (print-triangle height) (if (zero? height) (void) (begin (display (make-string height #\*)) (newline) (print-triangle (sub1 height))))) > (print-triangle 4)
****
***
**
*
有多种表,比如lambda或cond支持一系列甚至没有一个begin的表达式。这样的状态有时被叫做有一个隐含的begin。
Examples:
(define (print-triangle height) (cond [(positive? height) (display (make-string height #\*)) (newline) (print-triangle (sub1 height))])) > (print-triangle 4)
****
***
**
*
begin表在顶层(top level)、模块层(module level)或仅在内部定义之后作为一个body是特定的。在这些位置,begin的上下文被拼接到周围的上下文中,而不是形成一个表达式。
Example:
> (let ([curly 0]) (begin (define moe (+ 1 curly)) (define larry (+ 1 moe))) (list larry curly moe)) '(2 0 1)
这种拼接行为主要用于宏,我们稍后在《宏》中讨论。
在《Racket参考》的“(begin)”中也有关于begin0的文档。
一个begin0表达式具有与一个begin表达式相同的语法:
(begin0 expr ...+)
不同的是begin0返回第一个expr的结果,而不是最后的expr结果。begin0表对于实现发生在一个计算之后的副作用是有用的,尤其是在计算产生结果的一个未知数值的情况下。
Examples:
(define (log-times thunk) (printf "Start: ~s\n" (current-inexact-milliseconds)) (begin0 (thunk) (printf "End..: ~s\n" (current-inexact-milliseconds)))) > (log-times (lambda () (sleep 0.1) 0))
Start: 1668430707527.6035
End..: 1668430707627.6519
0
> (log-times (lambda () (values 1 2)))
Start: 1668430707629.9336
End..: 1668430707629.9768
1
2
在《Racket参考》的“(when+unless)”部分也有关于when和unless的文档。
when表将一个if样式条件与对“then”子句且无“else”子句的定序组合:
(when test-expr then-body ...+)
如果test-expr产生一个真值,那么所有的then-body被求值。最后的then-body结果是when表的结果。否则,没有then-body被求值而且结果是#<void>。
unless是相似的:
(unless test-expr then-body ...+)
不同的是test-expr结果是相反的:如果test-expr结果为#f,then-body被求值。
Examples:
(define (enumerate lst) (if (null? (cdr lst)) (printf "~a.\n" (car lst)) (begin (printf "~a, " (car lst)) (when (null? (cdr (cdr lst))) (printf "and ")) (enumerate (cdr lst))))) > (enumerate '("Larry" "Curly" "Moe")) Larry, Curly, and Moe.
(define (print-triangle height) (unless (zero? height) (display (make-string height #\*)) (newline) (print-triangle (sub1 height))))
> (print-triangle 4)
****
***
**
*