11 Iterations and Comprehensions
for 系列语法形式支持对 sequence(序列)进行迭代。列表、vector、字符串、字节字符串、输入 port 和 hash table 都可以用作序列,而像 in-range 这样的构造器还提供了更多种类的序列。
for 的变体以不同方式累积迭代结果,但它们都具有相同的语法形状。简化的 for 语法为
(for ([id sequence-expr] ...) body ...+)
for 循环遍历 sequence-expr 产生的序列。对于序列中的每个元素,for 将元素绑定到 id,然后对 body 求值以获取副作用。
> (for ([i '(1 2 3)]) (display i)) 123
> (for ([i "abc"]) (printf "~a..." i)) a...b...c...
> (for ([i 4]) (display i)) 0123
for 的 for/list 变体更符合 Racket 风格。它将 body 结果累积到列表中,而不是仅为副作用而对 body 求值。用更技术性的术语来说,for/list 实现了一个 list comprehension(列表推导)。
> (for/list ([i '(1 2 3)]) (* i i)) '(1 4 9)
> (for/list ([i "abc"]) i) '(#\a #\b #\c)
> (for/list ([i 4]) i) '(0 1 2 3)
for 的完整语法支持多个序列并行迭代,而 for* 变体将迭代嵌套而非并行运行。for 和 for* 的更多变体以不同方式累积 body 结果。在所有这些变体中,可以在绑定中包含修剪迭代的谓词。
不过,在详细介绍 for 的各种变体之前,最好先看看能产生有趣示例的序列生成器。
11.1 Sequence Constructors
in-range 函数生成一个数字序列,接受一个可选的起始数(默认为 0)、一个序列结束的数(不含该数)和一个可选的步长(默认为 1)。直接使用非负整数 k 作为序列是 (in-range k) 的简写。
> (for ([i 3]) (display i)) 012
> (for ([i (in-range 3)]) (display i)) 012
> (for ([i (in-range 1 4)]) (display i)) 123
> (for ([i (in-range 1 4 2)]) (display i)) 13
> (for ([i (in-range 4 1 -1)]) (display i)) 432
> (for ([i (in-range 1 4 1/2)]) (printf " ~a " i)) 1 3/2 2 5/2 3 7/2
in-naturals 函数类似,但起始数必须是精确非负整数(默认为 0),步长始终为 1,且没有上限。仅使用 in-naturals 的 for 循环将永远不会终止,除非主体表达式引发异常或以其他方式逃逸。
> (for ([i (in-naturals)]) (if (= i 10) (error "too much!") (display i))) 0123456789
too much!
stop-before 和 stop-after 函数给定一个序列和一个谓词,构造一个新序列。新序列类似于给定序列,但在谓词返回 true 的第一个元素之前或之后截断。
> (for ([i (stop-before "abc def" char-whitespace?)]) (display i)) abc
像 in-list、in-vector 和 in-string 这样的序列构造器只是显式地将列表、vector 或字符串用作序列。与 in-range 一起,这些构造器在给定错误类型的值时会引发异常,并且由于它们避免了运行时分派来确定序列类型,因此能实现更高效的代码生成;更多信息请参见 Iteration Performance。
> (for ([i (in-string "abc")]) (display i)) abc
> (for ([i (in-string '(1 2 3))]) (display i)) in-string: contract violation
expected: string?
given: '(1 2 3)
(part ("(lib scribblings/reference/reference.scrbl)" "sequences")) in (part ("(lib scribblings/reference/reference.scrbl)" "top")) provides more on sequences.
11.2 for and for*
for 的更完整语法为
(for (clause ...) body ...+)
clause = [id sequence-expr] | #:when boolean-expr | #:unless boolean-expr
当在 for 形式中提供多个 [id sequence-expr] 子句时,对应的序列将并行遍历:
> (for ([i (in-range 1 4)] [chapter '("Intro" "Details" "Conclusion")]) (printf "Chapter ~a. ~a\n" i chapter))
Chapter 1. Intro
Chapter 2. Details
Chapter 3. Conclusion
使用并行序列时,for 表达式在任何序列结束时停止迭代。这种行为允许创建无限数字序列的 in-naturals 用于索引:
> (for ([i (in-naturals 1)] [chapter '("Intro" "Details" "Conclusion")]) (printf "Chapter ~a. ~a\n" i chapter))
Chapter 1. Intro
Chapter 2. Details
Chapter 3. Conclusion
for* 形式与 for 语法相同,但将多个序列嵌套而非并行运行:
> (for* ([book '("Guide" "Reference")] [chapter '("Intro" "Details" "Conclusion")]) (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion
Reference Intro
Reference Details
Reference Conclusion
因此,for* 是嵌套 for 的简写,就像 let* 是嵌套 let 的简写一样。
clause 的 #:when boolean-expr 形式是另一种简写。它仅在 boolean-expr 产生 true 值时才允许对 body 求值:
> (for* ([book '("Guide" "Reference")] [chapter '("Intro" "Details" "Conclusion")] #:when (not (equal? chapter "Details"))) (printf "~a ~a\n" book chapter))
Guide Intro
Guide Conclusion
Reference Intro
Reference Conclusion
带有 #:when 的 boolean-expr 可以引用前面的任何迭代绑定。在 for 形式中,这种作用域只有在测试嵌套在前面绑定的迭代中时才有意义;因此,被 #:when 分隔的绑定是相互嵌套的,即使在 for 中也不是并行的。
> (for ([book '("Guide" "Reference" "Notes")] #:when (not (equal? book "Notes")) [i (in-naturals 1)] [chapter '("Intro" "Details" "Conclusion" "Index")] #:when (not (equal? chapter "Index"))) (printf "~a Chapter ~a. ~a\n" book i chapter))
Guide Chapter 1. Intro
Guide Chapter 2. Details
Guide Chapter 3. Conclusion
Reference Chapter 1. Intro
Reference Chapter 2. Details
Reference Chapter 3. Conclusion
#:unless 子句类似于 #:when 子句,但 body 仅在 boolean-expr 产生 false 值时才求值。
11.3 for/list and for*/list
for/list 形式与 for 语法相同,对 body 求值以获得的值放入新构造的列表中:
> (for/list ([i (in-naturals 1)] [chapter '("Intro" "Details" "Conclusion")]) (string-append (number->string i) ". " chapter)) '("1. Intro" "2. Details" "3. Conclusion")
for-list 形式中的 #:when 子句在对 body 求值的同时修剪结果列表:
> (for/list ([i (in-naturals 1)] [chapter '("Intro" "Details" "Conclusion")] #:when (odd? i)) chapter) '("Intro" "Conclusion")
#:when 的这种修剪行为在 for/list 中比在 for 中更有用。虽然普通的 when 形式在 for 中通常就够了,但 for/list 中的 when 表达式形式会导致结果列表包含 #<void> 而非省略列表元素。
for*/list 形式类似于 for*,将多个迭代嵌套:
> (for*/list ([book '("Guide" "Ref.")] [chapter '("Intro" "Details")]) (string-append book " " chapter)) '("Guide Intro" "Guide Details" "Ref. Intro" "Ref. Details")
for*/list 形式与嵌套的 for/list 形式不完全相同。嵌套的 for/list 会产生列表的列表,而非一个扁平化的列表。因此,与 #:when 类似,for*/list 的嵌套比 for* 的嵌套更有用。
11.4 for/vector and for*/vector
for/vector 形式可以使用与 for/list 形式相同的语法,但求值后的 body 进入新构造的 vector 而非列表:
> (for/vector ([i (in-naturals 1)] [chapter '("Intro" "Details" "Conclusion")]) (string-append (number->string i) ". " chapter)) '#("1. Intro" "2. Details" "3. Conclusion")
for*/vector 形式的行为类似,但迭代像 for* 一样嵌套。
for/vector 和 for*/vector 形式还允许预先提供要构造的 vector 长度。由此产生的迭代可以比普通的 for/vector 或 for*/vector 更高效地执行:
> (let ([chapters '("Intro" "Details" "Conclusion")]) (for/vector #:length (length chapters) ([i (in-naturals 1)] [chapter chapters]) (string-append (number->string i) ". " chapter))) '#("1. Intro" "2. Details" "3. Conclusion")
如果提供了长度,迭代在 vector 填满或请求的迭代完成时停止(以先到者为准)。如果提供的长度超过了请求的迭代次数,则 vector 中的剩余槽位将初始化为 make-vector 的默认参数。
11.5 for/and and for/or
for/and 形式使用 and 组合迭代结果,遇到 #f 时立即停止:
> (for/and ([chapter '("Intro" "Details" "Conclusion")]) (equal? chapter "Intro")) #f
for/or 形式使用 or 组合迭代结果,遇到 true 值时立即停止:
> (for/or ([chapter '("Intro" "Details" "Conclusion")]) (equal? chapter "Intro")) #t
通常,for*/and 和 for*/or 形式在嵌套迭代中提供相同的功能。
11.6 for/first and for/last
for/first 形式返回 body 首次求值的结果,跳过后续迭代。此形式与 #:when 子句配合使用最为有用。
> (for/first ([chapter '("Intro" "Details" "Conclusion" "Index")] #:when (not (equal? chapter "Intro"))) chapter) "Details"
如果 body 被求值零次,则结果为 #f。
for/last 形式运行所有迭代,返回最后一次迭代的值(如果没有迭代运行则返回 #f):
> (for/last ([chapter '("Intro" "Details" "Conclusion" "Index")] #:when (not (equal? chapter "Index"))) chapter) "Conclusion"
通常,for*/first 和 for*/last 形式在嵌套迭代中提供相同的功能:
> (for*/first ([book '("Guide" "Reference")] [chapter '("Intro" "Details" "Conclusion" "Index")] #:when (not (equal? chapter "Intro"))) (list book chapter)) '("Guide" "Details")
> (for*/last ([book '("Guide" "Reference")] [chapter '("Intro" "Details" "Conclusion" "Index")] #:when (not (equal? chapter "Index"))) (list book chapter)) '("Reference" "Conclusion")
11.7 for/fold and for*/fold
for/fold 形式是组合迭代结果的非常通用的方式。它的语法与 for 略有不同,因为累积变量必须在开头声明:
(for/fold ([accum-id init-expr] ...) (clause ...) body ...+)
在简单情况下,只提供一个 [accum-id init-expr],for/fold 的结果是 accum-id 的最终值,它以 init-expr 的值开始。在 clause 和 body 中,可以引用 accum-id 来获取其当前值,最后一个 body 为下一次迭代提供 accum-id 的值。
> (for/fold ([len 0]) ([chapter '("Intro" "Conclusion")]) (+ len (string-length chapter))) 15
> (for/fold ([prev #f]) ([i (in-naturals 1)] [chapter '("Intro" "Details" "Details" "Conclusion")] #:when (not (equal? chapter prev))) (printf "~a. ~a\n" i chapter) chapter)
1. Intro
2. Details
4. Conclusion
"Conclusion"
当指定多个 accum-id 时,最后一个 body 必须产生多个值,每个 accum-id 一个。for/fold 表达式本身产生多个结果值。
> (for/fold ([prev #f] [counter 1]) ([chapter '("Intro" "Details" "Details" "Conclusion")] #:when (not (equal? chapter prev))) (printf "~a. ~a\n" counter chapter) (values chapter (add1 counter)))
1. Intro
2. Details
3. Conclusion
"Conclusion"
4
11.8 Multiple-Valued Sequences
正如函数或表达式可以产生多个值一样,序列的单次迭代也可以产生多个元素。例如,hash table 作为序列每次迭代产生两个值:一个键和一个值。
正如 let-values 将多个结果绑定到多个标识符,for 可以将多个序列元素绑定到多个迭代标识符:
虽然 let 必须更改为 let-values 才能绑定多个标识符,但 for 只需在任何子句中允许使用括号括起来的标识符列表而非单个标识符。
> (for ([(k v) #hash(("apple" . 1) ("banana" . 3))]) (printf "~a count: ~a\n" k v))
apple count: 1
banana count: 3
这种对多值绑定的扩展适用于所有 for 变体。例如,for*/list 嵌套迭代、构建列表,并且也适用于多值序列:
> (for*/list ([(k v) #hash(("apple" . 1) ("banana" . 3))] [(i) (in-range v)]) k) '("apple" "banana" "banana" "banana")
11.9 Breaking an Iteration
for 的更完整语法为
(for (clause ...) body-or-break ... body)
clause = [id sequence-expr] | #:when boolean-expr | #:unless boolean-expr | break body-or-break = body | break break = #:break boolean-expr | #:final boolean-expr
也就是说,#:break 或 #:final 子句可以包含在迭代的绑定子句和主体中。在绑定子句中,#:break 类似于 #:unless,但当其 boolean-expr 为 true 时,for 内的所有序列都会停止。在 body 中,#:break 在其 boolean-expr 为 true 时对序列有同样的效果,并且它还会阻止当前迭代中后续 body 的求值。
例如,虽然在子句之间使用 #:unless 可以有效地跳过后续序列和主体,
> (for ([book '("Guide" "Story" "Reference")] #:unless (equal? book "Story") [chapter '("Intro" "Details" "Conclusion")]) (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion
Reference Intro
Reference Details
Reference Conclusion
但使用 #:break 会导致整个 for 迭代终止:
> (for ([book '("Guide" "Story" "Reference")] #:break (equal? book "Story") [chapter '("Intro" "Details" "Conclusion")]) (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion
> (for* ([book '("Guide" "Story" "Reference")] [chapter '("Intro" "Details" "Conclusion")]) #:break (and (equal? book "Story") (equal? chapter "Conclusion")) (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion
Story Intro
Story Details
#:final 子句类似于 #:break,但它不会立即终止迭代。相反,它允许每个序列最多再抽取一个元素,并且最多再对 body 进行一次求值。
> (for* ([book '("Guide" "Story" "Reference")] [chapter '("Intro" "Details" "Conclusion")]) #:final (and (equal? book "Story") (equal? chapter "Conclusion")) (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion
Story Intro
Story Details
Story Conclusion
> (for ([book '("Guide" "Story" "Reference")] #:final (equal? book "Story") [chapter '("Intro" "Details" "Conclusion")]) (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion
Story Intro
11.10 Iteration Performance
理想情况下,for 迭代应该与手写的递归函数调用循环一样快。然而,手写循环通常针对特定类型的数据(如列表)。在这种情况下,手写循环直接使用像 car 和 cdr 这样的选择器,而不是处理所有形式的序列并分派到适当的迭代器。
当关于要迭代的序列有足够明显的信息时,for 形式可以提供手写循环的性能,特别是当子句具有以下 fast-clause 形式之一时:
| fast-clause | = | [id fast-seq] | ||
| | | [(id) fast-seq] | |||
| | | [(id id) fast-indexed-seq] | |||
| | | [(id ...) fast-parallel-seq] |
| fast-seq | = | literal | ||
| | | (in-range expr) | |||
| | | (in-range expr expr) | |||
| | | (in-range expr expr expr) | |||
| | | (in-inclusive-range expr expr) | |||
| | | (in-inclusive-range expr expr expr) | |||
| | | (in-naturals) | |||
| | | (in-naturals expr) | |||
| | | (in-list expr) | |||
| | | (in-mlist expr) | |||
| | | (in-vector expr) | |||
| | | (in-string expr) | |||
| | | (in-bytes expr) | |||
| | | (in-value expr) | |||
| | | (stop-before fast-seq predicate-expr) | |||
| | | (stop-after fast-seq predicate-expr) |
| fast-indexed-seq | = | (in-indexed fast-seq) | ||
| | | (stop-before fast-indexed-seq predicate-expr) | |||
| | | (stop-after fast-indexed-seq predicate-expr) |
| fast-parallel-seq | = | (in-parallel fast-seq ...) | ||
| | | (stop-before fast-parallel-seq predicate-expr) | |||
| | | (stop-after fast-parallel-seq predicate-expr) |
> (define lst '(a b c d e f g h))
> (time (for ([i (in-range 100000)]) (for ([elem (in-list lst)]) ; fast (void)))) cpu time: 1 real time: 1 gc time: 0
> (time (for ([i (in-range 100000)]) (for ([elem '(a b c d e f g h)]) ; also fast (void)))) cpu time: 1 real time: 1 gc time: 0
> (time (for ([i (in-range 100000)]) (for ([elem lst]) ; slower (void)))) cpu time: 5 real time: 5 gc time: 0
> (time (let ([seq (in-list lst)]) (for ([i (in-range 100000)]) (for ([elem seq]) ; also slower (void))))) cpu time: 8 real time: 8 gc time: 0
上述语法并不完整,因为提供良好性能的语法模式集合是可扩展的,就像序列值的集合一样。序列构造器的文档应说明直接在 for clause 中使用它的性能优势。
(part ("(lib scribblings/reference/reference.scrbl)" "for")) in (part ("(lib scribblings/reference/reference.scrbl)" "top")) provides more on iterations and comprehensions.