《Racket概要》这一章介绍了一些基本的Racket的句法表:定义、过程程序、条件表达式等等。本节提供这些形式的更详细信息,以及一些附加的基本表。
4.1 标记法 |
4.2 标识符和绑定 |
4.3 函数调用(过程程序) |
4.3.1 求值顺序和实参数量 |
4.3.2 关键字参数 |
4.3.3 apply函数 |
4.4 lambda函数(过程) |
4.4.1 申明一个剩余(rest)参数 |
4.4.2 声明可选(optional)参数 |
4.4.3 声明关键字(keyword)参数 |
4.4.4 实参数量感知函数:case-lambda |
4.5 定义:define |
4.5.1 函数简写 |
4.5.2 柯里函数简写 |
4.5.3 多值和define-values |
4.5.4 内部定义 |
4.6 局部绑定 |
4.6.1 并行绑定:let |
4.6.2 顺序绑定:let* |
4.6.3 递归绑定:letrec |
4.6.4 命名let |
4.6.5 多值绑定:let-values,let*-values,letrec-values |
4.7 条件 |
4.7.1 简单分支:if |
4.7.2 组合测试:and和or |
4.7.3 编链测试:cond |
4.8 定序 |
4.8.1 前效应:begin |
4.8.2 后效应:begin0 |
4.8.3 if效应:when和unless |
4.9 赋值:set! |
4.9.1 使用赋值的指导原则 |
4.9.2 多值赋值:set!-values |
4.10 引用:quote和' |
4.11 准引用:quasiquote和‘ |
4.12 简单分派:case |
4.13 动态绑定:parameterize |
这一章(以及其余的文档)使用了一个稍微不同的标记法,而不是基于字符的《Racket概要》章里的语法。对于一个句法表something的使用表现为如下方式:
(something [id ...+] an-expr ...)
在本规范中斜体的元变量,如id和an-expr,使用Racket标识符的语法,所以an-expr是一元变量。一个命名约定隐式地定义了许多元变量的含义:
一个以id结束的元变量代表一个标识符,如x或my-favorite-martian。
一个以keyword结束的元标识符代表一个关键字,如#:tag。
一个以expr结束的元标识符代表任意子表,它将被解析为一个表达式。
一个以body结束的元标识符代表任意子表;它将被解析为一个局部定义或者一个表达式。一个body只有不被任何表达式前置时才能解析为一个定义,并且最后一个body必须是一个表达式;参见《内部定义》部分。
在语法中的方括号表示表的一个括号序列,这里方括号通常被使用(约定)。也就是说,方括号并不表示是句法表的可选部分。
一个...表示前置表的零个或多个重复,...+表示前置数据的一个或多个重复。另外,非斜体标识符代表它们自己。
那么,基于上面的语法,这里有一些something的与以上相符合的用法:
(something [x]) (something [x] (+ 1 2)) (something [x my-favorite-martian x] (+ 1 2) #f)
一些语法表规范指既不是隐式定义的也不是预定义的元变量。这样的元变量在主表后面定义,使用一个BNF-like表提供选择:
(something-else [thing ...+] an-expr ...)
thing = thing-id | thing-keyword
上面的例子表明,在一个something-else表中,一个thing要么是一个标识符要么是一个关键字。
一个表达式的上下文决定表达式中出现的标识符的含义。特别是,用语言racket开始一个模块时,如:
#lang racket
意味着,在模块中,标识符在本指南中的描述开始于这里意义的描述:cons引用创建了一个配对的函数,car引用提取了一个配对的第一个元素的函数,等等。
《符号(Symbol)》介绍了标识符语法。
诸如define、lambda和let之类的表,用一个或多个标识符关联一个意义;也就是说,它们绑定(bind)标识符。绑定应用的程序部分是绑定的范围(scope)。对一个给定的表达式有效的绑定集是表达式的环境(environment)。
例如,有以下内容:
#lang racket (define f (lambda (x) (let ([y 5]) (+ x y)))) (f 10)
define是f的绑定,lambda有一个对x的绑定,let有一个对y的绑定,对f的绑定范围是整个模块;x绑定的范围是(let ([y 5]) (+ x y));y绑定的范围仅仅是(+ xy)的环境包括对y、x和f的绑定,以及所有在racket中的绑定。
一个模块级的define仅能够绑定没有被定义过或者require进模块的标识符。然而,一个局部define或其它绑定表,能够给一个已经有一个绑定的标志符以一个新的局部绑定;这样的一个绑定覆盖(shadows)已经存在的绑定。
(define f (lambda (append) (define cons (append "ugly" "confusing")) (let ([append 'this-was]) (list append cons)))) > (f list) '(this-was ("ugly" "confusing"))
类似地,一个模块级define可以从这个模块的语言覆盖一个绑定。例如,一个racket模块里的(define cons 1)覆盖被racket提供的cons。故意覆盖一个语言绑定绝对是一个好主意——尤其对于像cons这种被广泛使用的绑定——但是覆盖把一个程序员从不得不去避免每一个晦涩的通过一个语言提供的绑定中解脱出来。
即使像define和lambda这些从绑定中得到它们的意义,尽管它们有转换器(transformer)绑定(这意味着它们表明语法表)而不是值绑定。由于define有一个转换器绑定,这个标识符define不能被它自己使用于获取一个值。然而,对define的常规绑定可以被覆盖。
> define eval:1:0: define: bad syntax
in: define
> (let ([define 5]) define) 5
同样,用这种方式来覆盖标准绑定绝对是一个好主意,但这种可能性是Racket的灵活性一个固有部分。
表的一个表达式:
(proc-expr arg-expr ...)
是一个函数调用——也被称为一个应用程序(procedure application)——当proc-expr不是一个被绑定为一个语法翻译器(如if或define)的标识符时。
一个函数调用通过首先求值proc-expr并都按顺序(由左至右)来求值。然后,如果arg-expr产生一个接受arg-expr提供的所有参数的函数,这个函数被调用。否则,将引发一个异常。
> (cons 1 null) '(1)
> (+ 1 2 3) 6
> (cons 1 2 3) cons: arity mismatch;
the expected number of arguments does not match the given
number
expected: 2
given: 3
arguments...:
1
2
3
> (1 2 3) application: not a procedure;
expected a procedure that can be applied to arguments
given: 1
arguments...:
2
3
某些函数,如cons,接受一个固定数量的参数。某些函数,如+或list,接受任意数量的参数。一些函数接受一系列参数计数;例如substring既接受两个参数也接受三个参数。一个函数的实参数量(arity)是它接受参数的数量。
除了通过位置参数外,有些函数接受关键字参数(keyword arguments)。因此,一个arg可以是一个arg-keyword arg-expr序列而不仅仅只是一个arg-expr:
《关键字(Keyword)》介绍了关键字。
(proc-expr arg ...)
arg = arg-expr | arg-keyword arg-expr
例如:
(go "super.rkt" #:mode 'fast)
用"super.rkt"作为一个位置参数调用这个函数绑定到go,并用'fast作为一个参数与#:mode关键字关联。一个关键字隐式地与它后面的表达式配对。
既然一个关键字本身不是一个表达式,那么
(go "super.rkt" #:mode #:fast)
就是一个语法错误。#:mode关键字必须跟着一个表达式以产生一个参数值,并且#:fast不是一个表达式。
关键字arg的顺序决定arg-expr求值的顺序,而一个函数接受关键字参数不依赖于参数列表中的位置。上面对go的调用可以等价地编写为:
(go #:mode 'fast "super.rkt")
在《Racket参考》的“(application)”部分提供了有关过程程序的更多信息。
函数调用的语法支持任意数量的参数,但是一个特定的调用总是指定一个固定数量的参数。因此,一个带一个参数列表的函数不能直接应用一个类似于+的函数到一个列表的所有项中:
(define (avg lst) ; 不会运行…… (/ (+ lst) (length lst)))
> (avg '(1 2 3)) +: contract violation
expected: number?
given: '(1 2 3)
(define (avg lst) ; 不总会运行…… (/ (+ (list-ref lst 0) (list-ref lst 1) (list-ref lst 2)) (length lst)))
> (avg '(1 2 3)) 2
> (avg '(1 2)) list-ref: index too large for list
index: 2
in: '(1 2)
apply函数提供了一种绕过这种限制的方法。它使用一个函数和一个list参数,并将函数应用到列表中的值:
(define (avg lst) (/ (apply + lst) (length lst)))
> (avg '(1 2 3)) 2
> (avg '(1 2)) 3/2
> (avg '(1 2 3 4)) 5/2
为方便起见,apply函数接受函数和列表之间的附加参数。额外的参数被有效地cons到参数列表:
(define (anti-sum lst) (apply - 0 lst))
> (anti-sum '(1 2 3)) -6
apply函数也接受关键字参数,并将其传递给调用函数:
(apply go #:mode 'fast '("super.rkt")) (apply go '("super.rkt") #:mode 'fast)
包含在apply的列表参数中的关键字不算作调用函数的关键字参数;相反,这个列表中的所有参数都被作为位置参数对待。要将一个关键字参数列表传递给一个函数,使用keyword-apply函数,它接受一个要应用的函数和三个列表。前两个列表是平行的,其中第一个列表包含关键字(按keyword排序),第二个列表包含一个与每个关键字对应的参数。第三个列表包含位置函数参数,就像apply。
(keyword-apply go '(#:mode) '(fast) '("super.rkt"))
一个lambda表达式创建一个函数。在最简单的情况,一个lambda表达式具有的表:
(lambda (arg-id ...) body ...+)
一个具有n个arg-id的lambda表接受n个参数:
> ((lambda (x) x) 1) 1
> ((lambda (x y) (+ x y)) 1 2) 3
> ((lambda (x y) (+ x y)) 1) #
: arity mismatch; the expected number of arguments does not match the given
number
expected: 2
given: 1
arguments...:
1
一个lambda表达式也可以有这种表:
(lambda rest-id body ...+)
也就是说,一个lambda表达式可以有一个没有被圆括号包围的单个rest-id。所得到的函数接受任意数量的参数,并且这个参数放入一个绑定到rest-id的列表:
> ((lambda x x) 1 2 3) '(1 2 3)
> ((lambda x x)) '()
> ((lambda x (car x)) 1 2 3) 1
带有一个rest-id的函数经常使用apply函数调用另外的函数,它接受任意数量的参数。
《apply函数》描述apply。
(define max-mag (lambda nums (apply max (map magnitude nums)))) > (max 1 -2 0) 1
> (max-mag 1 -2 0) 2
lambda表还支持必需参数与一个rest-id组合:
(lambda (arg-id ...+ . rest-id) body ...+)
这个表的结果是一个函数,它至少需要与arg-id一样多的参数,并且还接受任意数量的附加参数。
(define max-mag (lambda (num . nums) (apply max (map magnitude (cons num nums))))) > (max-mag 1 -2 0) 2
> (max-mag) max-mag: arity mismatch;
the expected number of arguments does not match the given
number
expected: at least 1
given: 0
一个rest-id变量有时称为一个rest参数(rest argument),因为它接受函数参数的“剩余(rest)”。
不只是一个标识符,一个lambda表中的一个参数(不仅是一个剩余参数)可以用一个标识符和一个缺省值指定:
(lambda gen-formals body ...+)
gen-formals = (arg ...) | rest-id | (arg ...+ . rest-id) arg = arg-id | [arg-id default-expr]
表的一个参数[arg-id default-expr]是可选的。当这个参数不在一个应用程序中提供,default-expr产生默认值。default-expr可以引用任何前面的arg-id,并且下面的每个arg-id也必须应该有一个默认值。
(define greet (lambda (given [surname "Smith"]) (string-append "Hello, " given " " surname))) > (greet "John") "Hello, John Smith"
> (greet "John" "Doe") "Hello, John Doe"
(define greet (lambda (given [surname (if (equal? given "John") "Doe" "Smith")]) (string-append "Hello, " given " " surname)))
> (greet "John") "Hello, John Doe"
> (greet "Adam") "Hello, Adam Smith"
一个lambda表可以声明一个参数来通过关键字传递,而不是通过位置传递。关键字参数可以与位置参数混合,而且默认值表达式可以提供给两种参数:
《关键字参数》介绍用关键字进行函数调用。
(lambda gen-formals body ...+)
gen-formals = (arg ...) | rest-id | (arg ...+ . rest-id) arg = arg-id | [arg-id default-expr] | arg-keyword arg-id | arg-keyword [arg-id default-expr]
由一个应用程序使用同一个arg-keyword提供一个参数指定为arg-keyword arg-id。关键字的位置——在参数列表中的标识符配对与一个应用程序中的参数匹配并不重要,因为它将通过关键字而不是位置与一个参数值匹配。
(define greet (lambda (given #:last surname) (string-append "Hello, " given " " surname)))
> (greet "John" #:last "Smith") "Hello, John Smith"
> (greet #:last "Doe" "John") "Hello, John Doe"
一个arg-keyword [arg-id default-expr]参数指定一个带一个默认值的关键字参数。
(define greet (lambda (#:hi [hi "Hello"] given #:last [surname "Smith"]) (string-append hi ", " given " " surname))) > (greet "John") "Hello, John Smith"
> (greet "Karl" #:last "Marx") "Hello, Karl Marx"
> (greet "John" #:hi "Howdy") "Howdy, John Smith"
> (greet "Karl" #:last "Marx" #:hi "Guten Tag") "Guten Tag, Karl Marx"
lambda表不直接支持创建一个接受“rest”关键字的函数。要构造一个接受所有关键字参数的函数,使用make-keyword-procedure函数。这个函数支持make-keyword-procedure通过最先的两个(按位置)参数中的并行列表接受关键字参数,然后来自一个应用程序的所有位置参数作为保留位置参数。
《apply函数》介绍了keyword-apply。
(define (trace-wrap f) (make-keyword-procedure (lambda (kws kw-args . rest) (printf "Called with ~s ~s ~s\n" kws kw-args rest) (keyword-apply f kws kw-args rest)))) > ((trace-wrap greet) "John" #:hi "Howdy") Called with (#:hi) ("Howdy") ("John")
"Howdy, John Smith"
在《Racket参考》的“(lambda)”中提供了更多函数表达式的内容。
case-lambda表创建一个函数,它可以根据提供的参数数量而具有完全不同的行为。一个case-lambda表达式有这样的表:
(case-lambda [formals body ...+] ...)
formals = (arg-id ...) | rest-id | (arg-id ...+ . rest-id)
每个[formals body ...+]类似于(lambda formals body ...+)。应用以case-lambda生成一个函数类似于应用一个lambda给匹配给定参数数量的第一种情况。
(define greet (case-lambda [(name) (string-append "Hello, " name)] [(given surname) (string-append "Hello, " given " " surname)])) > (greet "John") "Hello, John"
> (greet "John" "Smith") "Hello, John Smith"
> (greet) greet: arity mismatch;
the expected number of arguments does not match the given
number
given: 0
一个case-lambda函数不能直接支持可选参数或关键字参数。
一个基本定义具为如下表:
(define id expr)
在这种情况下,id被绑定到expr的结果。
(define salutation (list-ref '("Hi" "Hello") (random 2))) > salutation "Hi"
define表还支持函数定义的一个简写:
(define (id arg ...) body ...+)
这是以下内容的简写:
(define id (lambda (arg ...) body ...+))
(define (greet name) (string-append salutation ", " name)) > (greet "John") "Hi, John"
(define (greet first [surname "Smith"] #:hi [hi salutation]) (string-append hi ", " first " " surname))
> (greet "John") "Hi, John Smith"
> (greet "John" #:hi "Hey") "Hey, John Smith"
> (greet "John" "Doe") "Hi, John Doe"
函数简写通过define也支持一个剩余参数(rest argument)(即,一个最终参数以在一个列表中收集额外参数):
(define (id arg ... . rest-id) body ...+)
它是以下内容的一个简写:
(define id (lambda (arg ... . rest-id) body ...+))
(define (avg . l) (/ (apply + l) (length l))) > (avg 1 2 3) 2
注意下面的make-add-suffix函数,它接收一个字符串并返回另一个接受一个字符串的函数:
(define make-add-suffix (lambda (s2) (lambda (s) (string-append s s2))))
虽然不常见,但make-add-suffix的结果可以直接调用,就像这样:
> ((make-add-suffix "!") "hello") "hello!"
从某种意义上说,make-add-suffix是一个函数,接受两个参数,但一次只接受一个参数。一个函数,它接受它的参数的一些并返回一个函数以接受更多,这种函数有时被称为一个柯里函数(curried function)。
使用define的函数简写表,make-add-suffix可以等效地编写为:
(define (make-add-suffix s2) (lambda (s) (string-append s s2)))
这个简写反映了函数调用(make-add-suffix "!")的形态。define表更进一步支持定义反映嵌套函数调用的柯里函数的一个简写:
(define ((make-add-suffix s2) s) (string-append s s2))
> ((make-add-suffix "!") "hello") "hello!"
(define louder (make-add-suffix "!")) (define less-sure (make-add-suffix "?"))
> (less-sure "really") "really?"
> (louder "really") "really!"
用于define的函数简写的完整语法如下所示:
(define (head args) body ...+)
head = id | (head args) args = arg ... | arg ... . rest-id
这个简写的扩展有一个给定义中的每个head的嵌套lambda表,其最里面的head与最外面的lambda对应。
一个Racket表达式通常产生一个单独的结果,但有些表达式可以产生多个结果。例如,quotient和remainder各自产生一个值,但quotient/remainder同时产生同样的两个值:
> (quotient 13 3) 4
> (remainder 13 3) 1
> (quotient/remainder 13 3)
4
1
如上所示,REPL在自己的行打印每一结果值。
多值函数可以依据values函数来实现,它接受任意数量的值并将它们作为结果返回:
> (values 1 2 3)
1
2
3
(define (split-name name) (let ([parts (regexp-split " " name)]) (if (= (length parts) 2) (values (list-ref parts 0) (list-ref parts 1)) (error "not a )))) name"
> (split-name "Adam Smith")
"Adam"
"Smith"
define-values表同时绑定多个标识符到产生于一个单表达式的多个结果:
(define-values (id ...) expr)
由expr产生的结果数量必须与id的数量相匹配。
(define-values (given surname) (split-name "Adam Smith")) > given "Adam"
> surname "Smith"
一个define表(不是一个函数简写)等价于一个带有一个单个id的define-values表。
在《Racket参考》中的“(define)”部分提供了更多关于定义的内容。
当一个句法表的语法指定body,那相应的表可以是一个定义或一个表达式。作为一个body的一个定义是一个内部定义(internal definition)。
只要最后一个body是表达式,在一个body序列中的表达式和内部定义可以被混合。
例如, lambda的语法是:
(lambda gen-formals body ...+)
所以下面是语法的有效实例:
(lambda (f) ; 没有定义 (printf "running\n") (f 0)) (lambda (f) ; 一个定义 (define (log-it what) (printf "~a\n" what)) (log-it "running") (f 0) (log-it "done")) (lambda (f n) ; 两个定义 (define (call n) (if (zero? n) (log-it "done") (begin (log-it "running") (f n) (call (- n 1))))) (define (log-it what) (printf "~a\n" what)) (call n))
在一个特定的body序列中的内部定义是相互递归的,也就是说,任何定义都可以引用任何其它定义——只要这个引用在定义发生之前没有实际被求值。如果一个定义被过早引用,一个错误就会发生。
(define (weird) (define x x) x) > (weird) x: undefined;
cannot use before initialization
内部定义的一个序列只使用define很容易转换为一个等效的letrec表(如同在下一节中介绍的)。然而,其它的定义表可以表现为一个body,包括define-values、struct(见《程序员定义的数据类型》)或define-syntax(见《宏》)。
在《Racket参考》文档的“(intdef-body)”部分有内部定义更多知识点。
虽然内部define可用于局部绑定,Racket提供了三种表,它们给予程序员在绑定方面的更多控制:let、let*和letrec。
在《Racket参考》的“(let)”部分也有关于let的文档。
一个let表绑定一组标识符,每个对应某个表达式的结果,以在let主体中使用:
(let ([id expr] ...) body ...+)
id绑定”在并行(parallel)状态中”。也就是说,在右手边的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是按顺序求值的。
在《Racket参考》的“(let)”部分也有关于let*的文档。
let*的语法和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")
在《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)) #f
当一个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绑定一个只在函数主体中可见的函数标识符,并且用一些初始表达式的值隐式调用函数。
(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")
在《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。
> (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。
> (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表达式的结果是#
> (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必须是有一个参数的一个函数。
> (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表来说位于尾部位置。
(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。
(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的上下文被拼接到周围的上下文中,而不是形成一个表达式。
> (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表对于实现发生在一个计算之后的副作用是有用的,尤其是在计算产生结果的一个未知数值的情况下。
(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: 1531057885852.895
End..: 1531057885952.937
0
> (log-times (lambda () (values 1 2)))
Start: 1531057885953.197
End..: 1531057885953.242
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被求值而且结果是#
unless是相似的:
(unless test-expr then-body ...+)
不同的是test-expr结果是相反的:如果test-expr结果为#f,then-body被求值。
(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)
****
***
**
*
在《Racket参考》的“(set!)”中也有关于set!的文档。
使用set!赋值给一个变量:
(set! id expr)
一个set!表达式对expr求值并改变id(它必须限制在闭括号的环境内)为这个结果值。set!表达式自己的结果是#
(define greeted null)
(define (greet name) (set! greeted (cons name greeted)) (string-append "Hello, " name)) > (greet "Athos") "Hello, Athos"
> (greet "Porthos") "Hello, Porthos"
> (greet "Aramis") "Hello, Aramis"
> greeted '("Aramis" "Porthos" "Athos")
(define (make-running-total) (let ([n 0]) (lambda () (set! n (+ n 1)) n))) (define win (make-running-total)) (define lose (make-running-total))
> (win) 1
> (win) 2
> (lose) 1
> (win) 3
虽然使用set!有时是适当的,Racket风格通常不建议使用set!。下面的指导原则有助于解释什么时候使用set!是适当的。
与任何现代语言一样,赋值给一个共享标识符不是用传递一个参数给一个过程或取得其结果的替换。
(define name "unknown") (define result "unknown") (define (greet) (set! result (string-append "Hello, " name)))
> (set! name "John") > (greet) > result "Hello, John"
(define (greet name) (string-append "Hello, " name))
> (greet "John") "Hello, John"
> (greet "Anna") "Hello, Anna"
对一个局部变量的一系列赋值远差于嵌套绑定。
> (let ([tree 0]) (set! tree (list tree 1 tree)) (set! tree (list tree 2 tree)) (set! tree (list tree 3 tree)) tree) '(((0 1 0) 2 (0 1 0)) 3 ((0 1 0) 2 (0 1 0)))
> (let* ([tree 0] [tree (list tree 1 tree)] [tree (list tree 2 tree)] [tree (list tree 3 tree)]) tree) '(((0 1 0) 2 (0 1 0)) 3 ((0 1 0) 2 (0 1 0)))
使用赋值来从一个迭代中积累结果是不好的风格。通过一个循环参数积累更好。
(define (sum lst) (let ([s 0]) (for-each (lambda (i) (set! s (+ i s))) lst) s))
> (sum '(1 2 3)) 6
(define (sum lst) (let loop ([lst lst] [s 0]) (if (null? lst) s (loop (cdr lst) (+ s (car lst))))))
> (sum '(1 2 3)) 6
(define (sum lst) (apply + lst))
> (sum '(1 2 3)) 6
(define (sum lst) (for/fold ([s 0]) ([i (in-list lst)]) (+ s i)))
> (sum '(1 2 3)) 6
对于在有状态对象是必要或合适的情况下,那么用set!实现对象的状态很好的。
(define next-number! (let ([n 0]) (lambda () (set! n (add1 n)) n)))
> (next-number!) 1
> (next-number!) 2
> (next-number!) 3
所有其它的情况都相同,不使用赋值或突变的一个程序总是优于使用赋值或突变的一个程序。虽然副作用应该被避免,然而,如果结果代码可读性明显更高,或者如果实现了一个明显更好的算法,则应该使用这些副作用。
可突变值的使用,如向量和哈希表,对一个程序的风格提出了比直接使用set!更少的怀疑。不过,在一个用vector-set!的程序中简单替换set!显然没有改善程序的风格。
在《Racket参考》中的“(set!)”有关于set!-values的文档。
set!-values表一次赋值给多个变量,给一个生成值的一个适当数值的表达式:
(set!-values (id ...) expr)
这个表等价于使用let-values以从expr接收多个结果,然后将结果使用set!单独赋值给id。
(define game (let ([w 0] [l 0]) (lambda (win?) (if win? (set! w (+ w 1)) (set! l (+ l 1))) (begin0 (values w l) ; 交换双方…… (set!-values (w l) (values l w)))))) > (game #t)
1
0
> (game #t)
1
1
> (game #f)
1
2
在《Racket参考》中“(quote)”部分也有关于quote的文档。
quote表产生一个常数:
(quote datum)
datum的语法在技术上被指定为read函数解析为一个单个元素的任何内容。quote表的值是相同的值,其read将产生给定的datum。
datum可以是一个符号、一个布尔值、一个数值、一个(字符或字节)字符串、一个字符、一个关键字、一个空列表、一个包含更多类似值的点对(或列表),一个包含更多类似值的向量,一个包含更多类似值的哈希表,或者一个包含其它类似值的格子。
> (quote apple) 'apple
> (quote #t) #t
> (quote 42) 42
> (quote "hello") "hello"
> (quote ()) '()
> (quote ((1 2 3) #("z" x) . the-end)) '((1 2 3) #("z" x) . the-end)
> (quote (1 2 . (3))) '(1 2 3)
正如上面最后的示例所示,datum不需要匹配一个值的格式化的打印表。一个datum不能作为一个以#<开始的打印呈现,所以不能是#
quote表很少用于一个datum,它是一个布尔值、数字或字符串本身,因为这些值的打印表已经可以用作常量。quote表更常用于符号和列表,当没有被引用时,它具有其它含义(标识符、函数调用等等)。
一个表达式:
'datum
是以下内容的一个简写
(quote datum)
这个简写几乎总是用来代替quote。这个简写甚至应用于datum中,因此它可以生成一个包含quote的列表。
在《Racket参考》里的“(parse-quote)”提供有更多关于'简写的内容。
> 'apple 'apple
> '"hello" "hello"
> '(1 2 3) '(1 2 3)
> (display '(you can 'me)) (you can (quote me))
在《Racket参考》中的“(quasiquote)”部分也有关于quasiquote的文档。
quasiquote表类似于quote:
(quasiquote datum)
然而,对出现在datum之中的每个(unquote expr),expr被求值以产生一个替代unquote子表的值。
> (quasiquote (1 2 (unquote (+ 1 2)) (unquote (- 5 1)))) '(1 2 3 4)
此表可用于编写根据特定模式建造列表的函数。
> (define (deep n) (cond [(zero? n) 0] [else (quasiquote ((unquote n) (unquote (deep (- n 1)))))])) > (deep 8) '(8 (7 (6 (5 (4 (3 (2 (1 0))))))))
甚至可以以编程方式方便地构造表达式。(当然,第9次就超出了10次,你应该使用一个《宏》来做这个(第10次是当你学习了一本像《PLAI》那样的教科书之后)。)
> (define (build-exp n) (add-lets n (make-sum n)))
> (define (add-lets n body) (cond [(zero? n) body] [else (quasiquote (let ([(unquote (n->var n)) (unquote n)]) (unquote (add-lets (- n 1) body))))]))
> (define (make-sum n) (cond [(= n 1) (n->var 1)] [else (quasiquote (+ (unquote (n->var n)) (unquote (make-sum (- n 1)))))])) > (define (n->var n) (string->symbol (format "x~a" n))) > (build-exp 3) '(let ((x3 3)) (let ((x2 2)) (let ((x1 1)) (+ x3 (+ x2 x1)))))
unquote-splicing表和unquote相似,但其expr必须产生一个列表,而且unquote-splicing表必须出现在一个产生一个列表或一个向量的上下文里。顾名思义,这个结果列表被拼接到它自己使用的上下文中。
> (quasiquote (1 2 (unquote-splicing (list (+ 1 2) (- 5 1))) 5)) '(1 2 3 4 5)
使用拼接,我们可以修改上边我们的示例表达式的构造,以只需要一个单个的let表达式和一个单个的+表达式。
> (define (build-exp n) (add-lets n (quasiquote (+ (unquote-splicing (build-list n (λ (x) (n->var (+ x 1)))))))))
> (define (add-lets n body) (quasiquote (let (unquote (build-list n (λ (n) (quasiquote [(unquote (n->var (+ n 1))) (unquote (+ n 1))])))) (unquote body)))) > (define (n->var n) (string->symbol (format "x~a" n))) > (build-exp 3) '(let ((x1 1) (x2 2) (x3 3)) (+ x1 x2 x3))
如果一个quasiquote表出现在一个封闭的quasiquote表里,那这个内部的quasiquote有效地取消unquote表和unquote-splicing表的一层,结果一个第二层unquote或unquote-splicing表被需要。
> (quasiquote (1 2 (quasiquote (unquote (+ 1 2))))) '(1 2 (quasiquote (unquote (+ 1 2))))
> (quasiquote (1 2 (quasiquote (unquote (unquote (+ 1 2)))))) '(1 2 (quasiquote (unquote 3)))
> (quasiquote (1 2 (quasiquote ((unquote (+ 1 2)) (unquote (unquote (- 5 1))))))) '(1 2 (quasiquote ((unquote (+ 1 2)) (unquote 4))))
上面的求值实际上不会像显示那样打印。相反,quasiquote和unquote的速记形式将被使用:`(即一个反引号)和,(即一个逗号)。同样的简写可在表达式中使用:
> `(1 2 `(,(+ 1 2) ,,(- 5 1))) '(1 2 `(,(+ 1 2) ,4))
unquote-splicing的简写形式是,@:
> `(1 2 ,@(list (+ 1 2) (- 5 1))) '(1 2 3 4)
通过将一个表达式的结果与子句的值相匹配,case表分派到一个从句:
(case expr [(datum ...+) body ...+] ...)
每个datum将使用equal?对比expr的结果,然后相应的body被求值。case表可以为N个datum在O(log N)时间内分派正确的从句。
可以给每个从句提供多个datum,而且如果任何一个datum匹配,那么相应的body被求值。
> (let ([v (random 6)]) (printf "~a\n" v) (case v [(0) 'zero] [(1) 'one] [(2) 'two] [(3 4 5) 'many])) 0
'zero
一个case表的最后从句可以使用else,就像cond那样:
> (case (random 6) [(0) 'zero] [(1) 'one] [(2) 'two] [else 'many]) 'two
对于更一般的模式匹配(但没有分派时间保证),使用match,这个会在《模式匹配》中介绍。
在《Racket参考》中的“(parameterize)”部分也有关于parameterize的文档。
parameterize表把一个新值和body表达式的求值过程中的一个参数parameter相结合:
(parameterize ([parameter-expr value-expr] ...) body ...+)
术语“参数”有时用于指一个函数的参数,但Racket中的“参数”在这里有更具体的意义描述。
例如,error-print-width参数控制在错误消息中打印一个值的字符数:
> (parameterize ([error-print-width 5]) (car (expt 10 1024))) car: contract violation
expected: pair?
given: 10...
> (parameterize ([error-print-width 10]) (car (expt 10 1024))) car: contract violation
expected: pair?
given: 1000000...
一般来说,参数实现了一种动态绑定。make-parameter函数接受任何值并返回一个初始化为给定值的新参数。应用参数作为一个函数返回它的当前值:
> (define location (make-parameter "here")) > (location) "here"
在一个parameterize表里,每个parameter-expr必须产生一个参数。在body的求值过程中,每一个指定的参数给出对应于value-expr的结果。当控制离开parameterize表——无论是通过一个正常的返回、一个例外或其它逃逸——这个参数恢复到其先前的值:
> (parameterize ([location "there"]) (location)) "there"
> (location) "here"
> (parameterize ([location "in a house"]) (list (location) (parameterize ([location "with a mouse"]) (location)) (location))) '("in a house" "with a mouse" "in a house")
> (parameterize ([location "in a box"]) (car (location))) car: contract violation
expected: pair?
given: "in a box"
> (location) "here"
parameterize表不是一个像let的绑定表;每次上边location的使用都直接指向原始的定义。在parameterize主体被求值的整个时间内,一个parameterize表调整一个参数的值,即使对于参数的使用是在parameterize主体以外符合文本:
> (define (would-you-could-you?) (and (not (equal? (location) "here")) (not (equal? (location) "there")))) > (would-you-could-you?) #f
> (parameterize ([location "on a bus"]) (would-you-could-you?)) #t
如果一个参数的一个使用是在一个parameterize的主体内部符合文本,但是在parameterize表产生一个值之前没有被求值,那么这个使用没有明白这个被parameterize表所设置的值:
> (let ([get (parameterize ([location "with a fox"]) (lambda () (location)))]) (get)) "here"
一个参数的当前绑定可以通过用一个值作为一个函数调用这个参数来做必要的调整。如果一个parameterize已经调整了参数的值,那么直接应用参数过程仅仅影响与活动parameterize相关的值:
> (define (try-again! where) (location where)) > (location) "here"
> (parameterize ([location "on a train"]) (list (location) (begin (try-again! "in a boat") (location)))) '("on a train" "in a boat")
> (location) "here"
使用parameterize通常更适合于强制更新一个参数值——对于用let绑定一个新变量的大多数相同原因是更好地使用set! (参见《赋值:set!》)。
似乎变量和set!可以解决很多参数解决的相同问题。例如,lokation可以被定义为一个字符串,以及set!可以用来调整它的值:
> (define lokation "here")
> (define (would-ya-could-ya?) (and (not (equal? lokation "here")) (not (equal? lokation "there")))) > (set! lokation "on a bus") > (would-ya-could-ya?) #t
然而,参数比set!提供了几个关键的优点:
parameterize表有助于在控制因一个异常导致的逃逸时自动重置一个参数的值。 添加异常处理器和其它表以重绕一个set!是比较繁琐的。
参数可以和尾调用很好地工作(请参阅《尾递归》)。在一个parameterize表最后的body相对于parameterize表来说是处于尾部位置。
参数与线程恰当地工作(请参阅《Racket参考》"(threads)"部分)。parameterize表仅因为在当前线程中的求值而调整一个参数的值,以避免与其它线程竞争。