Racket指南

Racket指南

来源 https://blog.csdn.net/chinazhangyong/article/details/127872232

参考 https://github.com/OnRoadZy/RacketGuideInChinese

 

Racket指南

Matthew Flatt,
Robert Bruce Findler,
and PLT

本指南适用于新接触Racket的程序员或部分了解Racket的程序员。本指南假定你是有编程经验的。如果你是新学习编程,可以考虑阅读《如何设计程序》(How to Design Programs)。如果你想要一个对Racket特别快速的介绍,就从《快速:Racket的图片编程介绍》(Quick: An Introduction to Racket with Pictures)开始。

第2章提供了一个对Racket的简要介绍。从第3章开始,本指南深入细节——覆盖了大部分的Racket工具箱,但把更清晰的细节内容留给《Racket参考》(The Racket Reference)及其它参考手册介绍。

本手册源程序可查阅GitHub.

    1 欢迎来到Racket!

      1.1 用Racket进行交互

      1.2 定义和交互

      1.3 创建可执行程序

      1.4 给有LISP/Scheme经验的读者的一个说明

 

    2 Racket概要

      2.1 简单的值

      2.2 简单的定义和表达式

        2.2.1 定义

        2.2.2 缩进代码的提示

        2.2.3 标识

        2.2.4 函数调用(过程应用程序)

        2.2.5 带if、and、or和cond的条件句

        2.2.6 函数重复调用

        2.2.7 匿名函数与lambda

        2.2.8 用define、let和let*实现局部绑定

      2.3 列表、迭代和递归

        2.3.1 预定义列表循环

        2.3.2 从头开始列表迭代

        2.3.3 尾递归

        2.3.4 递归和迭代

      2.4 pair、list和Racket的语法

        2.4.1 用quote引用pair和symbol

        2.4.2 使用'缩写quote

        2.4.3 列表和Racket语法

 

    3 内置的数据类型

      3.1 布尔值(Boolean)

      3.2 数值(Number)

      3.3 字符(Character)

      3.4 字符串(Unicode)

      3.5 字节(Byte)和字节字符串(Byte String)

      3.6 符号(Symbol)

      3.7 关键字(Keyword)

      3.8 点对(Pair)和列表(List)

      3.9 向量(Vector)

      3.10 散列表(Hash Table)

      3.11 盒子

      3.12 无效值(Void)和未定义值(Undefined)

 

    4 表达式和定义

      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

 

    5 程序员定义的数据类型

      5.1 简单的结构类型:struct

      5.2 复制和更新

      5.3 结构子类型

      5.4 不透明结构类型与透明结构类型对比

      5.5 结构的比较

      5.6 结构类型的生成性

      5.7 预制结构类型

      5.8 更多的结构类型选项

 

    6 模块

      6.1 模块基础

        6.1.1 组织模块

        6.1.2 库集合

        6.1.3 包和集合

        6.1.4 添加集合

      6.2 模块语法

        6.2.1 module表

        6.2.2 #lang简写

        6.2.3 子模块

        6.2.4 main和test子模块

      6.3 模块路径

      6.4 输入:require

      6.5 输出:provide

      6.6 赋值和重定义

 

    7 合约

      7.1 合约和边界

        7.1.1 合约的违反

        7.1.2 合约与模块的测试

        7.1.3 嵌套合约边界测试

      7.2 函数的简单合约

        7.2.1 ->类型

        7.2.2 使用define/contract和 ->

        7.2.3 any和any/c

        7.2.4 运转你自己的合约

        7.2.5 高阶函数的合约

        7.2.6 带”???“的合约信息

        7.2.7 解析一个合约错误信息

      7.3 一般功能合约

        7.3.1 可选参数

        7.3.2 剩余参数

        7.3.3 关键字参数

        7.3.4 可选关键字参数

        7.3.5 case-lambda的合约

        7.3.6 参数和结果依赖

        7.3.7 检查状态变化

        7.3.8 多个结果值

        7.3.9 固定但静态未知数量

      7.4 合约:一个完整的例子

      7.5 结构上的合约

        7.5.1 确保一个特定值

        7.5.2 确保所有值

        7.5.3 检查数据结构的特性

      7.6 用#:exists和#:∃抽象合约

      7.7 附加实例

        7.7.1 一个客户管理器组建

        7.7.2 一个参数化(简单)栈

        7.7.3 一个字典

        7.7.4 一个队列

      7.8 建立新合约

        7.8.1 合约结构属性

        7.8.2 使所有警告和报警一致

      7.9 问题

        7.9.1 合约和eq?

        7.9.2 合约边界和define/contract

        7.9.3 存在的合约和判断

        7.9.4 定义递归合约

        7.9.5 混合set!和contract-out

 

    8 输入和输出

      8.1 端口的种类

      8.2 默认端口

      8.3 读写Racket数据

      8.4 数据类型和序列化

      8.5 字节、字符和编码

      8.6 I/O模式

 

    9 正则表达式

      9.1 编写正则表达式模式

      9.2 匹配正则表达式模式

      9.3 基本申明

      9.4 字符和字符类

        9.4.1 常用的字符类

        9.4.2 POSIX字符类

      9.5 量词

      9.6 簇

        9.6.1 反向引用

        9.6.2 非捕捉簇

        9.6.3 回廊

      9.7 替补

      9.8 回溯

      9.9 前寻与后寻

        9.9.1 前寻

        9.9.2 后寻

      9.10 一个扩展示例

 

    10 异常与控制

      10.1 异常

      10.2 提示和中止

      10.3 延续

 

    11 迭代和推导

      11.1 序列构造器

      11.2 for和for*

      11.3 for/list和for*/list

      11.4 for/vector and for*/vector

      11.5 for/and和for/or

      11.6 for/first和for/last

      11.7 for/fold和for*/fold

      11.8 多值序列

      11.9 打断迭代

      11.10 迭代性能

 

    12 模式匹配

 

    13 类和对象

      13.1 方法

      13.2 初始化参数

      13.3 内部和外部名称

      13.4 接口

      13.5 Final、Augment和Inner

      13.6 控制外部名称的范围

      13.7 混合

        13.7.1 混合和接口

        13.7.2 mixin表

        13.7.3 参数化的混合

      13.8 特征

        13.8.1 特征作为混合集

        13.8.2 特征里的继承与基类

        13.8.3 trait表

      13.9 类合约

        13.9.1 外部类合约

        13.9.2 内部类合约

 

    14 单元(组件)

      14.1 签名和单元

      14.2 调用单元

      14.3 链接单元

      14.4 一级单元

      14.5 完整的module签名和单元

      14.6 单元合约

        14.6.1 给签名添加合约

        14.6.2 给单元添加合约

      14.7 unit与module的比较

 

    15 反射和动态求值

      15.1 eval

        15.1.1 本地域

        15.1.2 名称空间

        15.1.3 名称空间和模块

      15.2 操纵名称空间

        15.2.1 创建和安装名称空间

        15.2.2 跨名称空间共享数据和代码

      15.3 脚本求值和使用load

 

    16 宏

      16.1 基于模式的宏

        16.1.1 define-syntax-rule

        16.1.2 词法范围

        16.1.3 define-syntax和syntax-rules

        16.1.4 序列的匹配

        16.1.5 标识宏

        16.1.6 set!转化器

        16.1.7 宏生成宏

        16.1.8 展开的例子:按引用调用函数

      16.2 通用宏转换器

        16.2.1 语法对象

        16.2.2 宏转化器程序

        16.2.3 混合模式和表达式:syntax-case

        16.2.4 with-syntax和generate-temporaries

        16.2.5 编译和运行时阶段

        16.2.6 一般阶段级别

          16.2.6.1 阶段和绑定

          16.2.6.2 阶段和模块

        16.2.7 语法污染

 

    17 创造语言

      17.1 模块语言

        17.1.1 隐式表绑定

        17.1.2 使用#lang s-exp

      17.2 读取器扩展

        17.2.1 源位置

        17.2.2 可读表

      17.3 定义新的#lang语言

        17.3.1 指定#lang语言

        17.3.2 使用#lang reader

        17.3.3 使用#lang s-exp syntax/module-reader

        17.3.4 安装语言

        17.3.5 源处理配置

        17.3.6 模块处理配置

 

    18 并发与同步

      18.1 线程

      18.2 线程邮箱

      18.3 信号

      18.4 通道

      18.5 缓冲异步通道

      18.6 可同步事件和sync

 

    19 性能

      19.1 DrRacket中的性能

      19.2 字节码和实时(JIT)编译器

      19.3 模块和性能

      19.4 函数调用优化

      19.5 突变和性能

      19.6 letrec性能

      19.7 Fixnum和Flonum优化

      19.8 未检查、不安全的操作

      19.9 外部指针

      19.10 正则表达式性能

      19.11 内存管理

      19.12 可访问性和垃圾回收

      19.13 弱盒及测试

      19.14 减少垃圾回收暂停

 

    20 并行

      20.1 前程并行

      20.2 现场并行

      20.3 分布式现场

 

    21 运行和创建可执行文件

      21.1 运行racket和gracket

        21.1.1 交互模式

        21.1.2 模块模式

        21.1.3 加载模式

      21.2 脚本

        21.2.1 Unix脚本

        21.2.2 Windows批处理文件

      21.3 创建独立可执行文件

 

    22 更多库

      22.1 图形和图形用户界面

      22.2 Web服务器

      22.3 使用外部库

      22.4 更多其它库

 

    23 Racket和Scheme的方言

      23.1 更多的Racket

      23.2 标准

        23.2.1 R5RS

        23.2.2 R6RS

      23.3 教学

 

    24 命令行工具和你的编辑器选择

      24.1 命令行工具

        24.1.1 同时编译和配置:raco

        24.1.2 交互式求值

        24.1.3 Shell补全

      24.2 Emacs

        24.2.1 主要模式

        24.2.2 小模式

        24.2.3 Evil模式的专有包

      24.3 Vim

      24.4 Sublime Text

 

    Bibliography

 

    Index

 

Racket概要

本章提供了一个对Racket的快速入门作为给这个指南余下部分的背景。有一些Racket经验的读者可以直接跳到《内置的数据类型》部分。

 

2.1 简单的值

Racket值包括数值、布尔值、字符串和字节字符串。在DrRacket和文档示例中(当你在着色状态下阅读文档时),值表达式显示为绿色。

数值(number)以通常的方式书写,包括分数和虚数:

数值(Number) (later in this guide) explains more about 数值(Numbers).

1       3.14
1/2     6.02e+23
1+2i    9999999999999999999999

布尔值(boolean)用#t表示真,用#f表示假。然而,在条件从句中,所有非#f值被视为真。

布尔值(Boolean) (later in this guide) explains more about 布尔值(boolean).

字符串(string)写在双引号("")之间。在一个字符串中,反斜杠(/)是一个转义字符;例如,一个反斜杠之后的一个双引号包括了字符串中的一个字面上的双引号。除了一个保留的双引号或反斜杠,任何Unicode字符都可以在字符串常量中出现。

字符串(Unicode) (later in this guide) explains more about 字符串(string).

"Hello, world!"
"Benjamin \"Bugsy\" Siegel"
"λx:(μα.α→α).xx"

当一个常量在REPL中被求值时,它通常打印与输入语法相同的结果。在某些情况下,打印格式是输入语法的一个标准化版本。在文档和在DrRacket的REPL中,结果打印为蓝色而不是绿色以突出打印结果与输入表达式之间的区别。

 

Examples:

> 1.0000

1.0

> "Bugs \u0022Figaro\u0022 Bunny"

"Bugs \"Figaro\" Bunny"

2.2 简单的定义和表达式

一个程序模块一般被写作

#lang ‹langname› ‹topform›*

topform›既是一个‹definition›也是一个‹expr›。REPL也对‹topform›求值。

在语法规范里,文本使用灰色背景,比如#lang,代表文本。除了(、)及[、]之前或之后不需要空格之外,文本与非结束符(像‹ID›)之间必须有空格。注释以;开始,直至这一行结束,空白也做相同处理。

《Racket参考》中的“(parse-comment)”提供有更多的有关注释的不同形式内容。

以后的内容遵从如下惯例:*在程序中表示零个或多个前面元素的重复,+表示一个或多个前面元素的重复,{} 组合一个序列作为一个元素的重复。

2.2.1 定义

表的一个定义:

定义:define (later in this guide) explains more about 定义.

( define ‹id› ‹expr› )

绑定‹id›到‹expr›的结果,而

( define ( ‹id› ‹id›* ) ‹expr›+ )

绑定第一个‹id›到一个函数(也叫一个程序),它通过余下的‹id›以参数作为命名。在函数情况下,该‹expr›是函数的函数体。当函数被调用时,它返回最后一个‹expr›的结果。

 

Examples:
(define pie 3)             ; 定义pie为3
 
(define (piece str)        ; 定义piece为一个
  (substring str 0 pie))   ; 带一个参数的函数
 
> pie

3

> (piece "key lime")

"key"

 

在底层,一个函数定义实际上与一个非函数定义相同,并且一个函数名不是不需在一个函数调用中使用。一个函数只是另一种类型的值,尽管打印形式不一定比数字或字符串的打印形式更完整。

 

Examples:
> piece

#<procedure:piece>

substring

#<procedure:substring>

 

一个函数定义能够包含函数体的多个表达式。在这种情况下,在调用函数时只返回最后一个表达式的值。其它表达式只对一些副作用进行求值,比如打印。

 

Examples:
(define (bake flavor)
  (printf "pre-heating oven...\n")
  (string-append flavor " pie"))
 
> (bake "apple")

pre-heating oven...

"apple pie"

 

Racket程序员更喜欢避免副作用,所以一个定义通常只有一个表达式。然而,重要是去懂得多个表达式在一个定义体内是被允许的,因为它解释了为什么以下nobake函数未在其结果中包含它的参数:

(define (nobake flavor)
  string-append flavor "jello")
 
> (nobake "green")

"jello"

在nobake中,没有圆括号在string-append flavor "jello"周围,那么它们是三个单独的表达式而不是一个函数调用表达式。表达式string-append和flavor被求值,但结果从未被使用。相反,该函数的结果仅是最终那个表达式的结果,"jello"。

2.2.2 缩进代码的提示

换行和缩进对于解析Racket程序来说并不重要,但大多数Racket程序员使用一套标准的约定来使代码更易读。例如,一个定义的主体通常在这个定义的第一行下面缩进。标识是在一个没有额外空格的括号内立即写出来的,而闭括号则从不自己独立一行。

当你在一个程序或REPL表达式里键入Enter(回车)键,DrRacket会根据标准风格自动缩进。例如,如果你在键入(define (greet name)后面敲击Enter,那么DrRacket自动为下一行插入两个空格。如果你改变了一个代码区域,你可以在DrRacket里选择它并敲击Tab键,那么DrRacket将重新缩进代码(没有插入任何换行)。象Emacs这样的编辑器提供一个带类似缩进支持的Racket或Scheme模式。

重新缩进不仅使代码更易于阅读,它还会以你希望的方式给你更多的反馈,象你的括号是否匹配等等。例如,如果你在给一个函数的最后参数之后遗漏了一个闭括号,则自动缩进在第一个参数下开始下一行,而不是在"define"关键字下:

(define (halfbake flavor
                  (string-append flavor " creme brulee")))

在这种情况下,缩进有助于突出错误。在其它情况下,当一个开括号没有匹配的闭括号,在缩进的地方可能是正常的,racket和DrRacket都使用源程序的缩进去提示一个括号可能丢失的地方。

2.2.3 标识

Racket对标识的语法是特别自由的。但排除以下特殊字符。

标识和绑定 (later in this guide) explains more about 标识.

   ( ) [ ] { } " , ' ` ; # | \

同时除了产生数字常数的字符序列,几乎任何非空白字符序列形成一个‹id›。例如substring是一个标识。另外,string-append和a+b是标识,而不是算术表达式。这里还有几个更多的例子:

+
Hfuhruhurr
integer?
pass/fail
john-jacob-jingleheimer-schmidt
a-b-c+1-2-3

2.2.4 函数调用(过程应用程序)

我们已经看到过许多函数调用,更传统的术语称之为过程应用程序。函数调用的语法是:

函数调用 (later in this guide) explains more about 函数调用.

( ‹id› ‹expr›* )

expr›的个数决定了提供给由‹id›命名的函数的参数个数。

racket语言预定义了许多函数标识,比如substringstring-append。下面有更多的例子。

在贯穿于整个文档的示例Racket代码中,预定义的名称的使用被链接到了参考手册(reference manual)。因此,你可以单击一个标识来获得关于其使用的完整详细资料。

> (string-append "rope" "twine" "yarn")  ; 添加字符串

"ropetwineyarn"

> (substring "corduroys" 0 4)            ; 提取子字符串

"cord"

> (string-length "shoelace")             ; 获取字符串长度

8

> (string? "Ceci n'est pas une string.") ; 识别字符串

#t

> (string? 1)

#f

> (sqrt 16)                              ; 找一个平方根

4

> (sqrt -16)

0+4i

> (+ 1 2)                                ; 数字相加

3

> (- 2 1)                                ; 数字相减

1

> (< 2 1)                                ; 数字比较

#f

> (>= 2 1)

#t

> (number? "c'est une number")           ; 识别数字

#f

> (number? 1)

#t

> (equal? 6 "half dozen")                ; 任意比较

#f

> (equal? 6 6)

#t

> (equal? "half dozen" "half dozen")

#t

2.2.5 ifandorcond的条件句

接下来最简单的表达式是if条件句:

( if ‹expr› ‹expr› ‹expr› )

条件 (later in this guide) explains more about 条件句.

第一个‹expr›总是被求值。如果它产生一个非#f值,那么第二个‹expr›被求值为整个if表达式的结果,否则第三个‹expr›被求值为结果。

 

Example:
> (if (> 2 3)
      "bigger"
      "smaller")

"smaller"

 

(define (reply s)
  (if (equal? "hello" (substring s 0 5))
      "hi!"
      "huh?"))
 
> (reply "hello racket")

"hi!"

> (reply "λx:(μα.α→α).xx")

"huh?"

复合的条件句可以由嵌套的if表达式构成。例如,当给定非字符串(non-strings)时,你可以编写reply函数来工作:

(define (reply s)
  (if (string? s)
      (if (equal? "hello" (substring s 0 5))
          "hi!"
          "huh?")
      "huh?"))

代替重复"huh?"事例,这个函数这样写会更好:

(define (reply s)
  (if (if (string? s)
          (equal? "hello" (substring s 0 5))
          #f)
      "hi!"
      "huh?"))

但这些嵌套的if很难阅读。Racket通过andor表提供了更易读的快捷表示,它可以和任意数量的表达式搭配:

组合测试:andor (later in this guide) explains more about and and or.

( and ‹expr›* )
( or ‹expr›* )

and表绕过情况:当一个表达式产生#f,它停止并返回#f,否则它继续运行。当or表遇到一个真的结果时,它同样的产生绕过情况。

 

Examples:
(define (reply s)
  (if (and (string? s)
           (>= (string-length s) 5)
           (equal? "hello" (substring s 0 5)))
      "hi!"
      "huh?"))
 
> (reply "hello racket")

"hi!"

> (reply 17)

"huh?"

 

嵌套if的另一种常见模式涉及测试的一个序列,每个测试都有自己的结果:

(define (reply-more s)
  (if (equal? "hello" (substring s 0 5))
      "hi!"
      (if (equal? "goodbye" (substring s 0 7))
          "bye!"
          (if (equal? "?" (substring s (- (string-length s) 1)))
              "I don't know"
              "huh?"))))

对测试的一个序列的快捷形式是cond表:

编链测试:cond (later in this guide) explains more about cond.

( cond {[ ‹expr› ‹expr›* ]}* )

一个cond表包含了括号之间的从句的一个序列。在每一个从句中,第一个‹expr›是一个测试表达式。如果它产生真值,那么从句的剩下‹expr›被求值,并且从句中的最后一个提供整个cond表达的答案,其余的从句被忽略。如果这个测试 ‹expr›产生#f,那么从句的剩余‹expr›被忽视,并继续下一个从句求值。最后的从句可以else作为一个#t测试表达式的同义词使用。

使用cond,reply-more函数可以更清楚地写成如下形式:

(define (reply-more s)
  (cond
   [(equal? "hello" (substring s 0 5))
    "hi!"]
   [(equal? "goodbye" (substring s 0 7))
    "bye!"]
   [(equal? "?" (substring s (- (string-length s) 1)))
    "I don't know"]
   [else "huh?"]))
 
> (reply-more "hello racket")

"hi!"

> (reply-more "goodbye cruel world")

"bye!"

> (reply-more "what is your favorite color?")

"I don't know"

> (reply-more "mine is lime green")

"huh?"

对于cond从句的方括号的使用是一种惯例。在Racket中,圆括号和方括号实际上是可互换的,只要(匹配)或[匹配]即可。在一些关键的地方使用方括号使Racket代码更易读。

2.2.6 函数重复调用

在我们早期的函数调用语法中,我们过分简单化了。一个函数调用的实际语法允许一个对这个函数的任意表达式,而不是仅仅一个‹id›:

函数调用 (later in this guide) explains more about 函数调用.

( ‹expr› ‹expr›* )

第一个‹expr›常常是一个‹id›,比如string-append+,但它可以是对一个函数的求值的任意情况。例如,它可以是一个条件表达式:

(define (double v)
  ((if (string? v) string-append +) v v))
 
> (double "mnah")

"mnahmnah"

> (double 5)

10

在语句构成上,在一个函数调用的第一个表达甚至可以是一个数值——但那会导致一个错误,因为一个数值不是一个函数。

> (1 2 3 4)

application: not a procedure;

 expected a procedure that can be applied to arguments

  given: 1

当你偶然忽略了一个函数名或在你使用额外的圆括号围绕一个表达式时,你最常会得到一个像“expected a procedure”这样的一条错误。

2.2.7 匿名函数与lambda

如果你不得不命名你所有的数值,那Racket中的编程就太乏味了。代替(+ 1 2)的写法,你不得不这样写:

lambda函数(过程) (later in this guide) explains more about lambda.

> (define a 1)
> (define b 2)
> (+ a b)

3

事实证明,要命名所有你的函数也可能是很乏味的。例如,你可能有一个函数 twice,它带了一个函数和一个参数。如果你已经有了这个函数的名字,那么使用 twice是比较方便的,如sqrt

(define (twice f v)
  (f (f v)))
 
> (twice sqrt 16)

2

如果你想去调用一个尚未定义的函数,你可以定义它,然后将其传递给twice:

(define (louder s)
  (string-append s "!"))
 
> (twice louder "hello")

"hello!!"

但是如果对twice的调用是唯一使用louder的地方,却还要写一个完整的定义是很可惜的。在Racket中,你可以使用一个lambda表达式去直接生成一个函数。lambda表后面是函数参数的标识,然后是函数的主体表达式:

( lambda ( ‹id›* ) ‹expr›+ )

通过自身求值一个lambda表产生一个函数:

> (lambda (s) (string-append s "!"))

#<procedure>

使用lambda,上述对twice的调用可以重写为:

> (twice (lambda (s) (string-append s "!"))
         "hello")

"hello!!"

> (twice (lambda (s) (string-append s "?!"))
         "hello")

"hello?!?!"

lambda的另一个用途是作为一个生成函数的函数的一个结果:

(define (make-add-suffix s2)
  (lambda (s) (string-append s s2)))
 
> (twice (make-add-suffix "!") "hello")

"hello!!"

> (twice (make-add-suffix "?!") "hello")

"hello?!?!"

> (twice (make-add-suffix "...") "hello")

"hello......"

Racket是一个词法作用域(lexically scoped)语言,这意味着函数中的s2总是通过make-add-suffix引用创建该函数调用的参数返回。换句话说,lambda生成的函数“记住”了右边的s2:

> (define louder (make-add-suffix "!"))
> (define less-sure (make-add-suffix "?"))
> (twice less-sure "really")

"really??"

> (twice louder "really")

"really!!"

我们有了对表(define ‹id› ‹expr›)的定义的一定程度的引用作为“非函数定义(non-function definitions)“。这种表征是误导性的,因为‹expr›可以是一个lambda表,在这种情况下,定义与使用“函数(function)”定义表是等价的。例如,下面两个louder的定义是等价的:

(define (louder s)
  (string-append s "!"))
 
(define louder
  (lambda (s)
    (string-append s "!")))
 
> louder

#<procedure:louder>

注意,对第二例子中louder的表达式是用lambda写成的“匿名”函数,但如果可能的话,无论如何,编译器推断出一个名称以使打印和错误报告尽可能地提供信息。

2.2.8 defineletlet*实现局部绑定

现在是在我们的Racket语法中收回另一个简化的时候了。在一个函数的主体中,定义可以出现在函数主体表达式之前:

内部定义 (later in this guide) explains more about 局部(内部)定义.

( define ( ‹id› ‹id›* ) ‹definition›* ‹expr›+ )
( lambda ( ‹id›* ) ‹definition›* ‹expr›+ )

在一个函数主体的开始的定义对这个函数主体来说是局部的。

 

Examples:
(define (converse s)
  (define (starts? s2) ; local to converse
    (define len2 (string-length s2))  ; local to starts?
    (and (>= (string-length s) len2)
         (equal? s2 (substring s 0 len2))))
  (cond
   [(starts? "hello") "hi!"]
   [(starts? "goodbye") "bye!"]
   [else "huh?"]))
 
> (converse "hello!")

"hi!"

> (converse "urp")

"huh?"

> starts? ; outside of converse, so...

starts?: undefined;

 cannot reference an identifier before its definition

  in module: top-level

 

创建局部绑定的另一种方法是let表。let的一个优势是它可以在任何表达式位置使用。另外,let可以一次绑定多个标识,而不是每个标识都需要一个单独的define

内部定义 (later in this guide) explains more about let and let*.

( let ( {[ ‹id› ‹expr› ]}* ) ‹expr›+ )

每个绑定从句是一个‹id›和一个‹expr›通过方括号包围,并且这个从句之后的表达式是let的主体。在每一个从句里,为了在主题中的使用,该‹id›被绑定到‹expr›的结果。

> (let ([x (random 4)]
        [o (random 4)])
    (cond
     [(> x o) "X wins"]
     [(> o x) "O wins"]
     [else "cat's game"]))

"O wins"

一个let表的绑定仅在let的主体中可用,因此绑定从句不能互相引用。相比之下,let*表允许后面的从句使用更早的绑定:

> (let* ([x (random 4)]
         [o (random 4)]
         [diff (number->string (abs (- x o)))])
    (cond
     [(> x o) (string-append "X wins by " diff)]
     [(> o x) (string-append "O wins by " diff)]
     [else "cat's game"]))

"cat's game"

 

2.3 列表、迭代和递归

Racket语言是Lisp语言的一种方言,名字来自于“LISt Processor”。内置的列表数据类型保留了这种语言的一个显著特征。

list函数接受任意数量的值并返回一个包含这些值的列表:

> (list "red" "green" "blue")

'("red" "green" "blue")

> (list 1 2 3 4 5)

'(1 2 3 4 5)

一个列表通常用'打印,但是一个列表的打印形式取决于它的内容。更多信息请看《点对(Pair)和列表(List)》。

就如你能够看到的那样,一个列表结果在REPL中打印为一个引用',并且采用一对圆括号包围这个列表元素的打印表。这里有一个容易混淆的地方,因为两个表达式都使用圆括号,比如(list "red" "green" "blue"),那么打印结果为'("red" "green" "blue")。除了引用,结果的圆括号在文档中和在DrRacket中打印为蓝色,而表达式的圆括号是棕色的。

在列表方面有许多预定义的函数操作。下面是少许例子:

> (length (list "hop" "skip" "jump"))        ; count the elements

3

> (list-ref (list "hop" "skip" "jump") 0)    ; extract by position

"hop"

> (list-ref (list "hop" "skip" "jump") 1)

"skip"

> (append (list "hop" "skip") (list "jump")) ; combine lists

'("hop" "skip" "jump")

> (reverse (list "hop" "skip" "jump"))       ; reverse order

'("jump" "skip" "hop")

> (member "fall" (list "hop" "skip" "jump")) ; check for an element

#f

 

2.4 pair、list和Racket的语法

cons函数实际上接受任意两个值,而不只是一个给第二个参数的列表。当第二个参数不是empty且不是自己通过cons产生的时,结果以一种特殊的方式打印出来。两个值用cons凑在一起被打印在括号之间,但在两者之间有一个点(即,一个被空格环绕的句点):

> (cons 1 2)

'(1 . 2)

> (cons "banana" "split")

'("banana" . "split")

因此,由cons产生的一个值并不总是一个列表。一般来说,cons的结果是一个 点对(pair)。更符合惯例的cons?函数名字是pair?,那我们从现在开始使用这个符合惯例的名字。

名字rest对非列表点对也意义不大;对first和rest更符合惯例的名字分别是car和cdr。(当然,符合惯例的名字也是没有意义的。请记住,“a”出现在“d”之前,并且cdr被声明为“could-er(可以)”。

 

Examples:

> (car (cons 1 2))

1

> (cdr (cons 1 2))

2

> (pair? empty)

#f

> (pair? (cons 1 2))

#t

> (pair? (list 1 2 3))

#t

 

Racket的点对数据类型和它对表的关系,连同打印的点符号和滑稽的名字car及cdr本质上是一个历史上的奇特事物。然而,点对深深地被连接进了Racket的文化、详述和实现上,因此它们在语言中得以存在下来。

在你犯一个错误时,你很可能会遇到一个非列表点对,比如不小心给cons把参数颠倒过来:

> (cons (list 2 3) 1)

'((2 3) . 1)

> (cons 1 (list 2 3))

'(1 2 3)

非列表点对有时被有意使用。例如,make-hash函数取得了一个点对的列表,其中每个点对的car是一个键同时cdr是一个任意值。

对新的Racket程序员唯一更困惑的情况莫过于非列表点对是对点对的打印习惯,其第二个元素是一个点对而不是一个列表:

> (cons 0 (cons 1 2))

'(0 1 . 2)

一般来说,打印一个点对的规则如下:除非该点紧接着是一个开括号,否则使用点表示法。在这种情况下,去掉点、开括号和匹配的闭括号。由此,'(0 . (1 . 2))变成'(0 1 . 2),'(1 . (2 . (3 . ())))变成'(1 2 3)。

2.4.1 用quote引用pair和symbol

一个列表在前面打印一个引号标记,但是如果一个列表的一个元素本身是一个列表,那么就不会为内部列表打印引号标记:

> (list (list 1) (list 2 3) (list 4))

'((1) (2 3) (4))

对于嵌套列表,尤其是quote表,你可以将列表作为一个表达式来写,基本上与列表打印的方式相同:

> (quote ("red" "green" "blue"))

'("red" "green" "blue")

> (quote ((1) (2 3) (4)))

'((1) (2 3) (4))

> (quote ())

'()

无论引用表是否由点括号消除规则规范,quote表都要包含点符号:

> (quote (1 . 2))

'(1 . 2)

> (quote (0 . (1 . 2)))

'(0 1 . 2)

当然,任何种类的列表都可以嵌套:

> (list (list 1 2 3) 5 (list "a" "b" "c"))

'((1 2 3) 5 ("a" "b" "c"))

> (quote ((1 2 3) 5 ("a" "b" "c")))

'((1 2 3) 5 ("a" "b" "c"))

如果用quote包裹标识,则得到看起来像一个标识的输出,但带有一个'前缀:

> (quote jane-doe)

'jane-doe

像一个引用标识那样打印的一个值是一个symbol(符号)。同样,括号输出不应该和表达式混淆,一个打印符号不应与一个标识混淆。特别是,除了符号和标识碰巧由相同的字母组成外,符号(quote map)与map标识或绑定到map的预定义函数无关。

的确,一个符号固有的值不过是它的字符内容。从这个意义上说,符号和字符串几乎是一样的东西,主要区别在于它们是如何打印的。函数symbol->string和string->symbol在它们之间转换。

 

Examples:

> map

#<procedure:map>

> (quote map)

'map

> (symbol? (quote map))

#t

> (symbol? map)

#f

> (procedure? map)

#t

> (string->symbol "map")

'map

> (symbol->string (quote map))

"map"

 

同样,对一个列表quote会自己自动作用于嵌套列表,在标识的一个括号序列上的quote会自己自动应用到标识上以创建一个符号列表:

> (car (quote (road map)))

'road

> (symbol? (car (quote (road map))))

#t

当一个符号在一个打印有'的列表中时,在符号上的这个'被省略了,因为'已经在做这项工作了:

> (quote (road map))

'(road map)

quote表对一个文字表达式,像一个数字或一个字符串这样的,没有影响:

> (quote 42)

42

> (quote "on the record")

"on the record"

2.4.2 使用'缩写quote

你可能已经猜到了,你可以通过仅放置一个'在一个表前面来缩写一个quote的使用:

> '(1 2 3)

'(1 2 3)

> 'road

'road

> '((1 2 3) road ("a" "b" "c"))

'((1 2 3) road ("a" "b" "c"))

在文档中,在一个表达式中的'和后面的表单一起被打印成绿色,因为这个组合是一个表达式,它是一个常量。在DrRacket,只有'被渲染成绿色。DrRacket更加精确校正,因为quote的意义可以根据一个表达式的上下文而变化。然而,在文档中,我们经常假定标准绑定是在范围内的,因此我们为了更清晰用绿色绘制引用表。

一个'以字面相当的方式扩展成一个quote表。你够明白如果你在一个有一个'的表前面放置一个'的这种情况:

> (car ''road)

'quote

> (car '(quote road))

'quote

'缩写在输出和输入中起作用。在打印输出时,REPL打印机识别符号'quote的两元素列表的第一个元素,在这种情况下,它使用’打印输出:

> (quote (quote road))

''road

> '(quote road)

''road

> ''road

''road

2.4.3 列表和Racket语法

现在你已经知道了关于点对和列表的真相,而且现在你已经明白了quote,你已经准备好理解我们一直在简化Racket真实语法的主要方法。

Racket的语法并不是直接在字符流中定义的。相反,语法是由两个层确定的:

  • 一个读取器(reader)层,将字符序列转换成列表、符号和其它常量;

  • 一个扩展器(expander)层,它处理列表、符号和其它常量,并将它们解析为表达式。

打印和读取的规则是互相协调的。例如,一个列表用圆括号打印,读取一对圆括号生成一个列表。类似地,一个非列表点对用点表示法打印,同时在输入上的一个点有效地运行点标记规则从反向得到一个点对。

读取层给表达式的一个推论是你可以在不被引用的表的表达式中使用点标记:

> (+ 1 . (2))

3

这个操作因为(+ 1 . (2))只是编写(+ 1 2)的另一种方法。用这种点表示法编写应用程序表达式实际上从来不是一个好主意,它只是Racket语法定义方法的一个推论。

通常,.被仅只带一个括号序列的读取器允许,并且只有在序列的最后一个元素之前。然而,一对.也可以出现在一个括号序列的一个单个元素周围,只要这个元素不是第一个或最后一个。这样的一个点对触发一个阅读器转换,它将.之间的元素移动到列表的前面。这个转换使一种通用的中缀表示法成为可能:

> (1 . < . 2)

#t

> '(1 . < . 2)

'(< 1 2)

这两个点转换是非传统的,并且它与非列表点对的点记法基本上没有关系。Racket程序员保守地使用中缀标记——大多用于非对称二元操作符,如<和is-a?。

 

内置的数据类型

上一章介绍了一些Racket的内建数据类型:数字、布尔值、字符串、列表、和过程。本节为数据的简单表提供一个内建数据类型的更完整的覆盖。

 

3.1 布尔值(Boolean)

Racket有表示布尔值的两个重要的常数:#t表示真,#f表示假。大写的#T和#F解析为同样的值,但小写形式是首选。

boolean?程序识别两个布尔常量。然而,在对if、cond、 and、or等等的一个测试表达式的结果里,除了#f之外,任何值都是记为真。

 

Examples:

> (= 2 (+ 1 1))

#t

> (boolean? #t)

#t

> (boolean? #f)

#t

> (boolean? "no")

#f

> (if "no" 1 0)

1

 

3.2 数值(Number)

一个Racket的数值(number)既可以是精确的也可以是不精确的:

  • 一个精确的数值是:

    • 一个任意大的或任意小的整数,比如:5,99999999999999999或-17;

    • 一个有理数,它是精确的两个任意小的或任意大的整数比,比如:1/2,99999999999999999/2或-3/4;

    • 一个带有精确的实部和虚部(即虚部不为零)的复数,比如:1+2i或1/2+3/4i。

  • 一个 不精确的数值是:

    • 一个数值的一个IEEE浮点表示,比如:2.0或3.14e+87,其中IEEE无穷大和一个非数值编写为:+inf.0,-inf.0和+nan.0(或-nan.0);

    • 一个带有IEEE浮点表示的实部和虚部的复数,比如:2.0+3.0i或-inf.0+nan.0i;作为一种特例,一个带有一个不精确的虚部的不精确的复数可以有一个精确的零实部。

带有一个小数点或指数说明符的不精确数字打印,以及作为整数和分数的精确数字打印。同样的的惯例申请读取数值常量,但#e或#i能够前缀一个数值以强制其解析为一个精确的或不精确的数值。前缀#b、#o和#x指定二进制、八进制和十六进制数的解释。

《The Racket Reference(Racket参考)》文档(4.2 Numbers(数值))有数值的语法的细微之处。

 

Examples:

> 0.5

0.5

> #e0.5

1/2

> #x03BB

955

 

包含一个精确数值的计算产生不精确的结果,以致不精确充当了一种数值方面的污染。注意,然而,Racket没有提供"不精确的布尔值",所以对不精确的数字的比较分支计算却仍然能产生精确的结果。过程exact->inexact和inexact->exact在两种数值类型之间转换。

 

Examples:

> (/ 1 2)

1/2

> (/ 1 2.0)

0.5

> (if (= 3.0 2.999) 1 2)

2

> (inexact->exact 0.1)

3602879701896397/36028797018963968

 

当精确的结果需要作为非有理数实数时,不精确的结果也由像sqrt、log和sin这样的过程产生。Racket仅能表示有理数和带有理数部分的复数。

 

Examples:

> (sin 0)   ; 有理数...

0

> (sin 1/2) ; 非有理数...

0.479425538604203

 

在性能而言,带小整数的计算通常是最快的,其中“小”意味着这个合二为一的数值小于有符号数值的机器字长。具有非常大的精确整数或具有非整精确数的计算会比不精确数的计算代价要高昂得多。

(define (sigma f a b)
  (if (= a b)
      0
      (+ (f a) (sigma f (+ a 1) b))))
 
> (time (round (sigma (lambda (x) (/ 1 x)) 1 2000)))

cpu time: 10 real time: 10 gc time: 0

8

> (time (round (sigma (lambda (x) (/ 1.0 x)) 1 2000)))

cpu time: 0 real time: 0 gc time: 0

8.0

数值类别整数(integer)、有理数(rational)、实数(real)(总是有理数)以及复数(complex)用通常的方法定义,并被过程integer?、rational?、real?以及complex?所验证。一些数学过程只接受实数,但大多数实现了对复数的标准扩展。

 

Examples:

> (integer? 5)

#t

> (complex? 5)

#t

> (integer? 5.0)

#t

> (integer? 1+2i)

#f

> (complex? 1+2i)

#t

> (complex? 1.0+2.0i)

#t

> (abs -5)

5

> (abs -5+2i)

abs: contract violation

  expected: real?

  given: -5+2i

> (sin -5+2i)

3.6076607742131563+1.0288031496599335i

 

=过程为了数值相等而比较数值。如果给定不精确数和精确数去作比较,它在比较之前从本质上将不精确数转换为精确数。相反,eqv?(乃至 equal?)过程比较数值既考虑精确性又考虑数值的相等。

 

Examples:

> (= 1 1.0)

#t

> (eqv? 1 1.0)

#f

 

当心涉及不精确数的比较,由于其天性会有出人意料的行为。甚至实际上简单的不精确数也许并不意味着你能想到的和他们的意义一致;例如,当一个二进制IEEE浮点数可以精确地表示为1/2时,它可能近似于1/10:

 

Examples:

> (= 1/2 0.5)

#t

> (= 1/10 0.1)

#f

> (inexact->exact 0.1)

3602879701896397/36028797018963968

 

《The Racket Reference(Racket参考)》文档(4.2 Numbers(数值))有关于数值和数值过程的更多内容。

 

3.3 字符(Character)

Racket 字符(character)对应于Unicode标量值(scalar value)。粗略地说,一个标量值是一个无符号整数,表示为21位,并且映射到某种自然语言字符或字符块的某些概念。从技术上讲,一个标量值是一个比Unicode标准中的一个“字符”更简单的概念,但它是一种有许多作用的近似值。例如,任何重音罗马字母都可以表示为一个标量值,就像任何普通的汉字字符一样。

虽然每个Racket字符对应一个整数,但字符数据类型和数值是有区别的。char->integer和integer->char过程在标量值和相应字符之间转换。

一个可打印字符通常打印为以#\后跟着代表字符的形式。一个非打印字符通常打印为以#\u后跟着十六进制数值的标量值的形式。几个字符以特殊方式打印;例如,空格和换行符分别打印为#\space和#\newline。

在《The Racket Reference(Racket参考)》的字符解析文档有字符语法的更好的知识点。

 

Examples:

> (integer->char 65)

#\A

> (char->integer #\A)

65

> #\λ

#\λ

> #\u03BB

#\λ

> (integer->char 17)

#\u0011

> (char->integer #\space)

32

 

display过程直接将一个字符写入到当前输出端口(详见《输入和输出》),与用于打印一个字符结果的字符常量语法形成对照。

 

Examples:

> #\A

#\A

> (display #\A)

A

 

Racket提供了几种对字符的分类和转换的过程。然而,注意某些Unicode字符要只有它们在一个字符串中和一个人所希望的那样转换才行(例如,”ß”的大写转换或者”Σ”的小写转换)。

 

Examples:

> (char-alphabetic? #\A)

#t

> (char-numeric? #\0)

#t

> (char-whitespace? #\newline)

#t

> (char-downcase #\A)

#\a

> (char-upcase #\ß)

#\ß

 

char=?过程比较两个或多个字符,char-ci=?比较字符但忽略大写。eqv?和equal?过程在字符方面的行为与char=?表现一样;当你更具体地声明正在比较的值是字符时使用char=?。

 

Examples:

> (char=? #\a #\A)

#f

> (char-ci=? #\a #\A)

#t

> (eqv? #\a #\A)

#f

 

在《The Racket Reference(Racket参考)》的字符部分中提供字符和字符过程的更多信息。

 

3.4 字符串(Unicode)

一个字符串(string)是一个固定长度的字符(characters)数组。它使用双引号打印,在字符串中的双引号和反斜杠字符是用反斜杠转义。其它普通的字符串转义被支持,包括\n用于一个换行,\r用于一个回车,使用\后边跟着多达三个八进制数字实现八进制转义,以及用\u(多达四位数)实现十六进制转义。在打印字符串时通常用\u显示一个字符串中的不可打印字符。

在《Racket参考》中的“读取字符串(Reading Strings)”文档有关于字符串语法的更好的知识点。

display过程直接将一个字符串中的字符写入当前输出端口(见《输入和输出》),在字符串常量语法对比中用于打印一个字符串结果。

 

Examples:

> "Apple"

"Apple"

> "\u03BB"

"λ"

> (display "Apple")

Apple

> (display "a \"quoted\" thing")

a "quoted" thing

> (display "two\nlines")

two

lines

> (display "\u03BB")

λ

 

一个字符串可以是可变的也可以是不可变的;作为表达式直接编写的字符串是不可变的,但大多数其它字符串是可变的。make-string过程创建一个给定一个长度和可选填充字符的可变字符串。string-ref过程从一个字符串(用基于0的索引)中访问一个字符。string-set!过程在一个可变字符串中更改一个字符。

 

Examples:

> (string-ref "Apple" 0)

#\A

> (define s (make-string 5 #\.))
> s

"....."

> (string-set! s 2 #\λ)
> s

"..λ.."

 

字符串排序和状态操作通常是区域无关(locale-independent)的;也就是说,它们对所有用户都采用相同的工作方式。一些区域相关(locale-dependent)的操作被提供,它们允许字符串折叠和排序的方式取决于最终用户的区域设置。如果你在排序字符串,例如,如果排序结果应该在机器和用户之间保持一致,使用string<?或string-ci<?,但如果排序纯粹是为一个最终用户整理字符串,使用string-locale<?或string-locale-ci<?。

 

Examples:

> (string<? "apple" "Banana")

#f

> (string-ci<? "apple" "Banana")

#t

> (string-upcase "Straße")

"STRASSE"

> (parameterize ([current-locale "C"])
    (string-locale-upcase "Straße"))

"STRAßE"

 

对于使用纯粹的ASCII、使用原始字节或编码/解码Unicode字符串为字节,使用字节字符串(byte strings)

在《Racket参考》中的字符串(strings)部分提供更多字符串和字符串过程的信息。

 

3.5 字节(Byte)和字节字符串(Byte String)

一个字节(byte)是一个在0到255之间的精确整数。byte?判断识别表示字节的数字。

 

Examples:

> (byte? 0)

#t

> (byte? 256)

#f

 

一个字节字符串(byte string)类似于一个字符串——参见《字符串(Unicode)》,但它的内容是字节序列而不是字符。字节字符串可用于处理纯ASCII文本而不是Unicode文本的应用程序中。一个字节字符串的打印形式特别支持这样使用,因为一个字节字符串打印像字节字符串的ASCII解码,但有一个#前缀。在字节字符串中不可打印的ASCII字符或非ASCII字节用八进制表示法编写。

在《Racket参考》中的“读取字符串(Reading Strings)”文档有关于字节字符串语法的更好的知识点。

 

Examples:

> #"Apple"

#"Apple"

> (bytes-ref #"Apple" 0)

65

> (make-bytes 3 65)

#"AAA"

> (define b (make-bytes 2 0))
> b

#"\0\0"

> (bytes-set! b 0 1)
> (bytes-set! b 1 255)
> b

#"\1\377"

 

一个字节字符串的display表写入其原始字节到当前输出端口(详见《输入和输出》部分)。从技术上讲,一个通常(即,字符)的display字符串打印字符串的UTF-8编码到当前输出端口,因为输出是以字节为单位的最终定义;然而一个字节字符串的display用无编码的方式写入原始字节。按同样的思路,当这个文档显示输出时,它严格说来是显示输出的UTF-8编码格式。

 

Examples:

> (display #"Apple")

Apple

> (display "\316\273")  ; 等同于"λ"

λ

> (display #"\316\273") ; λ的UTF-8编码

λ

 

对于在字符串和字节字符串之间的显式转换,Racket直接支持三种编码:UTF-8,Latin-1和当前的本地编码。字节到字节转换(特别是转换到UTF-8和从UTF-8转换来)的通用工具弥合了支持任意字符串编码的差异分歧。

 

Examples:

> (bytes->string/utf-8 #"\316\273")

"λ"

> (bytes->string/latin-1 #"\316\273")

"λ"

> (parameterize ([current-locale "C"])  ; C局部支持ASCII,
    (bytes->string/locale #"\316\273")) ; 仅仅,这样……

bytes->string/locale: byte string is not a valid encoding

for the current locale

  byte string: #"\316\273"

> (let ([cvt (bytes-open-converter "cp1253" ; 希腊代码页
                                   "UTF-8")]
        [dest (make-bytes 2)])
    (bytes-convert cvt #"\353" 0 1 dest)
    (bytes-close-converter cvt)
    (bytes->string/utf-8 dest))

"λ"

 

在《Racket参考》里的“字节字符串(Byte Strings)”部分提供了关于字节字符串和字节字符串函数的更详尽内容。

 

3.6 符号(Symbol)

一个符号(symbol)是一个原子值,它像一个前面的标识那样以'前缀打印。一个以'开始并带一个标识的表达式产生一个符号值。

 

Examples:

> 'a

'a

> (symbol? 'a)

#t

 

对于字符的任何序列,正好有一个相应的符号被保留(interned);调用string->symbol过程或者read一个语法标识,产生一个保留符号。由于保留符号可以被用eq?(或这样:eqv?或equal?)方便地比较,所以它们作为方便的值用于标签和枚举。

符号是区分大小写的。通过使用一个#ci前缀或其它方式,读取器能够被要求去折叠容器序列以获得一个符号,但是读取器通过默认方式保护容器。

 

Examples:

> (eq? 'a 'a)

#t

> (eq? 'a (string->symbol "a"))

#t

> (eq? 'a 'b)

#f

> (eq? 'a 'A)

#f

> #ci'A

'a

 

任何字符串(或者说,任何字符序列)都可以提供给string->symbol以获得对应的符号。对于读取器输入来说,任何字符都可以直接出现在一个标识里,空白和以下特殊字符除外:

   ( ) [ ] { } " , ' ` ; # | \

实际上,#仅仅不允许在一个符号开始位置,并且也仅仅不允许%在最后位置;除此之外,#是被允许的。此外,.本身不是一个符号。

空格或特殊字符可以通过用|或\引用包含进一个标识里。这些引用机制用于包含特殊字符或可能额外看起来像数字的标识的打印表中。

 

Examples:

> (string->symbol "one, two")

'|one, two|

> (string->symbol "6")

'|6|

 

在《Racket参考》中的“读取符号(Reading Symbols)”文档有关于符号语法的更好的知识点。

write函数打印一个没有一个'前缀的符号。一个符号的display表与对应的字符串相同。

 

Examples:

> (write 'Apple)

Apple

> (display 'Apple)

Apple

> (write '|6|)

|6|

> (display '|6|)

6

 

gensym和string->uninterned-symbol过程生成新的非保留(uninterned)符号,它不等同于(比照eq?)任何先前的保留或非保留符号。非保留符号作为新标签是有用的,它不会与其它任何值混淆。

 

Examples:

> (define s (gensym))
> s

'g42

> (eq? s 'g42)

#f

> (eq? 'a (string->uninterned-symbol "a"))

#f

 

在《Racket参考》中的“符号(Symbols)”文档有关于符号的更多信息。

 

3.7 关键字(Keyword)

一个关键字(keyword)值类似于一个符号(详见《符号(Symbol)》),但它的打印形式是用#:进行前缀。

在《Racket参考》里的“读取关键字”(Reading Keywords)文档有关于关键字的语法更好的知识点。

 

Examples:

> (string->keyword "apple")

'#:apple

> '#:apple

'#:apple

> (eq? '#:apple (string->keyword "apple"))

#t

 

更确切地说,一个关键字类似于一个标识;以同样的方式,一个标识可以被引用以生成一个符号,一个关键字可以被引用以生成一个值。在这两种情况下都使用同一术语“关键字”,但有时我们使用关键字值(keyword value)去更具体地针对一个引用关键字表达式的结果或使用string->keyword的结果。一个非引用关键字不是一个表达式,只是作为一个非引用标识,不产生一个符号:

 

Examples:

> not-a-symbol-expression

not-a-symbol-expression: undefined;

 cannot reference an identifier before its definition

  in module: top-level

> #:not-a-keyword-expression

eval:2:0: #%datum: keyword misused as an expression

  at: #:not-a-keyword-expression

 

尽管它们有相似之处,但关键字的使用方式不同于标识或符号。关键字是为了使用(不带引号)作为参数列表和在特定的句法形式的特殊标记。运行时的标记和枚举,而不是关键字用符号。下面的示例说明了关键字和符号的不同角色。

 

Examples:

> (define dir (find-system-path 'temp-dir)) ; not '#:temp-dir
> (with-output-to-file (build-path dir "stuff.txt")
    (lambda () (printf "example\n"))
    ; 可选的#:mode参数可以是'text或'binary
    #:mode 'text
    ; 可选的#:exists参数可以是'replace、'truncate、...
    #:exists 'replace)

 

3.8 点对(Pair)和列表(List)

一个点对(pair)把两个任意值结合。cons过程构建点对,car和cdr过程分别提取点对的第一和第二个点对元素。pair?判断识别点对。

一些点对通过圆括号包围两个点对元素的打印形式来打印,在开始位置放置一个',并在元素之间放置一个.。

 

Examples:

> (cons 1 2)

'(1 . 2)

> (cons (cons 1 2) 3)

'((1 . 2) . 3)

> (car (cons 1 2))

1

> (cdr (cons 1 2))

2

> (pair? (cons 1 2))

#t

 

一个列表(list)是一个点对的组合,它创建一个链表。更确切地说,一个列表要么是空列表null,要么是个点对,其第一个元素是一个列表元素,第二个元素是一个列表。list?判断识别列表。null?判断识别空列表。

一个列表通常打印为一个'后跟一对括号包裹列表元素。

 

Examples:

> null

'()

> (cons 0 (cons 1 (cons 2 null)))

'(0 1 2)

> (list? null)

#t

> (list? (cons 1 (cons 2 null)))

#t

> (list? (cons 1 2))

#f

 

当一个列表或点对的其中一个元素不能写成一个quote(引用)值时,使用list或cons打印。例如,一个用srcloc构建的值不能使用quote来编写,应该使用srcloc来编写:

> (srcloc "file.rkt" 1 0 1 (+ 4 4))

(srcloc "file.rkt" 1 0 1 8)

> (list 'here (srcloc "file.rkt" 1 0 1 8) 'there)

(list 'here (srcloc "file.rkt" 1 0 1 8) 'there)

> (cons 1 (srcloc "file.rkt" 1 0 1 8))

(cons 1 (srcloc "file.rkt" 1 0 1 8))

> (cons 1 (cons 2 (srcloc "file.rkt" 1 0 1 8)))

(list* 1 2 (srcloc "file.rkt" 1 0 1 8))

也参见list*。

如最后一个例子所示,list*是用来缩略一系列不能使用list缩写的cons。

write和display函数不带一个前导'、cons、list或list*打印一个点对或一个列表。对于一个点对或列表来说write和display没有区别,除非它们运用于列表的元素:

 

Examples:

> (write (cons 1 2))

(1 . 2)

> (display (cons 1 2))

(1 . 2)

> (write null)

()

> (display null)

()

> (write (list 1 2 "3"))

(1 2 "3")

> (display (list 1 2 "3"))

(1 2 3)

 

对于列表来说最重要的预定义过程是遍历列表元素的那些过程:

> (map (lambda (i) (/ 1 i))
       '(1 2 3))

'(1 1/2 1/3)

> (andmap (lambda (i) (i . < . 3))
         '(1 2 3))

#f

> (ormap (lambda (i) (i . < . 3))
         '(1 2 3))

#t

> (filter (lambda (i) (i . < . 3))
          '(1 2 3))

'(1 2)

> (foldl (lambda (v i) (+ v i))
         10
         '(1 2 3))

16

> (for-each (lambda (i) (display i))
            '(1 2 3))

123

> (member "Keys"
          '("Florida" "Keys" "U.S.A."))

'("Keys" "U.S.A.")

> (assoc 'where
         '((when "3:30") (where "Florida") (who "Mickey")))

'(where "Florida")

在《Racket参考》中的“点对和列表(Pairs and Lists)”提供更多有关点对和列表的信息。

点对是不可变的(与Lisp传统相反),并且pair?和list?仅识别不可变的点对和列表。mcons过程创建一个可变点对(mutable pair),它配合set-mcar!和set-mcdr!,及mcar和mcdr进行操作。一个可变点对用mcons打印,而write和display使用{和}打印:

 

Examples:

> (define p (mcons 1 2))
> p

(mcons 1 2)

> (pair? p)

#f

> (mpair? p)

#t

> (set-mcar! p 0)
> p

(mcons 0 2)

> (write p)

{0 . 2}

 

在《Racket参考》中的“可变点对和列表(Mutable Pairs and Lists)”中提供关于可变点对的更多信息。

 

3.9 向量(Vector)

一个向量(vector)是任意值的一个固定长度数组。与一个列表不同,一个向量支持常量时间访问和它的元素更新。

一个向量打印类似于一个列表——作为其元素的一个括号序列——但一个向量要在'之后加前缀#,或如果它的元素不能用引号表示则使用vector表示。

对于作为一个表达式的一个向量,可以提供一个可选长度。同时,一个向量作为一个表达式隐式地为它的内容quote(引用)这个表,这意味着在一个向量常数中的标识和括号表代表符号和列表。

在《Racket参考》中的“读取向量(Reading Vectors)”文档有向量的语法更好的知识点。

 

Examples:

> #("a" "b" "c")

'#("a" "b" "c")

> #(name (that tune))

'#(name (that tune))

> #4(baldwin bruce)

'#(baldwin bruce bruce bruce)

> (vector-ref #("a" "b" "c") 1)

"b"

> (vector-ref #(name (that tune)) 1)

'(that tune)

 

像字符串一样,一个向量要么是可变的,要么是不可变的,向量直接编写为表达式是不可变的。

向量可以通过vector->list和list->vector转换成列表,反之亦然。这种转换在与对列表的预定义过程相结合中是特别有用的。当分配额外的列表似乎太昂贵时,考虑使用像for/fold的循环表,它像列表一样识别向量。

 

Example:

> (list->vector (map string-titlecase
                     (vector->list #("three" "blind" "mice"))))

'#("Three" "Blind" "Mice")

 

在《Racket参考》中的“向量(vectors)”部分提供有关向量和向量过程的更多内容。

 

3.10 散列表(Hash Table)

一个散列表(hash table)实现了从键到值的一个映射,其中键和值都可以是任意的Racket值,以及对表的访问和更新通常是常量时间操作。键的比较使用equal?、eqv?或eq?,取决于散列表创建方式是否为make-hash、make-hasheqv或make-hasheq。

 

Examples:

> (define ht (make-hash))
> (hash-set! ht "apple" '(red round))
> (hash-set! ht "banana" '(yellow long))
> (hash-ref ht "apple")

'(red round)

> (hash-ref ht "coconut")

hash-ref: no value found for key

  key: "coconut"

> (hash-ref ht "coconut" "not there")

"not there"

 

hash、hasheqv和hasheq函数从键和值的一个初始设置创建不可变散列表,其中每个值作为它键后边的一个参数提供。不可变散列表可用hash-set扩展,它在恒定时间里产生一个新的不可变散列表。

 

Examples:

> (define ht (hash "apple" 'red "banana" 'yellow))
> (hash-ref ht "apple")

'red

> (define ht2 (hash-set ht "coconut" 'brown))
> (hash-ref ht "coconut")

hash-ref: no value found for key

  key: "coconut"

> (hash-ref ht2 "coconut")

'brown

 

一个原义的不可变散列表可以通过使用#hash(对基于equal?的表)、#hasheqv(对基于eqv?的表)或#hasheq(对基于eq?的表)编写为一个表达式。一个带括号的序列必须紧跟着#hash、#hasheq或#hasheqv,其中每个元素是一个带点的键–值对。这个#hash等等这些表都隐含的quote它们的键和值的子表。

 

Examples:

> (define ht #hash(("apple" . red)
                   ("banana" . yellow)))
> (hash-ref ht "apple")

'red

 

在《Racket参考》的“读取散列表(Reading Hash Tables)”文档有关于散列表原义的语法更好的知识点。

可变和不可变的散列表都像不可变散列表一样打印,否则如果所有的键和值可以用quote表示或者使用hash、hasheq或hasheqv,那么使用一个带引用的#hash、#hasheqv或#hasheq表。

 

Examples:

> #hash(("apple" . red)
        ("banana" . yellow))

'#hash(("apple" . red) ("banana" . yellow))

> (hash 1 (srcloc "file.rkt" 1 0 1 (+ 4 4)))

(hash 1 (srcloc "file.rkt" 1 0 1 8))

 

一个可变散列表可以选择性地弱方式(weakly)保留其键,因此仅仅只要在其它地方保留键,每个映射都被保留。

 

Examples:

> (define ht (make-weak-hasheq))
> (hash-set! ht (gensym) "can you see me?")
> (collect-garbage)
> (hash-count ht)

0

 

请注意,只要对应的键是可访问的,即使是一个弱散列表也会强健地保留它的值。当一个值指回到它的键,就造成了一个两难的依赖,以致这个映射永久被保留。要打破这个循环,映射键到一个暂存值(ephemeron),它用它的键(除这个散列表的隐性序对之外)序对值。

在《Racket参考》中的“星历(ephemerons)”文档有关于使用ephemerons更好的知识点。

 

Examples:

> (define ht (make-weak-hasheq))
> (let ([g (gensym)])
    (hash-set! ht g (list g)))
> (collect-garbage)
> (hash-count ht)

1

 

> (define ht (make-weak-hasheq))
> (let ([g (gensym)])
    (hash-set! ht g (make-ephemeron g (list g))))
> (collect-garbage)
> (hash-count ht)

0

在《Racket参考》中的“散列表(Hash Tables)”会提供关于散列表和散列表过程更多的信息。

 

3.11 盒子

一个盒子(box)是一个单元素向量。它可以打印成一个带引用的#&后边跟着这个盒子值的打印表。一个#&表也可以用来作为一个表达,但由于作为结果的盒子是常量,它实际上没有使用。

 

Examples:

> (define b (box "apple"))
> b

'#&"apple"

> (unbox b)

"apple"

> (set-box! b '(banana boat))
> b

'#&(banana boat)

 

在《Racket参考》的“盒子”提供关于盒子和盒子过程的更多信息。

 

3.12 无效值(Void)和未定义值(Undefined)

某些过程或表达式表不需要一个结果值。例如,display过程被别用仅为写输出的副作用。在这样的情况下,结果值通常是一个特殊的常量,它打印为#<void>。当一个表达式的结果是简单的#<void>时,REPL不打印任何东西。

void过程接受任意数量的参数并返回#<void>。(即,void标识绑定到一个返回#<void>的过程,而不是直接绑定到#<void>。)

 

Examples:
> (void)
> (void 1 2 3)
> (list (void))

'(#<void>)

 

undefined常量,它打印为#<undefined>,有时是作为一个参考的结果,其值是不可用的。在Racket以前的版本(6.1以前的版本),过早参考一个局部绑定会产生#<undefined>;相反,现在过早参考会引发一个异常。

在某些情况下,undefined结果仍然可以通过shared表产生。

(define (fails)
  (define x x)
  x)
 
> (fails)

x: undefined;

 cannot use before initialization

 

表达式和定义

Racket概要》这一章介绍了一些基本的Racket的句法表:定义、过程程序、条件表达式等等。本节提供这些形式的更详细信息,以及一些附加的基本表。

4.1 标记法

这一章(以及其余的文档)使用了一个稍微不同的标记法,而不是基于字符的《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要么是一个标识要么是一个关键字。

 

4.2 标识和绑定

一个表达式的上下文决定表达式中出现的标识的含义。特别是,用语言racket开始一个模块时,如:

#lang racket

意味着,在模块中,标识在本指南中的描述开始于这里意义的描述:cons引用创建了一个序对的函数,car引用提取了一个序对的第一个元素的函数,等等。

符号(Symbol)》介绍了标识语法。

诸如definelambdalet之类的表,用一个或多个标识关联一个意义;也就是说,它们绑定(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绑定的范围仅仅是(+ x y)的环境包括对y、x和f的绑定,以及所有在racket中的绑定。

一个模块级的define仅能够绑定没有被定义过或者require进模块的标识。然而,一个局部define或其它绑定表,能够给一个已经有一个绑定的标志符以一个新的局部绑定;这样的一个绑定覆盖(shadows)已经存在的绑定。

 

Examples:

(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这种被广泛使用的绑定——但是覆盖把一个程序员从不得不去避免每一个晦涩的通过一个语言提供的绑定中解脱出来。

即使像definelambda这些从绑定中得到它们的意义,尽管它们有转换器(transformer)绑定(这意味着它们表明语法表)而不是值绑定。由于define有一个转换器绑定,这个标识define不能被它自己使用于获取一个值。然而,对define的常规绑定可以被覆盖。

 

Examples:

define

eval:1:0: define: bad syntax

  in: define

> (let ([define 5]) define)

5

 

同样,用这种方式来覆盖标准绑定绝对是一个好主意,但这种可能性是Racket的灵活性一个固有部分。

 

4.3 函数调用(过程程序)

表的一个表达式:

(proc-expr arg-expr ...)

是一个函数调用——也被称为一个应用程序(procedure application)——当proc-expr不是一个被绑定为一个语法翻译器(如if或define)的标识符时。

4.3.1 求值顺序和实参数量

一个函数调用通过首先求值proc-expr并都按顺序(由左至右)来求值。然后,如果arg-expr产生一个接受arg-expr提供的所有参数的函数,这个函数被调用。否则,将引发一个异常。

 

Examples:

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

> (1 2 3)

application: not a procedure;

 expected a procedure that can be applied to arguments

  given: 1

 

某些函数,如cons,接受一个固定数量的参数。某些函数,如+或list,接受任意数量的参数。一些函数接受一系列参数计数;例如substring既接受两个参数也接受三个参数。一个函数的实参数量(arity)是它接受参数的数量。

4.3.2 关键字参数

除了通过位置参数外,有些函数接受关键字参数(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)”部分提供了有关过程程序的更多信息。

4.3.3 apply函数

函数调用的语法支持任意数量的参数,但是一个特定的调用总是指定一个固定数量的参数。因此,一个带一个参数列表的函数不能直接应用一个类似于+的函数到一个列表的所有项中:

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

4.4 lambda函数(过程)

一个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)

#<procedure>: arity mismatch;

 the expected number of arguments does not match the given

number

  expected: 2

  given: 1

4.4.1 申明一个剩余(rest)参数

一个lambda表达式也可以有这种表:

(lambda rest-id
  body ...+)

也就是说,一个lambda表达式可以有一个没有被圆括号包围的单个rest-id。所得到的函数接受任意数量的参数,并且这个参数放入一个绑定到rest-id的列表:

 

Examples:

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

 

Examples:

(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一样多的参数,并且还接受任意数量的附加参数。

 

Examples:

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

4.4.2 声明可选(optional)参数

不只是一个标识,一个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也必须应该有一个默认值。

 

Examples:

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

4.4.3 声明关键字(keyword)参数

一个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]参数指定一个带一个默认值的关键字参数。

 

Examples:

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

 

Examples:

(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)”中提供了更多函数表达式的内容。

4.4.4 实参数量感知函数:case-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给匹配给定参数数量的第一种情况。

 

Examples:

(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函数不能直接支持可选参数或关键字参数。

 

4.5 定义:define

一个基本定义具为如下表:

(define id expr)

在这种情况下,id被绑定到expr的结果。

 

Examples:

(define salutation (list-ref '("Hi" "Hello") (random 2)))
 
> salutation

"Hi"

 

4.5.1 函数简写

define表还支持函数定义的一个简写:

(define (id arg ...) body ...+)

这是以下内容的简写:

(define id (lambda (arg ...body ...+))

 

Examples:

(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-idbody ...+)

它是以下内容的一个简写:

(define id (lambda (arg ... . rest-idbody ...+))

 

Examples:

(define (avg . l)
  (/ (apply + l) (length l)))
 
> (avg 1 2 3)

2

 

4.5.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 argsbody ...+)
 
head   =   id
    |   (head args)
         
args   =   arg ...
    |   arg ... . rest-id

这个简写的扩展有一个给定义中的每个head的嵌套lambda表,其最里面的head与最外面的lambda对应。

4.5.3 多值和define-values

一个Racket表达式通常产生一个单独的结果,但有些表达式可以产生多个结果。例如,quotientremainder各自产生一个值,但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 <first> <last> name"))))
 
> (split-name "Adam Smith")

"Adam"

"Smith"

 

define-values表同时绑定多个标识到产生于一个单表达式的多个结果:

(define-values (id ...) expr)

expr产生的结果数量必须与id的数量相匹配。

 

Examples:

(define-values (given surname) (split-name "Adam Smith"))
 
> given

"Adam"

> surname

"Smith"

 

一个define表(不是一个函数简写)等价于一个带有一个单个iddefine-values表。

在《Racket参考》中的“(define)”部分提供了更多关于定义的内容。

4.5.4 内部定义

当一个句法表的语法指定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序列中的内部定义是相互递归的,也就是说,任何定义都可以引用任何其它定义——只要这个引用在定义发生之前没有实际被求值。如果一个定义被过早引用,一个错误就会发生。

 

Examples:

(define (weird)
  (define x x)
  x)
 
> (weird)

x: undefined;

 cannot use before initialization

 

内部定义的一个序列只使用define很容易转换为一个等效的letrec表(如同在下一节中介绍的)。然而,其它的定义表可以表现为一个body,包括define-valuesstruct(见《程序员定义的数据类型》)或define-syntax(见《》)。

在《Racket参考》文档的“(intdef-body)”部分有内部定义更多知识点。

4.6 局部绑定

虽然内部define可用于局部绑定,Racket提供了三种表,它们给予程序员在绑定方面的更多控制:let、let*和letrec。

4.6.1 并行绑定:let

在《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是按顺序求值的。

4.6.2 顺序绑定:let*

在《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")

4.6.3 递归绑定:letrec

在《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

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绑定一个只在函数主体中可见的函数标识,并且用一些初始表达式的值隐式调用函数。

 

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)

 

4.7 条件

大多数函数都可用于分支,如<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

4.7.1 简单分支:if

在《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的副作用,使用whenunless,对此我们将在后边《定序》部分描述。

4.7.2 组合测试:andor

在《Racket参考》的“(if)”部分有关于andor的文档。

Racket的andor是语法表,而不是函数。不像一个函数,如果前边的一个求值确定了答案,andor表会忽略后边表达式的求值。

(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

 

如果求值达到一个andor}表的最后的expr,那么expr的值直接决定andor}的结果。因此,最后的expr是在尾部的位置,这意味着上面的got-milk?函数在固定空间中运行。

尾递归》介绍尾部调用和尾部位置。

4.7.3 编链测试:cond

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表达式。

 

4.8 定序

Racket程序员喜欢编写尽可能少副作用的程序,因为纯粹的函数式代码更容易测试及组成更大的程序。然而,与外部环境的交互需要定序,例如写入一个显示器、打开一个图形窗口或在磁盘上操作一个文件时。

4.8.1 前效应:begin

在《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)

****

***

**

*

 

有多种表,比如lambdacond支持一系列甚至没有一个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)

 

这种拼接行为主要用于宏,我们稍后在《》中讨论。

4.8.2 后效应:begin0

在《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

 

4.8.3 if效应:whenunless

在《Racket参考》的“(when+unless)”部分也有关于whenunless的文档。

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)

****

***

**

*

 

4.9 赋值:set!

在《Racket参考》的“(set!)”中也有关于set!的文档。

使用set!赋值给一个变量:

(set! id expr)

一个set!表达式对expr求值并改变id(它必须限制在闭括号的环境内)为这个结果值。set!表达式自己的结果是#<void>。

 

Examples:

(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

4.9.1 使用赋值的指导原则

虽然使用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!显然没有改善程序的风格。

4.9.2 多值赋值:set!-values

在《Racket参考》中的“(set!)”有关于set!-values的文档。

set!-values表一次赋值给多个变量,给一个生成值的一个适当数值的表达式:

(set!-values (id ...) expr)

这个表等价于使用let-values以从expr接收多个结果,然后将结果使用set!单独赋值给id

 

Examples:

(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

 4.10 引用:quote和'

在《Racket参考》中“(quote)”部分也有关于quote的文档。

quote表产生一个常数:

(quote datum)

datum的语法在技术上被指定为read函数解析为一个单个元素的任何内容。quote表的值是相同的值,其read将产生给定的datum

datum可以是一个符号、一个布尔值、一个数值、一个(字符或字节)字符串、一个字符、一个关键字、一个空列表、一个包含更多类似值的点对(或列表),一个包含更多类似值的向量,一个包含更多类似值的哈希表,或者一个包含其它类似值的格子。

 

Examples:

> (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不能作为一个以#<开始的打印呈现,所以不能是#<void>、#<undefined>或一个过程。

quote表很少用于一个datum,它是一个布尔值、数字或字符串本身,因为这些值的打印表已经可以用作常量。quote表更常用于符号和列表,当没有被引用时,它具有其它含义(标识、函数调用等等)。

一个表达式:

'datum

是以下内容的一个简写

(quote datum)

这个简写几乎总是用来代替quote。这个简写甚至应用于datum中,因此它可以生成一个包含quote的列表。

在《Racket参考》里的“(parse-quote)”提供有更多关于'简写的内容。

 

Examples:

> 'apple

'apple

> '"hello"

"hello"

> '(1 2 3)

'(1 2 3)

> (display '(you can 'me))

(you can (quote me))

 

4.11 准引用:quasiquote和‘

在《Racket参考》中的“(quasiquote)”部分也有关于quasiquote的文档。

quasiquote表类似于quote:

(quasiquote datum)

然而,对出现在datum之中的每个(unquote expr),expr被求值以产生一个替代unquote子表的值。

 

Example:

> (quasiquote (1 2 (unquote (+ 1 2)) (unquote (- 5 1))))

'(1 2 3 4)

 

此表可用于编写根据特定模式建造列表的函数。

 

Examples:

> (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》那样的教科书之后)。)

 

Examples:

> (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表必须出现在一个产生一个列表或一个向量的上下文里。顾名思义,这个结果列表被拼接到它自己使用的上下文中。

 

Example:

> (quasiquote (1 2 (unquote-splicing (list (+ 1 2) (- 5 1))) 5))

'(1 2 3 4 5)

 

使用拼接,我们可以修改上边我们的示例表达式的构造,以只需要一个单个的let表达式和一个单个的+表达式。

 

Examples:

> (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表被需要。

 

Examples:

> (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的速记形式将被使用:`(即一个反引号)和,(即一个逗号)。同样的简写可在表达式中使用:

 

Example:

> `(1 2 `(,(+ 1 2) ,,(- 5 1)))

'(1 2 `(,(+ 1 2) ,4))

 

unquote-splicing的简写形式是,@:

 

Example:

> `(1 2 ,@(list (+ 1 2) (- 5 1)))

'(1 2 3 4)

4.12 简单分派:case

通过将一个表达式的结果与子句的值相匹配,case表分派到一个从句:

(case expr
  [(datum ...+) body ...+]
  ...)

每个datum将使用equal?对比expr的结果,然后相应的body被求值。case表可以为N个datum在O(log N)时间内分派正确的从句。

可以给每个从句提供多个datum,而且如果任何一个datum匹配,那么相应的body被求值。

 

Example:

> (let ([v (random 6)])
    (printf "~a\n" v)
    (case v
      [(0) 'zero]
      [(1) 'one]
      [(2) 'two]
      [(3 4 5) 'many]))

3

'many

 

一个case表的最后从句可以使用else,就像cond那样:

 

Example:

> (case (random 6)
    [(0) 'zero]
    [(1) 'one]
    [(2) 'two]
    [else 'many])

'zero

 

对于更一般的模式匹配(但没有分派时间保证),使用match,这个会在《模式匹配》中介绍。

 

4.13 动态绑定:parameterize

在《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表仅因为在当前线程中的求值而调整一个参数的值,以避免与其它线程竞争。

 

程序员定义的数据类型

在《Racket参考》中的“(structures)”部分也有关于数据结构类型的文档。

新的数据类型通常用struct表来创造,这是本章的主题。基于类的对象系统,我们参照《类和对象》,为创建新的数据类型提供了一种替换机制,但即使是类和对象也是根据结构类型实现。

5.1 简单的结构类型:struct

在《Racket参考》中的“(define-struct)”部分也有关于struct的文档。

作为一个最接近的,struct的语法是

(struct struct-id (field-id ...))
Examples:

(struct posn (x y))

struct表绑定struct-id和一个标识符的数值,它构建于struct-idfield-id

  • struct-id:一个构造器(constructor)函数,带有和acket[_field-id]的数值一样多的参数,并返回这个结构类型的一个实例。

    Example:
    > (posn 1 2)

    #<posn>

  • struct-id?:一个判断(predicate)函数,它带一个单个参数,同时如果它是这个结构类型的一个实例则返回#t,否则返回#f。

    Examples:
    > (posn? 3)

    #f

    > (posn? (posn 1 2))

    #t

  • struct-id-field-id:对于每个field-id,一个访问器(accessor)从这个结构类型的一个实例中解析相应字段的值。

    Examples:
    > (posn-x (posn 1 2))

    1

    > (posn-y (posn 1 2))

    2

  • struct:struct-id:一个结构类型描述符(structure type descriptor),它是一个值,体现结构类型作为一个第一类值(与#:super,在《更多的结构类型选项》中作为后续讨论)。

一个struct表对值的种类不设置约束条件,它可表现为这个结构类型的一个实例中的字段。例如,(posn "apple" #f)过程产生一个posn实例,即使"apple"和#f对posn实例的显性使用是无效的配套。执行字段值上的约束,比如要求它们是数值,通常是一个合约的工作,在《合约》中作为后续讨论。

5.2 复制和更新

struct-copy表克隆一个结构并可选地更新克隆中的指定字段。这个过程有时称为一个功能性更新(functional update),因为这个结果是一个带有更新字段值的结构。但原始的结构没有被修改。

(struct-copy struct-id struct-expr [field-id expr] ...)

出现在struct-copy后面的struct-id必须是由struct绑定的结构类型名称(即这个名称不能作为一个表达式直接被使用)。struct-expr必须产生结构类型的一个实例。结果是一个新实例,就像旧的结构类型一样,除这个被每个field-id标明的字段得到相应的expr的值之外。

Examples:
> (define p1 (posn 1 2))
> (define p2 (struct-copy posn p1 [x 3]))
> (list (posn-x p2) (posn-y p2))

'(3 2)

> (list (posn-x p1) (posn-x p2))

'(1 3)

5.3 结构子类型

struct的一个扩展表可以用来定义一个结构子类型(structure subtype),它是一种扩展一个现有结构类型的结构类型:

(struct struct-id super-id (field-id ...))

这个super-id必须是一个由struct绑定的结构类型名称(即名称不能被作为一个表达式直接使用)。

Examples:
(struct posn (x y))
(struct 3d-posn posn (z))

一个结构子类型继承其超类型的字段,并且子类型构造器接受在超类型字段的值之后的子类型字段的值。一个结构子类型的一个实例可以被用作这个超类型的判断和访问器。

Examples:
> (define p (3d-posn 1 2 3))
> p

#<3d-posn>

> (posn? p)

#t

> (3d-posn-z p)

3

; 3d-posn有一个x字段,但是这里却没有3d-posn-x选择器:
> (3d-posn-x p)

3d-posn-x: undefined;

 cannot reference undefined identifier

; 使用基类型的posn-x选择器去访问x字段:
> (posn-x p)

1

5.4 不透明结构类型与透明结构类型对比

用一个结构类型定义如下:

(struct posn (x y))

结构类型的一个实例以不显示关于字段值的任何信息的方式打印。也就是说,默认的结构类型是不透明的(opaque)。如果一个结构类型的访问器和修改器对一个模块保持私有,那么没有其它的模块可以依赖这个类型实例的表示。

让一个结构类型透明(transparent),在字段名序列后面使用#:transparent关键字:

(struct posn (x y)
        #:transparent)
 
> (posn 1 2)

(posn 1 2)

一个透明结构类型的一个实例像一个对构造器的调用一样打印,因此它显示这个结构字段值。一个透明结构类型也允许反射操作,比如struct?struct-info,在其实例中被使用(参见《反射和动态求值》)。

默认情况下,结构类型是不透明的,因为不透明的结构实例提供了更多的封装保证。也就是说,一个库可以使用不透明的结构来封装数据,而库中的客户机除了在库中被允许之外,也不能操纵结构中的数据。

5.5 结构的比较

一个通用的equal?比较自动出现在一个透明的结构类型的字段上,但是equal?默认仅针对不透明结构类型的实例标识:

(struct glass (width height) #:transparent)
 
> (equal? (glass 1 2) (glass 1 2))

#t

(struct lead (width height))
 
> (define slab (lead 1 2))
> (equal? slab slab)

#t

> (equal? slab (lead 1 2))

#f

通过equal?支持实例比较而不需要使结构型透明,你可以使用#:methods关键字、gen:equal+hash并执行三个方法:

(struct lead (width height)
  #:methods
  gen:equal+hash
  [(define (equal-proc a b equal?-recur)
     ; 比较a和b
     (and (equal?-recur (lead-width a) (lead-width b))
          (equal?-recur (lead-height a) (lead-height b))))
   (define (hash-proc a hash-recur)
     ; 计算首要的a哈希代码。
     (+ (hash-recur (lead-width a))
        (* 3 (hash-recur (lead-height a)))))
   (define (hash2-proc a hash2-recur)
     ; 计算次重要的a哈希代码。
     (+ (hash2-recur (lead-width a))
             (hash2-recur (lead-height a))))])
 
> (equal? (lead 1 2) (lead 1 2))

#t

列表中的第一个函数实现对两个lead的equal?测试;函数的第三个参数是用来代替equal?实现递归的相等测试,以便这个数据循环可以被正确处理。其它两个函数计算以哈希表(hash tables)使用的首要的和次重要的哈希代码:

> (define h (make-hash))
> (hash-set! h (lead 1 2) 3)
> (hash-ref h (lead 1 2))

3

> (hash-ref h (lead 2 1))

hash-ref: no value found for key

  key: #<lead>

拥有gen:equal+hash的第一个函数不需要递归比较结构的字段。例如,表示一个集合的一个结构类型可以通过检查这个集合的成员是相同的来执行相等,独立于内部表示的元素顺序。只是要注意哈希函数对任何两个假定相等的结构类型产生相同的值。

5.6 结构类型的生成性

每次对一个struct表求值时,它就生成一个与所有现有结构类型不同的结构类型,即使某些其它结构类型具有相同的名称和字段。

这种生成性对强制抽象和执行程序(比如口译员)是有用的,但小心放置一个struct表到被多次求值的位置。

Examples:
(define (add-bigger-fish lst)
  (struct fish (size) #:transparent) ; new every time
  (cond
   [(null? lst) (list (fish 1))]
   [else (cons (fish (* 2 (fish-size (car lst))))
               lst)]))
 
> (add-bigger-fish null)

(list (fish 1))

> (add-bigger-fish (add-bigger-fish null))

fish-size: contract violation;

 given value instantiates a different structure type with

the same name

  expected: fish?

  given: (fish 1)

(struct fish (size) #:transparent)
(define (add-bigger-fish lst)
  (cond
   [(null? lst) (list (fish 1))]
   [else (cons (fish (* 2 (fish-size (car lst))))
               lst)]))
 
> (add-bigger-fish (add-bigger-fish null))

(list (fish 2) (fish 1))

5.7 预制结构类型

虽然一个透明结构类型以显示内容的方式打印,但不像一个数值、字符串、符号或列表的打印表,结构的打印表不能用在一个表达式中以找回结构。

一个预制(prefab)(“被预先制造”)结构类型是一个内置的类型,它是已知的Racket打印机和表达式阅读器。有无限多这样的类型存在,并且它们通过名字、字段计数、超类型以及其它细节来索引。一个预制结构的打印表类似于一个向量,但它以#s开始而不是仅以#开始,而且打印表的第一个元素是预制结构类型的名称。

下面的示例显示具有一个字段的sprout预置结构类型的实例。第一个实例具有一个字段值'bean,以及第二个具有字段值'alfalfa:

> '#s(sprout bean)

'#s(sprout bean)

> '#s(sprout alfalfa)

'#s(sprout alfalfa)

像数字和字符串一样,预置结构是“自引用”,所以上面的引号是可选的:

> #s(sprout bean)

'#s(sprout bean)

当你用struct使用#:prefab关键字,而不是生成一个新的结构类型,你获得与现有的预制结构类型的绑定:

> (define lunch '#s(sprout bean))
> (struct sprout (kind) #:prefab)
> (sprout? lunch)

#t

> (sprout-kind lunch)

'bean

> (sprout 'garlic)

'#s(sprout garlic)

上面的字段名kind对查找预置结构类型无关紧要,仅名称sprout和字段数量是紧要的。同时,具有三个字段的预制结构类型sprout是一种不同于一个单个字段的结构类型:

> (sprout? #s(sprout bean #f 17))

#f

> (struct sprout (kind yummy? count) #:prefab) ; redefine
> (sprout? #s(sprout bean #f 17))

#t

> (sprout? lunch)

#f

一个预制结构类型可以有另一种预制结构类型作为它的超类型,它具有可变的字段,并它可以有自动字段。这些维度中的任何变化都对应于不同的预置结构类型,而且结构类型名称的打印表编码所有的相关细节。

> (struct building (rooms [location #:mutable]) #:prefab)
> (struct house building ([occupied #:auto]) #:prefab
    #:auto-value 'no)
> (house 5 'factory)

'#s((house (1 no) building 2 #(1)) 5 factory no)

每个预制结构类型都是透明的——但甚至比一个透明类型更抽象,因为可以创建实例而不必访问一个特定的结构类型声明或现有示例。总体而言,结构类型的不同选项提供了从更抽象到更方便的一连串可能性:

  • 不透明的(Opaque)(默认):没有访问结构类型声明,就不能检查或创造实例。正如下一节所讨论的,构造器看守(constructor guards)属性(properties)可以附加到结构类型上以进一步保护或专门化其实例的行为。

  • 透明的(Transparent):任何人都可以检查或创建一个没有访问结构类型声明的实例,这意味着这个值打印机可以显示一个实例的内容。然而,所有实例创建都经过一个构造器看守,这样可以控制一个实例的内容,并且实例的行为可以通过属性(properties)进行特例化。由于结构类型由其定义生成,实例不能简单地通过结构类型的名称来制造,因此不能由表达式读取器自动生成。

  • 预制的(Prefab):任何人都可以在任何时候检查或创建一个实例,而不必事先访问一个结构类型声明或一个实例。因此,表达式读取器可以直接制造实例。实例不能具有一个构造器看守属性

由于表达式读取器可以生成预制实例,所以在便利的序列化(serialization)比抽象更重要时它们是有用的。然而,如果他们如《数据类型和序列化》所描述那样被用serializable-struct定义,不透明透明的结构也可以被序列化。}]

5.8 更多的结构类型选项

无论是在结构类型级还是在个别字段级上,struct的完整语法支持许多选项:

(struct struct-id maybe-super (field ...)
        struct-option ...)
 
maybe-super   =  
 
    |   super-id
         
field   =   field-id
    |   [field-id field-option ...]

一个 struct-option总是以一个关键字开头:

#:mutable

会导致结构的所有字段是可变的,并且给每个field-id产生一个设置方式set-struct-id-field-id!,其在结构类型的一个实例中设置对应字段的值。

Examples:
> (struct dot (x y) #:mutable)
(define d (dot 1 2))
 
> (dot-x d)

1

> (set-dot-x! d 10)
> (dot-x d)

10

#:mutable选项也可以被用来作为一个field-option,在这种情况下,它使一个个别字段可变。

Examples:
> (struct person (name [age #:mutable]))
(define friend (person "Barney" 5))
 
> (set-person-age! friend 6)
> (set-person-name! friend "Mary")

set-person-name!: undefined;

 cannot reference undefined identifier

#:transparent

对结构实例的控制反射访问,如前面一节《不透明结构类型与透明结构类型对比》所讨论的那样。

#:inspector inspector-expr

推广#:transparent以支持更多的控制访问或反射操作。

#:prefab

访问内置结构类型,如前一节《预制结构类型》所讨论的。

#:auto-value auto-expr

指定一个被用于所有在结构类型里的自动字段值,这里一个自动字段被#:auto字段被标示。这个构造函数不接受给自动字段的参数。自动字段无疑是可变的(通过反射操作),但设置函数仅在#:mutable也被指定的时候被绑定。

Examples:
> (struct posn (x y [z #:auto])
               #:transparent
               #:auto-value 0)
> (posn 1 2)

(posn 1 2 0)

#:guard guard-expr

每当一个结构类型的实例被创建,都指定一个构造器看守(constructor guard)过程以供调用。在结构类型中这个看守获取与结构类型中的非自动字段相同数量的参数,再加上一个实例化类型的名称(如果一个子类型被实例化,在这种情况下最好使用子类型的名称报告一个错误)。看守应该返回与给定值相同数量的值,减去名称参数。如果某个参数不可接受,或者它可以转换一个参数,则这个看守可以引发一个异常。

Examples:
> (struct thing (name)
          #:transparent
          #:guard (lambda (name type-name)
                    (cond
                      [(string? name) name]
                      [(symbol? name) (symbol->string name)]
                      [else (error type-name
                                   "bad name: ~e"
                                   name)])))
> (thing "apple")

(thing "apple")

> (thing 'apple)

(thing "apple")

> (thing 1/2)

thing: bad name: 1/2

即使子类型实例被创建,这个看守也会被调用。在这种情况下,只有被构造器接受的字段被提供给看守(但是子类型的看守同时获得子类型添加的原始字段和现有字段)。

Examples:
> (struct person thing (age)
          #:transparent
          #:guard (lambda (name age type-name)
                    (if (negative? age)
                        (error type-name "bad age: ~e" age)
                        (values name age))))
> (person "John" 10)

(person "John" 10)

> (person "Mary" -1)

person: bad age: -1

> (person 10 10)

person: bad name: 10

#:methods interface-expr [body ...]

使方法定义与关联到一个通用接口(generic interface)的结构类型关联。例如,执行gen:dict方法允许一个结构类型的实例用作字典。执行gen:custom-write方法允许一个如何被显示(display)的结构类型的一个实例的定制。

Examples:
> (struct cake (candles)
          #:methods gen:custom-write
          [(define (write-proc cake port mode)
             (define n (cake-candles cake))
             (show "   ~a   ~n" n #\. port)
             (show " .-~a-. ~n" n #\| port)
             (show " | ~a | ~n" n #\space port)
             (show "---~a---~n" n #\- port))
           (define (show fmt n ch port)
             (fprintf port fmt (make-string n ch)))])
> (display (cake 5))

   .....   

 .-|||||-.

 |       |

-----------

#:property prop-expr val-expr

使一个属性(property)和值与结构类型相关联。例如,prop:procedure属性允许一个结构实例用作一个函数;属性值决定当使用这个结构作为一个函数时一个调用如何被执行。

Examples:
> (struct greeter (name)
          #:property prop:procedure
                     (lambda (self other)
                       (string-append
                        "Hi " other
                        ", I'm " (greeter-name self))))
(define joe-greet (greeter "Joe"))
 
> (greeter-name joe-greet)

"Joe"

> (joe-greet "Mary")

"Hi Mary, I'm Joe"

> (joe-greet "John")

"Hi John, I'm Joe"

#:super super-expr

用于一个与struct-id紧邻的super-id的一个替代者。代替一个结构类型的这个名字(它是一个表达式),super-expr应该产生一个结构类型的描述符(structure type descriptor)值。#:super的一个优点是结构类型的描述符是值,所以可以被传递给过程。

Examples:
(define (raven-constructor super-type)
  (struct raven ()
          #:super super-type
          #:transparent
          #:property prop:procedure (lambda (self)
                                      'nevermore))
  raven)
 
> (let ([r ((raven-constructor struct:posn) 1 2)])
    (list r (r)))

(list (raven 1 2) 'nevermore)

> (let ([r ((raven-constructor struct:thing) "apple")])
    (list r (r)))

(list (raven "apple") 'nevermore)

在《Racket参考》中的“(structures)”里提供有更多关于数据结构类型的内容。

 

模块

模块让你把Racket代码组织成多个文件和可重用的库。

 

6.1 模块基础

每个Racket模块通常驻留在自己的文件中。例如,假设文件"cake.rkt"包含以下模块:

"cake.rkt"

#lang racket
 
(provide print-cake)
 
; 画一个带n支蜡烛的蛋糕。
(define (print-cake n)
  (show "   ~a   " n #\.)
  (show " .-~a-. " n #\|)
  (show " | ~a | " n #\space)
  (show "---~a---" n #\-))
 
(define (show fmt n ch)
  (printf fmt (make-string n ch))
  (newline))

然后,其它模块可以导入"cake.rkt"以使用print-cake函数,因为"cake.rkt"中的provide行明确导出这个定义print-cake。show函数对"cake.rkt"是私有的(即它不能从其它模块被使用),因为show没有被导出。

下面的"random-cake.rkt"模块导入"cake.rkt":

"random-cake.rkt"

#lang racket
 
(require "cake.rkt")
 
(print-cake (random 30))

如果"cake.rkt"和"random-cake.rkt"模块在同一个目录里,在导入(require "cake.rkt")中的这个相对引用内的引用"cake.rkt"就会工作。UNIX样式的相对路径用于所有平台上的相对模块引用,就像HTML页面中的相对的URL一样。

6.1.1 组织模块

"cake.rkt"和"random-cake.rkt"示例演示如何组织一个程序模块的最常用的方法:把所有的模块文件在一个目录(也许是子目录),然后有模块通过相对路径相互引用。模块的一个目录可以作为一个项目,因为它可以在文件系统上移动或复制到其它机器上,并且相对路径保存模块之间的连接。

作为另一个例子,如果你正在构建一个糖果分类程序,你可能有一个主"sort.rkt"模块,它使用其它模块访问一个糖果数据库和一个控制分拣机。如果这个糖果数据库模块本身被组织进了处理条码和厂家信息的子模块,那么这个数据库模块可以是"db/lookup.rkt",它使用辅助器模块"db/barcodes.rkt"和"db/makers.rkt"。同样,这个分拣机驱动器"machine/control.rkt"可能会使用辅助器模块"machine/sensors.rkt"和"machine/actuators.rkt"。

"sort.rkt"模块使用相对路径"db/lookup.rkt"和"machine/control.rkt"从数据库和机器控制库导入:

"sort.rkt"

#lang racket
(require "db/lookup.rkt" "machine/control.rkt")
....

"db/lookup.rkt"模块类似地使用相对路径给它自己的源来访问"db/barcodes.rkt"和"db/makers.rkt"模块:

"db/lookup.rkt"

#lang racket
(require "barcode.rkt" "makers.rkt")
....

同上,"machine/control.rkt":

"machine/control.rkt"

#lang racket
(require "sensors.rkt" "actuators.rkt")
....

Racket工具所有工作自动使用相对路径。例如,

  racket sort.rkt

在命令行运行"sort.rkt"程序和自动加载并编译所需的模块。对于一个足够大的程序,从源编译可能需要很长时间,所以使用

  raco make sort.rkt

参见《raco:Racket命令行工具(raco:Racket Command-Line Tools)》中的“raco make: Compiling Source to Bytecode”部分以获取更多关于raco make的信息。

编译"sort.rkt"及其所有依赖成为字节码文件。如果字节码文件存在,运行racket sort.rkt,将自动使用字节码文件。

6.1.2 库集合

一个集合(collection)是一个已安装的库模块的按等级划分的组。一个集合中的一个模块通过一个引号引用,无后缀路径。例如,下面的模块引用"date.rkt"库,它是"racket"集合的一部分:

#lang racket
 
(require racket/date)
 
(printf "Today is ~s\n"
        (date->string (seconds->date (current-seconds))))

当你搜索在线Racket文档时,搜索结果显示提供每个绑定的模块。或者,如果你通过单击超链接到达一个绑定文档,则可以在绑定名称上悬停以查找哪些模块提供了它。

一个模块的引用,像racket/date,看起来像一个标识,但它并不是和printfdate->string相同的方式对待。相反,当require发现一个被引号包括的模块引用,它转化这个引用为一个基于集合的模块路径:

  • 首先,如果这个引用路径不包含/,那么require自动添加一个"/main"给这个引用。例如,(require slideshow)等价于(require slideshow/main)。

  • 其次,require隐式添加一个".rkt"后缀给这个路径。

  • 最后,require在已安装的集合中通过搜索来决定路径,而不是将路径处理为相对于封闭模块的路径。

作为一个最近似情况,一个集合被实现为一个文件系统目录。例如,"racket"集合大多位于"racket"安装的"collects"目录中的一个"racket"目录中,如以下报告:

#lang racket
 
(require setup/dirs)
 
(build-path (find-collects-dir) ; 主集合目录
            "racket")

然而,Racket安装的"collects"目录仅仅是一个require寻找集合目录的地方。其它地方包括用户指定通过(find-user-collects-dir)报告的目录以及通过PLTCOLLECTS搜索路径配置的目录。最后,并且最典型,集合通过安装的包(packages)找到。

6.1.3 包和集合

 

一个包(package)是通过Racket包管理器(或者作为一个预安装包括在一个Racket分发中)。例如,racket/gui库是由"gui"包提供的,而parser-tools/lex是由"parser-tools"库提供的。

更确切地说,racket/gui由 "gui-lib"提供,parser-tools/lex由"parser-tools-lib"提供,并且"gui"和"parser-tools"包用文档扩展"gui-lib"和"parser-tools-lib"。

 

Racket程序不直参考包(packages)。相反,程序通过集合(collections)参考库,添加或删除一个包改变可获得的基于集合库的集合。一个单个包可以在多个集合中提供库,并且两个不同的包可以在同一集合(但不是同一个库,并且包管理器确保安装的包在该层级不冲突)中提供库。

有关包的更多信息,请参阅《Racket中的包管理》(Package Management in Racket)。

6.1.4 添加集合

回顾《组织模块》部分的糖果排序示例,假设"db/"和"machine/"中的那个模块需要一套公共的助手函数。辅助函数可以被放在一个"utils/"目录里,同时"db/"或"machine/"中的模块可以用开始于"../utils/"的相对路径访问公用模块。只要一组模块在一个单一项目中协同工作,最好保持相对路径。一个程序员可以不用知道你的Racket配置而继承相对路径引用。

有些库是为了被用于跨多个项目,因此将库的源保存在一个目录内与它的使用没有意义。在这种情况下,最好的选择是添加一个新集合。这个库处于一个集合里后,它可以用一个非引用路径引用,就像是包括在Racket发行里的库一样。

你可以通过将文件放置在Racket安装包里或通过(get-collects-search-dirs)报告的一个目录下添加一个新的集合。或者,你可以通过设置PLTCOLLECTS环境变量添加到搜索目录列表。如果你设置PLTCOLLECTS,通过用冒号(UNIX和Mac OS)或分号(Windows)启动这个值包括一个空路径,从而保留原始搜索路径。然而,最好的选择是添加一个包。

创建一个包并不意味着你必须用一个包服务器或者执行一个复制你的源代码到一个归档格式中的绑定步骤注册。创建一个包只简单地意味着使用包管理器将你的库作为一个来自它们当前源位置的的集合的本地访问。

例如,假设你有一个目录"/usr/molly/bakery",它包含"cake.rkt"模块(来自于本节的开始部分)和其它相关模块。为了使模块可以作为一个"bakery"集合获取,或者

  • 使用raco pkg命令行工具:

      raco pkg install --link /usr/molly/bakery

    当所提供的路径包含一个目录分隔符时,这里--link标记实际上不需要。

  • 从File(文件)菜单使用DrRacket的Package Manager(包管理器)项。在Do What I Mean(做我打算的)面板,点击Browse...(浏览……),选择"/usr/molly/bakery"目录,并且单击Install(安装)。

之后,从任何模块中(require bakery/cake)将从"/usr/molly/bakery/cake.rkt"输入print-cake函数。

默认情况下,你安装的目录的名称既用作包名称,又用作包提供的集合。同样,包管理器通常默认只为当前用户安装,而不是在一个Racket安装的所有用户。有关更多信息,请参阅《Racket中的包管理(Package Management in Racket)。

如果你打算分发你的库给其他人,请仔细选择集合和包名称。集合名称空间是分层的,但顶级集合名是全局的,包名称空间是扁平的。考虑将一次性库放在一些顶级名称下,就像"molly"这种标识制造器。在制作烘焙食品库的最终集合时,使用像"bakery"这样的一个集合名。

在你的库被放入一个集合之后,你仍然可以使用raco make以编译库源,但更好而且更方便的是使用raco setup。raco setup命令取得一个集合名(而不是一个文件名)并编译集合内所有的库。此外,raco setup可以建立文档以收集和添加文档到文档索引,作为通过集合中的一个"info.rkt"模块做详细说明。有关raco setup的详细信息请看《raco设置:安装管理(raco setup: Installation Management)》。

 

6.2 模块语法

在一个模块文件的开始的这个#lang开始对一个module表的一个简写,很像'是对一个quote表的一个简写。不同于',#lang简写在REPL内不能正常执行,部分是因为它必须由一个文件结束(end-of-file)终止,也因为#lang的普通写法依赖于封闭文件的名称。

6.2.1 module

既可在REPL又可在一个文件中工作的一个模块声明的普通写法表,是

(module name-id initial-module-path
  decl ...)

其中的name-id是模块的一个名称,initial-module-path是一个初始的输入口,每个decl是一个输入口、输出口、定义或表达式。在一个文件的情况下,name-id通常匹配包含文件的名称,减去其目录路径或文件扩展名,但在模块通过其文件路径requirename-id被忽略。

initial-module-path是必需的,因为为了在模块主体中进一步使用,require表更必须被输入。换句话说,initial-module-path输入引导语法,它在主体内可被使用。最常用的initial-module-path是racket,它提供了本指南中描述的大部分绑定,包括requiredefineprovide。另一种常用的initial-module-path是racket/base,它提供了较少的功能,但仍然是大多数最常用的函数和语法。

例如,前面一节的"cake.rkt"例子可以编写为

(module cake racket
  (provide print-cake)
 
  (define (print-cake n)
    (show "   ~a   " n #\.)
    (show " .-~a-. " n #\|)
    (show " | ~a | " n #\space)
    (show "---~a---" n #\-))
 
  (define (show fmt n ch)
    (printf fmt (make-string n ch))
    (newline)))

此外,这个module表可以在一个REPL中被求值以申明一个cake模块,它不与任何文件相关联。为指向是这样一个独立模块,这样引用模块名称:

 

Examples:
> (require 'cake)
> (print-cake 3)

   ...   

 .-|||-.

 |     |

---------

 

声明一个模块不会立即求值这个模块的主体定义和表达式。这个模块必须在顶层明确地被require以触发求值。在求值被触发一次之后,后续的require不会重新求值模块主体。

 

Examples:
> (module hi racket
    (printf "Hello\n"))
> (require 'hi)

Hello

> (require 'hi)

 

6.2.2 #lang简写

一个#lang简写的主体没有特定的语法,因为这个语法是由接着的#lang语言名称确定。

在#lang racket的情况下,语法为:

#lang racket
decl ...

其如同以下内容读取:

(module name racket
  decl ...)

这里name是衍生自包含#lang表的文件的名称。

#lang racket/base表具有和#lang racket同样的语法,除了普通写法的扩展使用racket/base而不是racket。#lang scribble/manual表相反,有一个完全不同的语法,甚至看起来不像Racket,在这个指南里我们不准备去描述。

除非另有规定,被作为一个使用#lang记号的“语言”文件化的一个模块将以和#lang racket同样的方式扩展到module。这个文件化的语言名称也可以用modulerequire来直接使用。

6.2.3 子模块

一个module表可以被嵌套在一个模块内,在这种情况下,这个嵌套的module表声明一个子模块(submodule)。子模块可以被使用一个引用名称的外围模块直接引用。下面的例子通过从zoo子模块输入tiger打印"Tony":

"park.rkt"

#lang racket
 
(module zoo racket
  (provide tiger)
  (define tiger "Tony"))
 
(require 'zoo)
 
tiger

运行一个模块不是必须运行其子模块。在上面的例子中,运行"park.rkt"来运行它的子模块zoo仅因为"park.rkt"模块require这个zoo子模块。否则,一个模块及其每一个子模块可以独立运行。此外,如果"park.rkt"被编译成一个字节码文件(通过raco make),那么"park.rkt"代码或zoo代码可以独立加载。

子模块可以嵌套于子模块,而且一个子模块可以被一个模块而不是其外围模块通过使用一个子模块路径(submodule path)直接引用。

一个module*表类似于一个嵌套的module表:

(module* name-id initial-module-path-or-#f
  decl ...)

module*表不同于module在于它反转这个对于子模块和外围模块的参考的可能性:

  • module申明的一个子模块模块可被其外围模块require,但这个子模块不能require这个外围模块或在词法上参考外围模块的绑定。

  • module*申明的一个子模块可以require其外围模块,但是这个外围模块不能require这个子模块。

此外,一个module*表可以在一个initial-module-path的位置指定#f,在这种情况下,子模块领会所有外围模块的绑定——包括没有使用provide输出的绑定。

module*和#f申明的子模块的一个使用是通过一个并不从这个模块通常输出的子模块输出附加绑定:

"cake.rkt"

#lang racket
 
(provide print-cake)
 
(define (print-cake n)
  (show "   ~a   " n #\.)
  (show " .-~a-. " n #\|)
  (show " | ~a | " n #\space)
  (show "---~a---" n #\-))
 
(define (show fmt n ch)
  (printf fmt (make-string n ch))
  (newline))
 
(module* extras #f
  (provide show))

在这个修订的"cake.rkt"模块里,show不是被一个采用(require "cake.rkt")的模块输入,因为大部分"cake.rkt"的客户端不想要这个额外的函数。一个模块可以需要这个使用(require (submod "cake.rkt" extras))访问另外的隐藏show函数的extra子模块。

6.2.4 main和test子模块

下面"cake.rkt"的变体包括一个调用print-cake的main子模块:

"cake.rkt"

#lang racket
 
(define (print-cake n)
  (show "   ~a   " n #\.)
  (show " .-~a-. " n #\|)
  (show " | ~a | " n #\space)
  (show "---~a---" n #\-))
 
(define (show fmt n ch)
  (printf fmt (make-string n ch))
  (newline))
 
(module* main #f
  (print-cake 10))

运行一个模块不会运行其module*定义的子模块。尽管如此,还是可以通过racket或DrRacket运行上面的模块打印一个带10支蜡烛的蛋糕,因为main子模块是一个特殊情况。

当一个模块作为一个程序名称提供给racket可执行文件或在DrRacket中直接运行,如果这个模块有一个main子模块,这个main子模块会在其外围模块之后运行。当一个模块直接运行时,声明一个main子模块从而指定额外的行为去被执行,以代替require作为在一个更大的程序里的一个库。

一个main子模块不必用module*声明。如果main模块不需要使用其外围模块的绑定,则可以被用module来声明。更通常的是,main使用module+来声明:

(module+ name-id
  decl ...)

module+申明的一个子模块就像一个以使用#f作为其initial-module-pathmodule*申明的子模块。此外,多个module+表可以指定相同的子模块名称,在这种情况下,module+表的主体被组合以创建一个单独的子模块。

module+的组合行为对定义一个test子模块是非常有用的,它可以使用raco test方便地运行,用同样的方式main也可以方便地使用racket运行。例如,下面的"physics.rkt"模块输出drop和to-energy函数,并且它定义了一个test模块以支持单元测试:

"physics.rkt"

#lang racket
(module+ test
  (require rackunit)
  (define ε 1e-10))
 
(provide drop
         to-energy)
 
(define (drop t)
  (* 1/2 9.8 t t))
 
(module+ test
  (check-= (drop 0) 0 ε)
  (check-= (drop 10) 490 ε))
 
(define (to-energy m)
  (* m (expt 299792458.0 2)))
 
(module+ test
  (check-= (to-energy 0) 0 ε)
  (check-= (to-energy 1) 9e+16 1e+15))

输入"physics.rkt"到一个更大的程序不会运行drop和to-energy测试——即使引发这个测试代码的加载,如果模块被编译——但在一个命令行中运行raco test physics.rkt会运行这个测试。

上边的"physics.rkt"模块相当于使用module*

"physics.rkt"

#lang racket
 
(provide drop
         to-energy)
 
(define (drop t)
  (* 1/2 49/5 t t))
 
(define (to-energy m)
  (* m (expt 299792458 2)))
 
(module* test #f
  (require rackunit)
  (define ε 1e-10)
  (check-= (drop 0) 0 ε)
  (check-= (drop 10) 490 ε)
  (check-= (to-energy 0) 0 ε)
  (check-= (to-energy 1) 9e+16 1e+15))

使用module+代替module*允许对交错函数定义进行测试。

module+的组合行为有时也是对一个main模块有帮助的。即使在组合不被需要时,(module+ main ....)因为它比(module* main #f ....)更具可读性而是首选。

 

6.3 模块路径

一个模块路径(module path)是对一个模块的一个引用,作为require的使用,或者作为一个module表中的initial-module-path。它可以是几种表中的任意一种:

(quote id)

一个引用标识的一个模块路径(module path)指的是使用这个标识的一个非文件module声明。模块引用的这种表在一个REPL中具有更多意义。

 

Examples:
> (module m racket
    (provide color)
    (define color "blue"))
> (module n racket
    (require 'm)
    (printf "my favorite color is ~a\n" color))
> (require 'n)

my favorite color is blue

 

rel-string

一个字符串模块路径是使用UNIX样式约定的一个相对路径:/是路径分隔符,..指父目录,.指同一目录。rel-string不必以一个路径分隔符开始或结束。如果路径没有后缀,".rkt"会自动添加。

这个路径是相对于封闭文件,如果有的话,或者是相对于当前目录。(更确切地说,路径是相对于 (current-load-relative-directory)的值),这是在加载一个文件时设置的。

模块基础》使用相对路径显示示例。

如果一个相对路径以一个".ss"后缀结尾,它会被转换成".rkt"。如果实现引用模块的文件实际上以".ss"结束,当试图加载这个文件(但一个".rkt"后缀优先)时后缀将被改回来。这种双向转换提供了与Racket旧版本的兼容。

id

作为一个引用标识的一个模块路径引用一个已经安装的库。这个id约束只包含ASCII字母、ASCII数字、+、-、_和/,这里/分隔标识内的路径元素。这个元素指的是集合(collection)和子集合(sub-collection),而不是目录和子目录。

这种表的一个例子是racket/date。它是指模块的源是"racket"集合中的"date.rkt"文件,它被安装为Racket的一部分。".rkt"后缀被自动添加。

这种表的另一个例子是racket,它通常被使用在初始输入时。这个路径racket是对racket/main的简写;当一个id没有/,那么/main自动被添加到结尾。因此,racket或racket/main指的是其源是"racket"集合里的"main.rkt"文件的模块。

 

Examples:
> (module m racket
    (require racket/date)
  
    (printf "Today is ~s\n"
            (date->string (seconds->date (current-seconds)))))
> (require 'm)

Today is "Monday, November 14th, 2022"

 

当一个模块的完整路径以".rkt"结束时,如果没有这样的文件存在但有一个".ss"后缀的文件存在,那么这个".ss"后缀自动被替代。这种转换提供了与Racket旧版本的兼容。

(lib rel-string)

像一个非引号标识路径,但表示为一个字符串而不是一个标识。另外,rel-string可以以一个文件后缀结束,在这种情况下,".rkt"不被自动添加。

这种表的例子包括(lib "racket/date.rkt")和(lib "racket/date"),这等效于racket/date。其它的例子包括(lib "racket")、(lib "racket/main")和(lib "racket/main.rkt"),都等效于racket。

 

Examples:
> (module m (lib "racket")
    (require (lib "racket/date.rkt"))
  
    (printf "Today is ~s\n"
            (date->string (seconds->date (current-seconds)))))
> (require 'm)

Today is "Monday, November 14th, 2022"

 

(planet id)

通过PLaneT服务器访问一个被分发的第三方库。这个库在其被需要的第一时间被下载,然后使用这个本地副本。

这个id编码了被一个/分隔的几条信息:包所有者,然后是带可选版本信息的包名称,以及对一个带包的特定库的一个可选路径。就像id作为一个 lib路径、一个被自动添加的".rkt"后缀以及在没有子路径提供时被用作路径的/main的简写。

 

Examples:
> (module m (lib "racket")
    ; 使用"schematics"的"random.plt" 1.0, 文件"random.rkt":
    (require (planet schematics/random:1/random))
    (display (random-gaussian)))
> (require 'm)

0.9050686838895684

 

如同其它表,如果没有以".rkt"结尾的执行文件存在,一个以".ss"结尾的实现文件可以自动被取代。

(planet package-string)

就像一个planet的符号表,但使用一个字符串而不是一个标识。同样,package-string可以以一个文件后缀结束,在这种情况下,".rkt"不被添加。

与其它表一样,在如果没有以".rkt"结束的的执行文件存在而一个以".ss"结束的实现文件可以自动被替代时,一个".ss"扩展名转换为".rkt"。

(planet rel-string (user-string pkg-string vers ...))
 
vers   =   nat
    |   (nat nat)
    |   (= nat)
    |   (+ nat)
    |   (- nat)

一般更通用的表去访问来自于PLaneT服务器的一个库。在这种通用表中,一个PLaneT引用像一个有一个相对路径的lib引用那样开始,但这个路径后面跟随关于库的制造者、包和版本的信息。指定的包被按需下载和安装。

这个vers在这个包的可接受版本上指定一个约束,这里一个版本号是一个非负整数序列,并且这个约束确定序列中的每个元素的允许值。如果没有为一个特定元素提供约束,则任何版本被允许;特别是,省略所有vers意味着任何版本都被接受。强烈推荐至少指定一个vers

对于一个版本约束,一个单纯的nat与(+ nat)相同,其匹配nat或高于这个版本号的相应元素。一个(start-nat end-nat)匹配包括在start-natend-nat范围内的任何数值。一个(= nat)恰好匹配nat。一个(- nat)匹配nat或更低的。

 

Examples:
> (module m (lib "racket")
    (require (planet "random.rkt" ("schematics" "random.plt" 1 0)))
    (display (random-gaussian)))
> (require 'm)

0.9050686838895684

 

自动的".ss"和".rkt"转换应用为其它表。

(file string)

指定一个文件,其string是一个使用当前平台的约定的相对或绝对路径。这个表不是轻量的,并且当一个单纯的、轻量的rel-string足够时,它应该不(not)被使用。

这个自动的".ss"和".rkt"转换应用为其它表。

(submod base element ...+)
 
base   =   module-path
    |   "."
    |   ".."
         
element   =   id
    |   ".."

是指base的一个子模块。在submod中的element的序列指定一个子模块名称的路径以到达最终的子模块。

 

Examples:
> (module zoo racket
    (module monkey-house racket
      (provide monkey)
      (define monkey "Curious George")))
> (require (submod 'zoo monkey-house))
> monkey

"Curious George"

 

使用"."作为在submod中的base代表外围模块。使用".."作为base等效于使用"."后跟一个额外的".."。当一个表(quote id)的路径指一个子模块时,它等效于(submod "." id)。

使用".."作为一个element取消一个子模块步骤,实际上指定外围模块。例如,(submod "..")指路径出现在其中的子模块的封闭模块。

 

Examples:
> (module zoo racket
    (module monkey-house racket
      (provide monkey)
      (define monkey "Curious George"))
    (module crocodile-house racket
      (require (submod ".." monkey-house))
      (provide dinner)
      (define dinner monkey)))
> (require (submod 'zoo crocodile-house))
> dinner

"Curious George"

 

6.4 输入:require

require表从其它模块输入。一个require表可以出现在一个模块中,在这种情况它将来自于指定模块的绑定引入到输入模块中。一个require表也可以出现在顶层,在这种情况它既输入绑定也实例化(instantiates)指定的模块;更确切地说,如果主体定义和指定模块的表达式还没有被求值则对其求值。

一个单个的require可以同时指定多个输入:

(require require-spec ...)

在一个单一的require表里指定多个require-spec与使用多个每个包含一个单一require-spec的require本质上是一样的。其区别很小,且局限于顶层:一个单一的require可以最多一次导入一个给定标识,而一个单独的require可以代替一个以前的require(都是仅在顶层,在一个模块之外)的绑定。

一个require-spec的允许形态被递归地定义为:

module-path

在其最简单的表中,一个require-spec是一个module-path(如前一节《模块路径》中定义的)。在这种情况下,被require引入的这个绑定通过provide声明来确定,申明中的每个模块被各个module-path引用。

 

Examples:

> (module m racket
    (provide color)
    (define color "blue"))
> (module n racket
    (provide size)
    (define size 17))
> (require 'm 'n)
> (list color size)

'("blue" 17)

 

(only-in require-spec id-maybe-renamed ...)
 
id-maybe-renamed   =   id
    |   [orig-id bind-id]

一个only-in表限制被一个低级的require-spec引入的绑定的设置。此外,only-in选择性地重命名每个可保存的绑定:在一个[orig-id bind-id]表里,orig-id引用一个被require-spec隐含的绑定,并且bind-id是这个在输入上下文中将被绑定的名称,以代替orig-id。

 

Examples:

> (module m (lib "racket")
    (provide tastes-great?
             less-filling?)
    (define tastes-great? #t)
    (define less-filling? #t))
> (require (only-in 'm tastes-great?))
> tastes-great?

#t

> less-filling?

less-filling?: undefined;

 cannot reference an identifier before its definition

  in module: top-level

> (require (only-in 'm [less-filling? lite?]))
> lite?

#t

 

(except-in require-spec id ...)

这个表是 only-in的补充:它从被require-spec指定的集合中排除指定的绑定。

(rename-in require-spec [orig-id bind-id] ...)

这个表支持像only-in的重命名,但自不作为一个orig-id提交的require-spec中分离单独的标识。

(prefix-in prefix-id require-spec)

这是一个重命名的简写,这里prefix-id被添加到被require-spec指定的每个标识的前面。

除了only-in、except-in、rename-in2和prefix-in表可以被嵌套以实现输入绑定的更复杂的操作。例如,

(require (prefix-in m: (except-in 'm ghost)))

输入m输出的所有绑定,除了ghost绑定之外,并带用m:前缀的局部名称:

等价地,这个prefix-in可以被应用在except-in之前,只是带except-in的省略是被使用m:前缀所指定:

(require (except-in (prefix-in m: 'm) m:ghost))

 

6.5 输出:provide

默认情况下,一个模块的所有定义对这个模块是私有的。provide表指定定义以造成在模块被require的地方可获取。

(provide provide-spec ...)

一个provide表只能出现在模块级(即在一个module的当前主体中)。在一个单一的provide中指定多个provide-spec和使用每个带有一个单一的provide-spec的多个provide明显是一样的。

每个标识遍及这个模块中的所有provide最多可以从一个模块中输出一次。更确切地说,用于每个输出的外部名称必须是不同的;相同的内部绑定可以用不同的外部名称输出多次。

一个provide-spec的允许形态被递归定义为:

identifier

在最简单表中,一个provide-spec标明一个在被输出的模块内的绑定。这个绑定既可以来自于一个局部定义,也可以来自于一个输入。

(rename-out [orig-id export-id] ...)

一个rename-out表类似于只指定一个标识,但这个输出绑定orig-id是给定一个不同的名称,export-id,到输入模块。

(struct-out struct-id)

一个struct-out表输出被(struct struct-id ....)创建的绑定。

参见《程序员定义的数据类型》以获取define-struct的更多信息。

(all-defined-out)

all-defined-out简写输出所有在输出模块中(与输入相反)被定义的绑定。

all-defined-out简写的使用通常被阻止,因为它导致对一个模块的实际导出不太明确,并且因为Racket程序员习惯于认为定义可以自由地添加到一个模块而不影响其公共接口(在all-defined-out被使用时候不是这样)。

(all-from-out module-path)

all-from-out简写输出在使用一个基于module-path的require-spec输入的模块中的所有绑定。

尽管不同的module-path可以适用于同一个基于文件的模块,但带all-from-out的重复输出是明确基于module-path引用,而不是被实际引用的模块。

(except-out provide-spec id ...)

就像provide-spec,但省略每个id的输出,其中id是绑定到省略的外部名称。

(prefix-out prefix-id provide-spec)

就像provide-spec,但为每个输出的绑定添加prefix-id到外部名称的开头。

 

6.6 赋值和重定义

在一个模块中的变量定义上的set!使用仅限于定义模块的主体。也就是说,一个模块被允许去改变它自己定义的值,这样的变化对于输入模块是可见的。但是,一个输入上下文不允许去更改一个被输入的绑定的值。

 

Examples:

> (module m racket
    (provide counter increment!)
    (define counter 0)
    (define (increment!)
      (set! counter (add1 counter))))
> (require 'm)
> counter

0

> (increment!)
> counter

1

> (set! counter -1)

set!: cannot mutate module-required identifier

  at: counter

  in: (set! counter -1)

 

在上述例子中,一个模块通过提供一个突变函数总是能够授予其它能力以改变其输出,如increment!。

在输入变量工作上的禁令有助于支持程序的模块化推理。例如,在这个模块中,

(module m racket
  (provide rx:fish fishy-string?)
  (define rx:fish #rx"fish")
  (define (fishy-string? s)
    (regexp-match? rx:fish s)))

函数fishy-string?将始终匹配包含“fish”的字符串,不管其它模块如何使用rx:fish绑定。出于同样的帮助程序员的原因,在对输入工作上的禁令也允许许多程序去被更有效地执行。

在同一行中,当一个模块不包含一个在这个模块中定义的特定标识的set!,那么该标识被认为一个不会被改变的常量(constant)——即使通过重新声明该模块。

因此,一个模块的重定义通常不被允许。对于基于文件的模块,简单地更改该文件不会导致任何情况下的一个重新声明,因为基于文件的模块是按需加载的,而先前加载的声明满足将来的请求。它有可能使用Racket的反射支持以重新声明一个模块,然而,非文件模块可以在REPL中被重新声明;在这种情况下,如果重新申明涉及一个以前的静态绑定的重定义,也许会失败。

> (module m racket
    (define pie 3.141597))
> (require 'm)
> (module m racket
    (define pie 3))

define-values: assignment disallowed;

 cannot re-define a constant

  constant: pie

  in module:'m

作为探索和调试目的,Racket反射层提供一个compile-enforce-module-constants参数来使常量的执行无效。

> (compile-enforce-module-constants #f)
> (module m2 racket
    (provide pie)
    (define pie 3.141597))
> (require 'm2)
> (module m2 racket
    (provide pie)
    (define pie 3))
> (compile-enforce-module-constants #t)
> pie

3

 

合约

本章对Racket的合约系统提供了一个详细的介绍。

在《Racket参考》中的“合约(Contracts)”部分提供有对合约更详细的信息。

    7.1 合约和边界

      7.1.1 合约的违反

      7.1.2 合约与模块的测试

      7.1.3 嵌套合约边界测试

    7.2 函数的简单合约

      7.2.1 ->类型

      7.2.2 使用define/contract和 ->

      7.2.3 any和any/c

      7.2.4 运转你自己的合约

      7.2.5 高阶函数的合约

      7.2.6 带”???“的合约信息

      7.2.7 解析一个合约错误信息

    7.3 一般功能合约

      7.3.1 可选参数

      7.3.2 剩余参数

      7.3.3 关键字参数

      7.3.4 可选关键字参数

      7.3.5 case-lambda的合约

      7.3.6 参数和结果依赖

      7.3.7 检查状态变化

      7.3.8 多个结果值

      7.3.9 固定但静态未知数量

    7.4 合约:一个完整的例子

    7.5 结构上的合约

      7.5.1 确保一个特定值

      7.5.2 确保所有值

      7.5.3 检查数据结构的特性

    7.6 用#:exists和#:∃抽象合约

    7.7 附加实例

      7.7.1 一个客户管理器组建

      7.7.2 一个参数化(简单)栈

      7.7.3 一个字典

      7.7.4 一个队列

    7.8 建立新合约

      7.8.1 合约结构属性

      7.8.2 使所有警告和报警一致

    7.9 问题

      7.9.1 合约和eq?

      7.9.2 合约边界和define/contract

      7.9.3 存在的合约和判断

      7.9.4 定义递归合约

      7.9.5 混合set!和contract-out

 

7.1 合约和边界

如同两个商业伙伴之间的一个合约,一个软件合约是双方之间的一个协议。这个协议规定了从一方传给另一方的每一”产品“(或值)的义务和保证。

因此,一个合约确定了双方之间的一个边界。每当一个值跨越这个边界,这个合约监督系统执行合约检查,确保合作伙伴遵守既定合约。

在这种精神下,Racket主要在模块边界支持合约。具体来说,程序员可以附加合约到provide从句从而对输出值的使用施加约束和承诺。例如,输出描述

#lang racket
 
(provide (contract-out [amount positive?]))
 
(define amount ...)

对上述amount值的模块的所有客户端的承诺将始终是一个正数。合约系统仔细地监测了该模块的义务。每次一个客户端引用amount时,监视器检查amount值是否确实是一个正数。

合约库是建立在Racket语言中内部的,但是如果你希望使用racket/base,你可以像这样明确地输入合约库:

#lang racket/base
(require racket/contract) ; 现在我们可以写合约了。
 
(provide (contract-out [amount positive?]))
 
(define amount ...)

7.1.1 合约的违反

如果我们把amount绑定到一个非正的数字上,

#lang racket
 
(provide (contract-out [amount positive?]))
 
(define amount 0)

那么,当模块被需要时,监控系统发出一个合同违反的信号并将违背承诺归咎于这个模块。

一个更大的错误将是绑定amount到一个非数字值上:

#lang racket
 
(provide (contract-out [amount positive?]))
 
(define amount 'amount)

在这种情况下,监控系统将应用positive?到一个符号,但是positive?报告一个错误,因为它的定义域仅是数字。为了使合约能取得我们对所有Racket值的意图,我们可以确保这个数值既是一个数值同时也是正的,用and/c结合两个合约:

(provide (contract-out [amount (and/c number? positive?)]))

7.1.2 合约与模块的测试

在这一章中的所有合约和模块(不包括那些只是跟随的是使用描述模块的标准#lang语法编写。由于模块充当一个合约中各方之间的边界,因此示例涉及多个模块。

为了在一个单一的模块内或者在DrRacket的定义范围(definitions area)内用多个模块进行测试,使用Racket的子模块。例如,尝试如下所示本节中早先的示例:

#lang racket
 
(module+ server
  (provide (contract-out [amount (and/c number? positive?)]))
  (define amount 150))
 
(module+ main
  (require (submod ".." server))
  (+ amount 10))

每个模块及其合约都用前面的module+关键字包裹在圆括号中。module后面的第一个表是该模块的名称,它被用在一个随后的require语句中(其中通过一个require每个引用用".."对名称进行前缀)。

7.1.3 嵌套合约边界测试

在许多情况下,在模块边界上附加合约是有意义的。然而,能够以一个比模块更细致的方式使用合约通常是方便的。这个define/contract表提供这种使用的权利:

#lang racket
 
(define/contract amount
  (and/c number? positive?)
  150)
 
(+ amount 10)

在这个例子中,define/contract表确定在amount的定义与其周边上下文之间的一个合约边界。换言之,这里的双方是这个定义及包含它的这个模块。

创造这些嵌套合约边界(nested contract boundaries)的表有时对使用来说是微妙的,因为它们也许有意想不到的性能影响或归咎于似乎不直观的一方。这些微妙之处在《使用define/contract和 ->》和《合约边界和define/contract》中被讲解。

 

7.2 函数的简单合约

一个数学函数有一个定义域(domain)和一个值域(range)。定义域表示这个函数可以作为参数接受的值的类型,值域表示它生成的值的类型。用其定义域和值域描述一个函数的常规符号是

f : A -> B

这里A是这个函数的定义域,B是值域。

一个编程语言中的函数也有定义域和值域,而一个合约可以确保一个函数在其定义域中只接收值并且在其值域中只产生值。一个->为一个函数创建这样的一个合约。一个->之后的表为定义域指定定义域并且最后为值域指定一个合约。

这里有一个可以代表一个银行帐户的模块:

#lang racket
 
(provide (contract-out
          [deposit (-> number? any)]
          [balance (-> number?)]))
 
(define amount 0)
(define (deposit a) (set! amount (+ amount a)))
(define (balance) amount)

这个模块输出两个函数:

  • deposit,它接受一个数字并返回某个未在合约中指定的值,

  • balance,它返回指示账户当前余额的一个数值。

当一个模块输出一个函数时,它在自己作为“服务器(server)”与“客户端(client)”的输入这个函数的模块之间建立两个通信通道。如果客户端模块调用该函数,它发送一个值进入服务器模块。相反,如果这样一个函数调用结束并且这个函数返回一个值,这个服务器模块发送一个值回到客户端模块。这种客户端-服务器区别是很重要的,因为当出现问题时,一方或另一方将被归咎。

如果一个客户端模块准备应用deposit到'millions,这将违反其合约。合约监视系统会获得这个违规并因为与上述模块违背合约而归咎于这个客户端。相比之下,如果balance函数准备返回'broke,合同监视系统将归咎于服务器模块。

一个->本身不是一个合约;它是一种合约组合(contract combinator),它结合其它合约以构成一个合约。

7.2.1 ->类型

如果你已经习惯了数学函数,你可以选择一个合约箭头出现在函数的定义域和值域之间而不是在开头。如果你已经阅读过《How to Design Programs》,那你已经见过这个很多次了。事实上,你也许已经在其他人的代码中看到比如这些合约:

(provide (contract-out
          [deposit (number? . -> . any)]))

如果一个Racket的S表达式包含在中间带一个符号的两个点,读取器重新安排这个S表达式并放置符号到前面,就如《列表和Racket语法》里描述的那样。因此,

(number? . -> . any)

只是编写的另一种方式

(-> number? any)

7.2.2 使用define/contract和 ->

在《嵌套合约边界测试》中引入的define/contract表也可以用来定义合约中的函数。例如,

(define/contract (deposit amount)
  (-> number? any)
  ; 实现在这里进行
  ....)

它用合约更早定义deposit函数。请注意,这对deposit的使用有两个潜在的重要影响:

  1. 由于合约总是在调用deposit时进行检查,即使在定义它的模块内,这也可能增加合约被检查的次数。这可能导致一个性能下降。如果函数在循环中反复调用或使用递归时尤其如此。

  2. 在某些情况下,当在同一模块中被其它代码调用时,一个函数可以编写来接受一组更宽松的输入。对于此类用例,通过define/contract建立的合约边界过于严格。

7.2.3 anyany/c

用于deposit的any合约匹配任何结果,并且它只能用于一个函数合约的值域位置。代替上面的any,我们可以使用更具体的合约void?,它表示函数总会返回(void)值。然而,void?合约会要求合约监视系统每次在函数被调用时去检查这个返回值,即使“客户端”模块不能很好用这个值工作。相反,any告诉监视系统检查这个返回值,它告诉一个潜在客户端这个“服务器”模块对这个函数的返回值不作任何承诺,甚至不管它是一个单独的值或多个值。

any/c合约类似于any,在那里它对一个值不做要求。不像anyany/c表示一个单个值,并且它适合用作一个参数合约。使用any/c作为一个值域合约强迫一个对这个函数产生一个单个值的检查。就像这样,

(-> integer? any)

描述一个接受一个整数并返回任意数值的函数,然而

(-> integer? any/c)

描述接受一个整数并生成一个单个结果(但对结果没有更多说明)的一个函数。以下函数

(define (f x) (values (+ x 1) (- x 1)))

匹配(-> integer? any),但不匹配(-> integer? any/c)。

当对承诺来自一个函数的一个单个结果特别重要时,使用any/c作为一个结果合约。当你希望对一个函数的结果尽可能少地承诺(并尽可能少地检查)时,使用any/c

7.2.4 运转你自己的合约

deposit函数将给定的数值添加到amount中。当该函数的合约阻止客户端将它应用到非数值时,这个合约仍然允许它们把这个函数应用到复数、负数或不精确的数字中,但没有一个能合理地表示钱的金额。

合约系统允许程序员定义他们自己的合约作为函数:

#lang racket
 
(define (amount? a)
  (and (number? a) (integer? a) (exact? a) (>= a 0)))
 
(provide (contract-out
          ; 一个金额是一个美分的自然数
          ; 是给定的数字的一个amount?
          [deposit (-> amount? any)]
          [amount? (-> any/c boolean?)]
          [balance (-> amount?)]))
 
(define amount 0)
(define (deposit a) (set! amount (+ amount a)))
(define (balance) amount)

这个模块定义了一个amount?函数并在->合约内使用它作为一个合约。当一个客户端用(-> amount? any)调用deposit函数作为输出时,它必须提供一个精确的、非负的整数,否则amount?函数应用到参数将返回#f,这将导致合约监视系统归咎于客户端。类似地,服务器模块必须提供一个精确的、非负的整数作为balance的结果以保持无可归咎。

当然,将一个通信通道限制为客户端不明白的值是没有意义的。因此,这个模块也输出amount?判断本身,用一个合约表示它接受一个任意值并返回一个布尔值。

在这种情况下,我们也可以使用natural-number/c代替amount?,因为它恰恰意味着相同的检查:

(provide (contract-out
          [deposit (-> natural-number/c any)]
          [balance (-> natural-number/c)]))

接受一个参数的每一个函数可以当作一个判断从而被用作一个合约。然而,为了结合现有的对一个新参数的检查,合约连接符像and/cor/c往往是有用的。例如,这里还有另一种途径去编写上述合约:

(define amount/c
  (and/c number? integer? exact? (or/c positive? zero?)))
 
(provide (contract-out
          [deposit (-> amount/c any)]
          [balance (-> amount/c)]))

其它值也作为合约提供双重任务。例如,如果一个函数接受一个数值或#f,(or/c number? #f)就够了。同样,amount/c合约也许已经用一个0代替zero?来编写。如果你使用一个正则表达式作为一个合约,该合约接受与正则表达式匹配的字符串和字节字符串。

当然,你可以用连接符像and/c混合你自己的合约执行函数。这里有一个用于创建来自于银行记录的字符串的模块:

#lang racket
 
(define (has-decimal? str)
  (define L (string-length str))
  (and (>= L 3)
       (char=? #\. (string-ref str (- L 3)))))
 
(provide (contract-out
          ; 转换一个随机数为一个字符串
          [format-number (-> number? string?)]
 
          ; 用一个十进制点转换一个金额为一个字符串,
          ; 就像在美国货币的一个金额那样。
          [format-nat (-> natural-number/c
                          (and/c string? has-decimal?))]))

输出函数format-number的合约指定该函数接受一个数值并生成一个字符串。这个输出函数format-nat的合约比format-number的其中之一更有趣。它只接受自然数。它的值域合约承诺在右边的第三个位置带有一个.的字符串。

如果我们希望加强format-nat的值域合约的承诺,以便它只接受带数字和一个点的字符串,我们可以这样编写:

#lang racket
 
(define (digit-char? x)
  (member x '(#\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\0)))
 
(define (has-decimal? str)
  (define L (string-length str))
  (and (>= L 3)
       (char=? #\. (string-ref str (- L 3)))))
 
(define (is-decimal-string? str)
  (define L (string-length str))
  (and (has-decimal? str)
       (andmap digit-char?
               (string->list (substring str 0 (- L 3))))
       (andmap digit-char?
               (string->list (substring str (- L 2) L)))))
 
....
 
(provide (contract-out
          ....
          ; 转换美分的一个金额(自然数)
          ; 成为一个基于美元的字符串
          [format-nat (-> natural-number/c
                          (and/c string?
                                 is-decimal-string?))]))

另外,在这种情况下,我们可以使用一个正则表达式作为一个合约:

#lang racket
 
(provide
 (contract-out
  ....
  ; 转换美分的一个数量(自然数)
  ; 成为一个基于美元的字符串
  [format-nat (-> natural-number/c
                  (and/c string? #rx"[0-9]*\\.[0-9][0-9]"))]))

7.2.5 高阶函数的合约

函数合约不仅仅局限于在其定义域或值域上的简单判断。还包括它们自己的函数合约,能够被用作参数及一个函数结果。

例如:

(-> integer? (-> integer? integer?))

是描述一个柯里函数的一个合约。它匹配接受一个参数的函数并接着在返回另一个接受一个前面的第二个参数,最后返回一个整数。如果一个服务器用这个合约输出一个函数make-adder,并且如果make-adder返回一个函数外还返回一个值,那么这个服务器应被归咎。如果make-adder确实返回一个函数,但这个返回函数被应用于一个整数外还有一个值,则客户端应被归咎。

同样,合约

(-> (-> integer? integer?integer?)

描述接受其它函数作为其输入的函数。如果一个服务器用这个合约输出一个函数twice并且twice被应用给一个带一个参数的函数外还给一个值,那么客户端应被归咎。如果twice被应用给一个带一个参数的函数并且twice对一个整数调用这个给定的函数外还对一个值,那么服务器应被归咎。

7.2.6 带”???“的合约信息

你编写了你的模块。你添加了合约。你将它们放入接口以便客户端程序员拥有来自接口的所有信息。这是一门艺术:

> (module bank-server racket
    (provide
     (contract-out
      [deposit (-> (λ (x)
                     (and (number? x) (integer? x) (>= x 0)))
                   any)]))
  
    (define total 0)
    (define (deposit a) (set! total (+ a total))))

几个客户端使用了你的模块。其他人转而使用了他们的模块。突然他们中的一个看到了这个错误消息:

> (require 'bank-server)
> (deposit -10)

deposit: contract violation

  expected: ???

  given: -10

  in: the 1st argument of

      (-> ??? any)

  contract from: bank-server

  blaming: top-level

   (assuming the contract is correct)

  at: eval:2:0

???在那里代表什么?如果我们有这样一个数据类型的名字,就像我们有字符串、数字等等,那不是很好吗?

针对这种情况,Racket提供了扁平命名合约(flat named contract)。在这一术语中使用“合约”表明合约是第一类值。这个“扁平(flat)”意味着数据的集合是内建的数据原子类的一个子集;它们由一个接受所有Racket值并产生一个布尔值的判断来描述。这个“命名(named)”部分表示我们想要做的事情,它将去命名这个合约以便错误消息变得明白易懂:

> (module improved-bank-server racket
    (provide
     (contract-out
      [deposit (-> (flat-named-contract
                    'amount
                    (λ (x)
                      (and (number? x) (integer? x) (>= x 0))))
                   any)]))
  
    (define total 0)
    (define (deposit a) (set! total (+ a total))))

用这个小小的更改,这个错误消息就变得相当易读:

> (require 'improved-bank-server)
> (deposit -10)

deposit: contract violation

  expected: amount

  given: -10

  in: the 1st argument of

      (-> amount any)

  contract from: improved-bank-server

  blaming: top-level

   (assuming the contract is correct)

  at: eval:5:0

7.2.7 解析一个合约错误信息

一般来说,每个合约错误信息由六部分组成:

  • 一个用合约关联的函数或方法的名称。而且这个短语“合约违反”或“违反合约”取决于是否这个合约被客户端或服务器违反;例如在前面的示例中:

    deposit: contract violation

     

  • 一个被违反的合约的准确方面的描述,

    expected: amount

    given: -10

     

  • 这个完整的合约加上一个路径显示哪个方面被违反,

    in: the 1st argument of

    (-> amount any)

     

  • 合约被放置的这个模块(或者更广泛地说,合同所规定的边界),

    contract from: improved-bank-server

     

  • 哪个应被归咎,

    blaming: top-level

    (assuming the contract is correct)

     

  • 以及这个合约出现的源程序位置。

    at: eval:5:0

 

7.3 一般功能合约

->合约构造器为带有一个固定数量参数的函数工作,并且这里这个结果合约不依赖于这个输入参数。为了支持其它类型的函数,Racket提供额外的合约构造器,尤其是 ->*->i

7.3.1 可选参数

请看一个字符串处理模块的摘录,该灵感来自于《Scheme cookbook》:

#lang racket
 
(provide
 (contract-out
  ; 用(可选的)char填充给定的左右两个str以使其左右居中
  ; 
  [string-pad-center (->* (string? natural-number/c)
                          (char?)
                          string?)]))
 
(define (string-pad-center str width [pad #\space])
  (define field-width (min width (string-length str)))
  (define rmargin (ceiling (/ (- width field-width) 2)))
  (define lmargin (floor (/ (- width field-width) 2)))
  (string-append (build-string lmargin (λ (x) pad))
                 str
                 (build-string rmargin (λ (x) pad))))

这个模块输出string-pad-center,一个函数,它在中心用给定字符串创建一个给定的width的一个字符串。这个默认的填充字符是#\space;如果这个客户端模块希望使用一个不同的字符,它可以用第三个参数——一个重写默认值的char——调用string-pad-center。

这个函数定义使用可选参数,它对于这种功能是合适的。这里有趣的一点是string-pad-center的合约的表达方式。

合约组合器->*,要求几组合约:

  • 第一个是对所有必需参数的合约的一个括号组。在这个例子中,我们看到两个:string?natural-number/c

  • 第二个是对所有可选参数的合约的一个括号组:char?

  • 最后一个是一个单一的合约:函数的结果。

请注意,如果默认值不满足合约,则不会获得此接口的合约错误。如果不能信任你自己去正确获得初始值,则需要在边界上传递初始值。

7.3.2 剩余参数

max操作符至少接受一个实数,但它接受任意数量的附加参数。你可以使用一个剩余参数(rest argument)编写其它此类函数,例如在max-abs中:

参见《申明一个剩余(rest)参数》以获取剩余参数的介绍。

(define (max-abs n . rst)
  (foldr (lambda (n m) (max (abs n) m)) (abs n) rst))

通过一个合约描述这个函数需要一个对->*进一步的扩展:一个#:rest关键字在必需参数和可选参数之后指定在一个参数列表上的一个合约:

(provide
 (contract-out
  [max-abs (->* (real?) () #:rest (listof real?real?)]))

正如对->*的通常情况,必需参数合约被封闭在第一对括号中,在这种情况下是一个单一的实数。空括号表示没有可选参数(不包含剩余参数)。剩余参数合约跟着#:rest;因为所有的额外的参数必须是实数,剩余参数的列表必须满足合约(listof real?)。

7.3.3 关键字参数

其实->合约构造器也包含对关键字参数的支持。例如,考虑这个函数,它创建一个简单的GUI并向用户询问一个yes-or-no的问题:

参见《声明关键字(keyword)参数》以获取关键字参数的介绍。

#lang racket/gui
 
(define (ask-yes-or-no-question question
                                #:default answer
                                #:title title
                                #:width w
                                #:height h)
  (define d (new dialog% [label title] [width w] [height h]))
  (define msg (new message% [label question] [parent d]))
  (define (yes) (set! answer #t) (send d show #f))
  (define (no) (set! answer #f) (send d show #f))
  (define yes-b (new button%
                     [label "Yes"] [parent d]
                     [callback (λ (x y) (yes))]
                     [style (if answer '(border) '())]))
  (define no-b (new button%
                    [label "No"] [parent d]
                    [callback (λ (x y) (no))]
                    [style (if answer '() '(border))]))
  (send d show #t)
  answer)
 
(provide (contract-out
          [ask-yes-or-no-question
           (-> string?
               #:default boolean?
               #:title string?
               #:width exact-integer?
               #:height exact-integer?
               boolean?)]))

如果你真的想通过一个GUI问一个yes或no的问题,你应该使用message-box/custom。对此事而论,通常会比用较“yes”或“no”更确切的回答来提供按钮更好。

ask-yes-or-no-question的合约使用->,同样的方式lambda(或基于define的函数)允许一个关键字先于一个函数正式的参数,->允许一个关键字先于一个函数合约的参数合约。在这种情况下,这个合约表明ask-yes-or-no-question必须接收四个关键字参数,每一个关键字为:#:default、#:title、#:width和#:height。如同在一个函数定义中,在->中关键字的顺序相对于其它的每个来说对函数的客户端无关紧要;只有参数合约的相对顺序没有关键字问题。

7.3.4 可选关键字参数

当然,ask-yes-or-no-question(从上一个问题中引来)中有许多参数有合理的默认值并且应该被设为可选的:

(define (ask-yes-or-no-question question
                                #:default answer
                                #:title [title "Yes or No?"]
                                #:width [w 400]
                                #:height [h 200])
  ...)

要指定这个函数的合约,我们需要再次使用->*。它支持关键字,正如你在可选参数和强制参数部分中所期望的一样。在这种情况下,我们有强制关键字#:default和可选关键字#:title、#:width和#:height。所以,我们像这样编写合约:

(provide (contract-out
          [ask-yes-or-no-question
           (->* (string?
                 #:default boolean?)
                (#:title string?
                 #:width exact-integer?
                 #:height exact-integer?)
 
                boolean?)]))

也就是说,我们把强制关键字方在第一部分中,同时我们把可选关键字放在在第二部分中。

7.3.5 case-lambda的合约

case-lambda定义的一个函数可以对其参数施加不同的约束取决于多少参数被提供。例如,report-cost函数可以既可以转换一对数值也可以转换一个字符串为一个新字符串:

参见《实参数量感知函数:case-lambda》以获得case-lambda的介绍。

(define report-cost
  (case-lambda
    [(lo hi) (format "between $~a and $~a" lo hi)]
    [(desc) (format "~a of dollars" desc)]))
 
> (report-cost 5 8)

"between $5 and $8"

> (report-cost "millions")

"millions of dollars"

对这样的一个函数的合约用case->组合器构成,它根据需要组合多个功能合约:

(provide (contract-out
          [report-cost
           (case->
            (integer? integer? . -> . string?)
            (string? . -> . string?))]))

如你所见,report-cost的合约组合了两个函数合约,它与其功能所需的解释一样多的从句。

7.3.6 参数和结果依赖

以下是来自一个虚构的数值模块的一个摘录:

(provide
 (contract-out
  [real-sqrt (->i ([argument (>=/c 1)])
                  [result (argument) (<=/c argument)])]))

这个词“indy”意味着暗示归咎会被分配到合约本身,因为这个合约必须被认为是一个独立的组件。响应两个现有标签选择名称——“lax”和“picky”——为在研究文献中的函数合约的不同语义。

这个输出函数real-sqrt的合约使用->i比使用->*更好。这个“i”代表是一个印地依赖(indy dependent)合约,意味函数值域的合约依赖于该参数的值。在result的合约这一行里argument的出现意味着那个结果依赖于这个参数。在特别情况下,real-sqrt的参数大于或等于1,所以一个很基本的正确性检查是结果小于参数。

一般来说,一个依赖函数合约看起来更像一般的->*合约,但是在合约的其它地方可以使用名字。

回到银行帐户示例,假设我们一般化这个模块以支持多个帐户并且我们也包括一个取款操作。 改进后的银行帐户模块包括一个account结构类型和以下函数:

(provide (contract-out
          [balance (-> account? amount/c)]
          [withdraw (-> account? amount/c account?)]
          [deposit (-> account? amount/c account?)]))

但是,除了要求一个客户端为一个取款提供一个有效金额外,金额应小于或等于指定账户的余额,并且结果账户会比它开始时的钱少。同样,该模块可能承诺一个存款通过为账户增加钱来产生一个帐户。以下实现通过合约强制执行这些约束和保证:

#lang racket
 
; 第1部分:合约定义
(struct account (balance))
(define amount/c natural-number/c)
 
; 第2部分:输出
(provide
 (contract-out
  [create   (amount/c . -> . account?)]
  [balance  (account? . -> . amount/c)]
  [withdraw (->i ([acc account?]
                  [amt (acc) (and/c amount/c (<=/c (balance acc)))])
                 [result (acc amt)
                         (and/c account?
                                (lambda (res)
                                  (>= (balance res)
                                      (- (balance acc) amt))))])]
  [deposit  (->i ([acc account?]
                  [amt amount/c])
                 [result (acc amt)
                         (and/c account?
                                (lambda (res)
                                  (>= (balance res)
                                      (+ (balance acc) amt))))])]))
 
; 第3部分:函数定义
(define balance account-balance)
 
(define (create amt) (account amt))
 
(define (withdraw a amt)
  (account (- (account-balance a) amt)))
 
(define (deposit a amt)
  (account (+ (account-balance a) amt)))

在第2部分中这个合约为create和balance提供了典型的类型保证。然而,对于withdraw和deposit,该合约检查并保证对balance和deposit的更为复杂的约束。在对withdraw的合约上的第二个参数使用(balance acc)来检查所提供的取款金额是否足够小,其中acc是在->i之中给定的函数第一个参数的名称。在withdraw结果上的合约使用acc和amt来保证不超过所要求的金额被提取。在deposit上的合约同样在结果合约中使用acc和amount来保证至少和提供的一样多的钱被存入账户。

正如上面所编写的,当一个合约检查失败时,该错误消息不是很显著。下面的修订在一个助手函数mk-account-contract中使用flat-named-contract以提供更好的错误消息。

#lang racket
 
; 第1部分:合约定义
(struct account (balance))
(define amount/c natural-number/c)
 
(define msg> "account a with balance larger than ~a expected")
(define msg< "account a with balance less than ~a expected")
 
(define (mk-account-contract acc amt op msg)
  (define balance0 (balance acc))
  (define (ctr a)
    (and (account? a) (op balance0 (balance a))))
  (flat-named-contract (format msg balance0) ctr))
 
; 第2部分:导出
(provide
 (contract-out
  [create   (amount/c . -> . account?)]
  [balance  (account? . -> . amount/c)]
  [withdraw (->i ([acc account?]
                  [amt (acc) (and/c amount/c (<=/c (balance acc)))])
                 [result (acc amt) (mk-account-contract acc amt >= msg>)])]
  [deposit  (->i ([acc account?]
                  [amt amount/c])
                 [result (acc amt)
                         (mk-account-contract acc amt <= msg<)])]))
 
; 第3部分:函数定义
(define balance account-balance)
 
(define (create amt) (account amt))
 
(define (withdraw a amt)
  (account (- (account-balance a) amt)))
 
(define (deposit a amt)
  (account (+ (account-balance a) amt)))

7.3.7 检查状态变化

->i合约组合器也可以确保一个函数仅按照一定的约束修改状态。例如,考虑这个合约(它是来自框架中的函数preferences:add-panel的一个略微简化的版本):

(->i ([parent (is-a?/c area-container-window<%>)])
      [_ (parent)
       (let ([old-children (send parent get-children)])
         (λ (child)
           (andmap eq?
                   (append old-children (list child))
                   (send parent get-children))))])

它表示该函数接受一个被命名为parent的单一参数,并且parent必须是一个匹配这个接口area-container-window<%>的对象。

这个值域合约确保该函数通过添加一个新的child到列表的前面来仅仅修改parent的children。它通过使用_代替一个正常的标识符来完成这个,它告诉这个合约库该值域合约并不依赖于任何结果的值,因此当这个函数被调用时,而不是返回时,该合约库求值这个跟着_的表达式。因此对get-children方法的调用发生在合约被调用下的函数之前。当合约下的函数返回时,它的结果作为child被传递进去,并且合约确保该函数返回后的child与该函数调用之前的child相同,但是有许许多多的child,在列表前面。

要去明白在一个集中在这点上的玩具例子中的不同,考虑这个程序:

#lang racket
(define x '())
(define (get-x) x)
(define (f) (set! x (cons 'f x)))
(provide
 (contract-out
  [f (->i () [_ (begin (set! x (cons 'ctc x)) any/c)])]
  [get-x (-> (listof symbol?))]))

如果你将需要这个模块,调用f,那么get-x的结果会是'(f ctc)。相反,如果f的合约是

(->i () [res (begin (set! x (cons 'ctc x)) any/c)])

(只改变res的下划线),那么get-x的结果会是'(ctc f)。

7.3.8 多个结果值

函数split接受char的一个列表并且传递在#\newline(如果有)的第一次出现之前的字符串以及这个列表的剩余部分:

(define (split l)
  (define (split l w)
    (cond
      [(null? l) (values (list->string (reverse w)) '())]
      [(char=? #\newline (car l))
       (values (list->string (reverse w)) (cdr l))]
      [else (split (cdr l) (cons (car l) w))]))
  (split l '()))

它是一个典型的多值函数,通过遍历一个单个列表返回两个值。

这样一个函数的合约可以使用普通函数箭头->,此后当它作为最后结果出现时,->特别地处理values

(provide (contract-out
          [split (-> (listof char?)
                     (values string? (listof char?)))]))

这样一个函数的合约也可以使用->*编写:

(provide (contract-out
          [split (->* ((listof char?))
                      ()
                      (values string? (listof char?)))]))

和前面一样,带->*的参数的合约被包裹在一对额外的圆括号中对(并且必须总是这样被包裹)中,并且这个空括号对表示这里没有可选参数。这个结果的合约是在values内部:一个字符串和字符的一个列表。

现在,假设我们还希望确保split的第一个结果是在列表格式中的这个给定单词的一个前缀。在这种情况下,我们需要使用这个->i合约组合器:

(define (substring-of? s)
  (flat-named-contract
    (format "substring of ~s" s)
    (lambda (s2)
      (and (string? s2)
           (<= (string-length s2) (string-length s))
           (equal? (substring s 0 (string-length s2)) s2)))))
 
(provide
 (contract-out
  [split (->i ([fl (listof char?)])
              (values [s (fl) (substring-of? (list->string fl))]
                      [c (listof char?)]))]))

->*->i组合使用函数中的参数来创建范围的合约。是的,它不只是返回一个合约,而是函数产生值的数量:每个值的一个合约。在这种情况下,第二个合约和以前一样,确保第二个结果是char列表。与此相反,第一个合约增强旧的,因此结果是给定单词的前缀。

当然,这个合约对于检查来说是值得的。这里有一个稍微廉价的版本:

(provide
 (contract-out
  [split (->i ([fl (listof char?)])
              (values [s (fl) (string-len/c (length fl))]
                      [c (listof char?)]))]))

7.3.9 固定但静态未知数量

想象一下你自己为一个函数编写了一个合约,这个函数接受其它一些函数并且一个最终前者应用于后者的数值的列表。除非这个给定的函数的数量匹配给定列表的长度,否则你的过程就会陷入困难。

考虑这个n-step函数:

; (number ... -> (union #f number?)) (listof number) -> void
(define (n-step proc inits)
  (let ([inc (apply proc inits)])
    (when inc
      (n-step proc (map (λ (x) (+ x inc)) inits)))))

n-step的参数是proc,一个函数proc的结果要么是数值要么是假(false),以及一个列表。它接着应用proc到这个列表inits中。只要proc返回一个数值,n-step把那个数值处理为一个在inits和递归里的每个数值的增量值。当proc返回false时,这个循环停止。

这里有两个应用:

; nat -> nat
(define (f x)
  (printf "~s\n" x)
  (if (= x 0) #f -1))
(n-step f '(2))
 
; nat nat -> nat
(define (g x y)
  (define z (+ x y))
  (printf "~s\n" (list x y z))
  (if (= z 0) #f -1))
 
(n-step g '(1 1))

一个n-step的合约必须指定proc的行为的两方面:其数量必须在inits里包括元素的数量,同时它必须返回一个数值或#f。后者是容易的,前者是困难的。乍一看,这似乎暗示一个合约分配了一个可变数量(variable-arity)给了proc:

(->* ()
     #:rest (listof any/c)
     (or/c number? false/c))

然而,这个合约表明这个函数必须接受任意(any)数量的参数,而不是一个特定(specific)的但不确定(undetermined)的数值。因此,应用n-step到(lambda (x) x)和(list 1)违反合约,因为这个给定的函数只接受一个参数。

正确的合约使用unconstrained-domain->组合器,它仅指定一个函数的值域,而不是它的定义域。它接下来可能连接这个合约到一个数量测试以指定n-step的正确合约:

(provide
 (contract-out
  [n-step
   (->i ([proc (inits)
          (and/c (unconstrained-domain->
                  (or/c false/c number?))
                 (λ (f) (procedure-arity-includes?
                         f
                         (length inits))))]
         [inits (listof number?)])
        ()
        any)]))

 

7.4 合约:一个完整的例子

 

本节开发对于同一个例子的合约的几种不同特点:Racket的argmax函数。根据它的Racket文档,这个函数接受一个过程proc和一个非空的值列表,lst。它

返回在最大化proc的结果的列表lst中的first元素。对first的强调是我们的。

 

 

例子:
> (argmax add1 (list 1 2 3))

3

> (argmax sqrt (list 0.4 0.9 0.16))

0.9

> (argmax second '((a 2) (b 3) (c 4) (d 1) (e 4)))

'(c 4)

 

这里是这个函数的可能最简单的合约:

version 1

#lang racket
 
(define (argmax f lov) ...)
 
(provide
 (contract-out
  [argmax (-> (-> any/c real?) (and/c pair? list?) any/c)]))

这个合约捕捉argmax的非正式描述的两个必备条件:

  • 这个给定的函数必须产生按<进行比较的数值。特别是,这个合约(-> any/c number?)不可行,因为number?也承认Racket中的复数有效。

  • 给定列表必须至少包含一项。

当组合名称时,合约解释在同级的argmax的行为作为在一个模块签名(除空表方面外)中的一个ML(机器语言)函数类型。

然而,合约可能比一个类型签名更值得关注。看一看argmax的第二个合约:

version 2

#lang racket
 
(define (argmax f lov) ...)
 
(provide
 (contract-out
  [argmax
    (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) ()
         (r (f lov)
            (lambda (r)
              (define f@r (f r))
              (for/and ([v lov]) (>= f@r (f v))))))]))

它是一个依赖合约,它命名两个参数并使用这个名称在结果上添加一个判断。这个判断计算 (f r)——这里r是argmax的结果——并接着验证这个值大于或等于在lov的项目上的所有f值。

这是可能的吗?——argmax会通过返回一个随机值作弊,这个随机值意外地最大化f超过lov的所有元素。用一个合约,就有可能排除这种可能性:

version 2 rev. a

#lang racket
 
(define (argmax f lov) ...)
 
(provide
 (contract-out
  [argmax
    (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) ()
         (r (f lov)
            (lambda (r)
              (define f@r (f r))
              (and (memq r lov)
                   (for/and ([v lov]) (>= f@r (f v)))))))]))

memq函数确保r是相等(intensionally equal)也就是说,那些喜欢在硬件层面思考的人的“指针相等(pointer equality)”。于lov的其中一个成员。当然,片刻的反思显露出要构成这样一个值是不可能的。函数是Racket中的不透明值,并且没有应用一个函数,无法确定某个随机输入值是否产生一个输出值或触发某些异常。因此我们从这里开始忽略这种可能性。

版本2确切地阐述了argmax文档的整体观点,但它没能传达出这个结果是这个给定的最大化给定的函数f的列表的第一个元素。这是一个传达这个非正式文档的第二个方面的版本:

version 3

#lang racket
 
(define (argmax f lov) ...)
 
(provide
 (contract-out
  [argmax
    (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) ()
         (r (f lov)
            (lambda (r)
              (define f@r (f r))
              (and (for/and ([v lov]) (>= f@r (f v)))
                   (eq? (first (memf (lambda (v) (= (f v) f@r)) lov))
                        r)))))]))

那就是,memf函数确定lov的第一个元素,f下的lov的值等于f下的r的值。如果此元素是有意等于r,argmax的结果就是正确的。

第二个细化步骤介绍了两个问题。首先,条件都重新计算lov的所有元素的f的值。第二,这个合约现在很难阅读。合约应该有一个简洁的表达方式,它可以让一个客户端可以用一个简单的扫描进行理解。让我们用具有合理意义的名称的两个辅助函来数消除可读性问题:

version 3 rev. a

#lang racket
 
(define (argmax f lov) ...)
 
(provide
 (contract-out
  [argmax
    (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) ()
         (r (f lov)
            (lambda (r)
              (define f@r (f r))
              (and (is-first-max? r f@r f lov)
                   (dominates-all f@r f lov)))))]))
 
; 这里
 
;  f@r大于或等于在lov中v的所有(f v)
(define (dominates-all f@r f lov)
  (for/and ([v lov]) (>= f@r (f v))))
 
;  r是eq?于lov的第一个元素v,因为它的(pred? v)
(define (is-first-max? r f@r f lov)
  (eq? (first (memf (lambda (v) (= (f v) f@r)) lov)) r))

原则上,这两个判断的名称表示它们的功能和表达不需要读取它们的定义。

这一步给我们带来了新引进的低效率问题。为了避免因lov上的所有 v引起的(f v)的重复计算,我们改变合约以致其计算这些值和重用它们是必要的:

version 3 rev. b

#lang racket
 
(define (argmax f lov) ...)
 
(provide
 (contract-out
  [argmax
    (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) ()
         (r (f lov)
            (lambda (r)
              (define f@r (f r))
              (define flov (map f lov))
              (and (is-first-max? r f@r (map list lov flov))
                   (dominates-all f@r flov)))))]))
 
; 这里
 
;  f@r大于或等于flov中所有的f@v
(define (dominates-all f@r flov)
  (for/and ([f@v flov]) (>= f@r f@v)))
 
;  r是lov+flov里第一个x的(first x),整理为(= (second x) f@r)
(define (is-first-max? r f@r lov+flov)
  (define fst (first lov+flov))
  (if (= (second fst) f@r)
      (eq? (first fst) r)
      (is-first-max? r f@r (rest lov+flov))))

现在对结果的判断为lov的元素再次计算了f的所有值一次。

单词“eager(热切,急切)”来自于合约语言学文献。

当版本3去调用f时也许还太急切。然而无论lov包含有多少成员,Racket的argmax总是调用f,让我们想象一下,为了说明目的,我们自己的实现首先检查列表是否是单体。如果是这样,第一个元素将是lov的唯一元素,在这种情况下就不需要计算(f r)。Racket的argmax隐含论证它不仅承诺第一个值,它最大化f超过lov但同样f产生一个结果的值。 事实上,由于f可能发散或增加一些例外输入,argmax应该尽可能避免调用f。

下面的合约演示了如何调整高阶依赖合约,以避免过度依赖:

version 4

#lang racket
 
(define (argmax f lov)
  (if (empty? (rest lov))
      (first lov)
      ...))
 
(provide
 (contract-out
  [argmax
    (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) ()
         (r (f lov)
            (lambda (r)
              (cond
                [(empty? (rest lov)) (eq? (first lov) r)]
                [else
                 (define f@r (f r))
                 (define flov (map f lov))
                 (and (is-first-max? r f@r (map list lov flov))
                      (dominates-all f@r flov))]))))]))
 
; where
 
;  f@r大于或等于flov中所有的f@v
(define (dominates-all f@r lov) ...)
 
;  r是lov+flov里第一个x的(first x),整理为(= (second x) f@r)
(define (is-first-max? r f@r lov+flov) ...)

注意,这种考虑不适用于一阶合同的世界。只有一个高阶(或惰性)语言迫使程序员去以如此精确度去表达合约。

发散或异常提升函数的问题应该让读者对带副作用的函数的一般性问题保持警惕。如果这个给定的函数f有明显的影响——表明它把它的调用记录到了一个文件——那么argmax的客户端将能够观察每次调用argmax的两套日志。确切地讲,如果值列表包含多个元素,这个日志将包含lov上的每一个值的两个f调用。如果f对于计算来说太昂贵,则加倍调用承受一个高成本。

用过度热切的合约来避免这种成本以及来标志问题,一个合约系统可以记录已约定的函数参数的i/o并使用这些散列表的相关规范。这是PLT研究中的一个课题。敬请关注。

 

7.5 结构上的合约

模块以两种方式处理结构。首先它们输出struct的定义,即某种创造结构的资格、存取它们的字段的资格、修改它们的资格以及区别这种结构于世界上所有其它值的资格。其次,有时一个模块输出一个特定的结构并希望它的字段包含某种值。本节讲解如何使用合约保护结构的两种使用。

7.5.1 确保一个特定值

如果你的模块定义了一个变量做为一个结构,那么你可以使用struct/c指定结构的形态:

#lang racket
(require lang/posn)
 
(define origin (make-posn 0 0))
 
(provide (contract-out
          [origin (struct/c posn zero? zero?)]))

在这个例子中,该模块输入一个代表位置的库,它输出一个posn结构。posn中的一个创建并输出所代表的网格原点,即(0,0)。

又见vector/c及类似的对(扁平)复合数据的合约组合器。

7.5.2 确保所有值

How to Design Programs》这本书教授了posn应该只包含在它们两个字段里的数值。用合约我们可以执行以下这种非正式数据定义:

#lang racket
(struct posn (x y))
 
(provide (contract-out
          [struct posn ((x number?) (y number?))]
          [p-okay posn?]
          [p-sick posn?]))
 
(define p-okay (posn 10 20))
(define p-sick (posn 'a 'b))

这个模块输出整个结构定义:posn、posn?、posn-x、posn-y、set-posn-x!和set-posn-y!。每个函数执行或承诺一个posn结构的这两个字段是数值——当这些值穿过模块边界传递时。因此,如果一个客户端在10和'a上调用posn,这个合约系统就发出一个合约违反信号。

然而,posn模块内的p-sick的创建,并没有违反该合约。这个函数posn在内部使用,所以'a和'b不穿过模块边界。同样,当p-sick穿过posn的边界时,该合约承诺一个posn?并且别的什么也没有。特别是,这个检查并没有需要p-sick的字段是数值。

用模块边界的合约检查的联系意味着p-okay和p-sick从一个客户端的角度看起来相似,直到客户端选取了以下片断:

#lang racket
(require lang/posn)
 
... (posn-x p-sick) ...

使用posn-x是这个客户端能够找到一个posn在x字段里包含的内容的唯一途径。posn-x的应用程序发送回p-sick进入posn模块以及发送回这个结果值——这里的'a——给客户端,再跨越模块边界。在这一点上,这个合约系统发现一个承诺被违反了。具体来说,posn-x没有返回一个数值但却返回了一个符号,因此应该被归咎。

这个具体的例子表明,对一个违反合约的解释并不总是指明错误的来源。好消息是,这个错误被定位在posn模块内。坏消息是,这种解释是误导性的。虽然posn-x产生了一个符号而不是一个数值是真的,它是从符号创建了posn的这个程序员的责任,亦即这个程序员添加了

(define p-sick (posn 'a 'b))

到这个模块中。所以,当你在寻找基于违反合约的bug时,把这个例子记在心里。

如果我们想修复p-sick的合约以便在sick被输出时这个错误被捕获,一个单一的改变就足够了:

(provide
 (contract-out
  ...
  [p-sick (struct/c posn number? number?)]))

更确切地说,代替作为一个直白的posn?的输出p-sick,我们使用一个struct/c合约来执行对其组件的约束。

7.5.3 检查数据结构的特性

struct/c编写的合约立即检查数据结构的字段,但是有时这能够对一个程序的性能具有灾难性的影响,这个程序本身并不检查整个数据结构。

作为一个例子,考虑二叉搜索树搜索算法。一个二叉搜索树就像一个二叉树,除了这些数值被组织在树中以便快速搜索这个树。特别是,对于树中的每个内部节点,左边子树中的所有数值都小于节点中的数值,同时右子树中的所有数值都大于节点中的数值。

我们可以实现一个搜索函数in?,它利用二叉搜索树结构的优势。

#lang racket
 
(struct node (val left right))
 
 
 
 ; 利用二叉搜索树不变量,
 ; 确定二叉搜索树“b”中是否存在“n”。
 
(define (in? n b)
  (cond
    [(null? b) #f]
    [else (cond
            [(= n (node-val b))
             #t]
            [(< n (node-val b))
             (in? n (node-left b))]
            [(> n (node-val b))
             (in? n (node-right b))])]))
 
; 一个识别二叉搜索树的判断。
(define (bst-between? b low high)
  (or (null? b)
      (and (<= low (node-val b) high)
           (bst-between? (node-left b) low (node-val b))
           (bst-between? (node-right b) (node-val b) high))))
 
(define (bst? b) (bst-between? b -inf.0 +inf.0))
 
(provide (struct-out node))
(provide (contract-out
          [bst? (any/c . -> . boolean?)]
          [in? (number? bst? . -> . boolean?)]))

在一个完整的二叉搜索树中,这意味着in?函数只需探索一个对数节点。

对in?的合约保证其输入是一个二叉搜索树。但仔细的思考表明,该合约违背了二叉搜索树算法的目的。特别是,考虑到in?函数里内部的cond。这是in?函数获取其速度的地方:它避免在每次递归调用时搜索整个子树。现在把它与bst-between?函数比较。在这种情况下它返回#t,它遍历整个树,意味in?的加速没有实现。

为了解决这个问题,我们可以利用一种新的策略来检查这个二叉搜索树合约。特别是,如果我们只检查了in?看着的节点上的合约,我们仍然可以保证这个树至少部分形成良好,但是没有改变复杂性。

要做到这一点,我们需要使用struct/dc来定义bst-between?。像struct/c一样,struct/dc为一个结构定义一个合约。与struct/c不同,它允许字段被标记为惰性,这样当匹配选择器被调用时,这些合约才被检查。同样,它不允许将可变字段被标记为惰性。

struct/dc表接受这个结构的每个字段的一个合约并返回结构上的一个合约。更有趣的是,struct/dc允许我们编写依赖合约,也就是说,合约中的某些合约取决于其它字段。我们可以用这个去定义二叉搜索树合约:

#lang racket
 
(struct node (val left right))
 
; 确定“n”是否在二进制搜索树“b”中
(define (in? n b) ... as before ...)
 
; bst-between : number number -> contract
 
; 构建了一个二叉搜索树合约
; whose values are between low and high
(define (bst-between/c low high)
  (or/c null?
        (struct/dc node [val (between/c low high)]
                        [left (val) #:lazy (bst-between/c low val)]
                        [right (val) #:lazy (bst-between/c val high)])))
 
(define bst/c (bst-between/c -inf.0 +inf.0))
 
(provide (struct-out node))
(provide (contract-out
          [bst/c contract?]
          [in? (number? bst/c . -> . boolean?)]))

一般来说,struct/dc的每个使用都必须命名字段并且接着为每个字段指定合约。在上面,val字段是一个接受low与high之间的值的合约。left和right字段依赖于val的值,被它们的第二个子表达式所表示。他们也用#:lazy关键字标记以表明它们只有当合适的存取器被结构实例被调用时应该被检查。它们的合约是通过递归调用bst-between/c函数来构建的。综合起来,这个合约确保了在原始示例中被检查的bst-between?函数的同样的事情,但这里这个检查只发生在in?探索这个树时。

虽然这个合约提高了in?的性能,把它恢复到无合约版本的对数行为上,但它仍然施加相当大的恒定开销。因此,这个合约库也提供define-opt/c,它通过优化其主体来降低常数因子。它的形态和上面的define一样。它希望它的主体是一个合约并且接着优化该合约。

(define-opt/c (bst-between/c low high)
  (or/c null?
        (struct/dc node [val (between/c low high)]
                        [left (val) #:lazy (bst-between/c low val)]
                        [right (val) #:lazy (bst-between/c val high)])))

 

7.6 用#:exists和#:∃抽象合约

合约系统提供可以保护抽象化的存在性合约,确保你的模块的客户端不能依赖于为你的数据结构所做的精确表示选择。

如果你不能容易地键入Unicode字符,你可以键入#:exists来代替#:∃;在DrRacket里,键入\exists后跟着alt-\或control-(取决于你的平台)会生成∃。

 

contract-out表允许你编写

#:∃ name-of-a-new-contract

作为其从句之一。这个声明引进这个变量name-of-a-new-contract,将它绑定到一个新的隐藏关于它保护的值的信息的合约。

 

作为一个例子,考虑这(简单的)一列数据结构的实现:

#lang racket
(define empty '())
(define (enq top queue) (append queue (list top)))
(define (next queue) (car queue))
(define (deq queue) (cdr queue))
(define (empty? queue) (null? queue))
 
(provide
 (contract-out
  [empty (listof integer?)]
  [enq (-> integer? (listof integer?) (listof integer?))]
  [next (-> (listof integer?) integer?)]
  [deq (-> (listof integer?) (listof integer?))]
  [empty? (-> (listof integer?) boolean?)]))

此代码纯粹按照列表实现一个队列,这意味着数据结构的客户端可以对数据结构直接使用car和cdr(也许偶然地),从而在描述里的任何改变(例如,对于一个更有效表示,它支持摊销的固定时间队列和队列操作)可能会破坏客户机代码。

为确保这个队列描述是抽象的,我们可以在contract-out表达式里使用#:∃,就像这样:

(provide
 (contract-out
  #:∃ queue
  [empty queue]
  [enq (-> integer? queue queue)]
  [next (-> queue integer?)]
  [deq (-> queue queue)]
  [empty? (-> queue boolean?)]))

现在,如果数据结构的客户端尝试使用car和cdr,它们会收到一个错误,而不是用队列内部的东西来搞砸。

也参见《存在的合约和判断》。

 

7.7 附加实例

本节说明Racket合约实施的当前状态,用一系列来自于《Design by Contract, by Example》[Mitchell02]的例子。

米切尔(Mitchell)和麦金(McKim)的合约设计准则DbC源于1970年代风格的代数规范。DbC的总体目标是依据它的观察指定一个代数的构造器。当我们换种方式表达米切尔和麦金的术语同时我们用最适合的途径,我们保留他们的术语“类”(classes)和“对象”(objects):

  • 从命令中分离查询。

    一个查询(query)返回一个结果但不会改变一个对象的可观察性。一个命令(command)改变一个对象的可见性但不返回结果。在应用程序实现中一个命令通常返回同一个类的一个新对象。

  • 从派生查询中分离基本查询。

    一个派生查询(derived query)返回一个根据基本查询可计算的结果。

  • 对于每个派生查询,编写一个根据基本查询指定结果的岗位条件合约。

  • 对于每个命令,编写一个根据基本查询指定对可观测性更改的岗位条件合约。

  • 对于每个查询和命令,决定一个合适的前置条件合约。

以下各节对应于在米切尔和麦金的书中的一章(但不是所有的章都显示在这里)。我们建议你先阅读合约(在第一模块的末尾附近),然后是实现(在第一个模块中),然后是测试模块(在每一节的结尾)。

米切尔和麦金使用Eiffel语言作为底层编程语言同时采用一个传统的命令式编程风格。我们的长期目标是翻译他们的例子为有应用价值的Racket、面向结构的命令式Racket以及Racket的类系统。

注:模仿米切尔和McKim的参数性非正式概念(参数多态性),我们用一类合约。在几个地方,一类合约的使用改进了米切尔和麦金的设计(参见接口中的注释)。

7.7.1 一个客户管理器组建

为了更好地跟踪漏洞(bug),这第一个模块包含一个独立模块里的一些结构定义。

#lang racket
; data definitions
 
(define id? symbol?)
(define id-equal? eq?)
(define-struct basic-customer (id name address) #:mutable)
 
; interface
(provide
 (contract-out
  [id?                   (-> any/c boolean?)]
  [id-equal?             (-> id? id? boolean?)]
  [struct basic-customer ((id id?)
                          (name string?)
                          (address string?))]))
; end of interface

该模块包含使用上述内容的程序。

#lang racket
 
(require "1.rkt") ; the module just above
 
; implementation
; [listof (list basic-customer? secret-info)]
(define all '())
 
(define (find c)
  (define (has-c-as-key p)
    (id-equal? (basic-customer-id (car p)) c))
  (define x (filter has-c-as-key all))
  (if (pair? x) (car x) x))
 
(define (active? c)
  (pair? (find c)))
 
(define not-active? (compose not active? basic-customer-id))
 
(define count 0)
(define (get-count) count)
 
(define (add c)
  (set! all (cons (list c 'secret) all))
  (set! count (+ count 1)))
 
(define (name id)
  (define bc-with-id (find id))
  (basic-customer-name (car bc-with-id)))
 
(define (set-name id name)
  (define bc-with-id (find id))
  (set-basic-customer-name! (car bc-with-id) name))
 
(define c0 0)
; end of implementation
 
(provide
 (contract-out
  ; how many customers are in the db?
  [get-count (-> natural-number/c)]
  ; is the customer with this id active?
  [active?   (-> id? boolean?)]
  ; what is the name of the customer with this id?
  [name      (-> (and/c id? active?) string?)]
  ; change the name of the customer with this id
  [set-name  (->i ([id id?] [nn string?])
                  [result any/c] ; result contract
                  #:post (id nn) (string=? (name id) nn))]
 
  [add       (->i ([bc (and/c basic-customer? not-active?)])
                  ; A pre-post condition contract must use
                  ; a side-effect to express this contract
                  ; via post-conditions
                  #:pre () (set! c0 count)
                  [result any/c] ; result contract
                  #:post () (> count c0))]))

测试:

#lang racket
(require rackunit rackunit/text-ui "1.rkt" "1b.rkt")
 
(add (make-basic-customer 'mf "matthias" "brookstone"))
(add (make-basic-customer 'rf "robby" "beverly hills park"))
(add (make-basic-customer 'fl "matthew" "pepper clouds town"))
(add (make-basic-customer 'sk "shriram" "i city"))
 
(run-tests
 (test-suite
  "manager"
  (test-equal? "id lookup" "matthias" (name 'mf))
  (test-equal? "count" 4 (get-count))
  (test-true "active?" (active? 'mf))
  (test-false "active? 2" (active? 'kk))
  (test-true "set name" (void? (set-name 'mf "matt")))))

7.7.2 一个参数化(简单)栈

#lang racket
 
; a contract utility
(define (eq/c x) (lambda (y) (eq? x y)))
 
(define-struct stack (list p? eq))
 
(define (initialize p? eq) (make-stack '() p? eq))
(define (push s x)
  (make-stack (cons x (stack-list s)) (stack-p? s) (stack-eq s)))
(define (item-at s i) (list-ref (reverse (stack-list s)) (- i 1)))
(define (count s) (length  (stack-list s)))
(define (is-empty? s) (null? (stack-list s)))
(define not-empty? (compose not is-empty?))
(define (pop s) (make-stack (cdr (stack-list s))
                            (stack-p? s)
                            (stack-eq s)))
(define (top s) (car (stack-list s)))
 
(provide
 (contract-out
  ; predicate
  [stack?     (-> any/c boolean?)]
 
  ; primitive queries
  ; how many items are on the stack?
  [count      (-> stack? natural-number/c)]
 
  ; which item is at the given position?
  [item-at
   (->d ([s stack?] [i (and/c positive? (<=/c (count s)))])
        ()
        [result (stack-p? s)])]
 
  ; derived queries
  ; is the stack empty?
  [is-empty?
   (->d ([s stack?])
        ()
        [result (eq/c (= (count s) 0))])]
 
  ; which item is at the top of the stack
  [top
   (->d ([s (and/c stack? not-empty?)])
        ()
        [t (stack-p? s)] ; a stack item, t is its name
        #:post-cond
        ([stack-eq s] t (item-at s (count s))))]
 
  ; creation
  [initialize
   (->d ([p contract?] [s (p p . -> . boolean?)])
        ()
        ; Mitchell and McKim use (= (count s) 0) here to express
        ; the post-condition in terms of a primitive query
        [result (and/c stack? is-empty?)])]
 
  ; commands
  ; add an item to the top of the stack
  [push
   (->d ([s stack?] [x (stack-p? s)])
        ()
        [sn stack?] ; result kind
        #:post-cond
        (and (= (+ (count s) 1) (count sn))
             ([stack-eq s] x (top sn))))]
 
  ; remove the item at the top of the stack
  [pop
   (->d ([s (and/c stack? not-empty?)])
        ()
        [sn stack?] ; result kind
        #:post-cond
        (= (- (count s) 1) (count sn)))]))

测试:

#lang racket
(require rackunit rackunit/text-ui "2.rkt")
 
(define s0 (initialize (flat-contract integer?=))
(define s2 (push (push s0 2) 1))
 
(run-tests
 (test-suite
  "stack"
  (test-true
   "empty"
   (is-empty? (initialize (flat-contract integer?=)))
  (test-true "push" (stack? s2))
  (test-true
   "push exn"
   (with-handlers ([exn:fail:contract? (lambda _ #t)])
     (push (initialize (flat-contract integer?)) 'a)
     #f))
  (test-true "pop" (stack? (pop s2)))
  (test-equal? "top" (top s2) 1)
  (test-equal? "toppop" (top (pop s2)) 2)))

7.7.3 一个字典

#lang racket
 
; a shorthand for use below
(define-syntax ⇒
  (syntax-rules ()
    [(⇒ antecedent consequent) (if antecedent consequent #t)]))
 
; implementation
(define-struct dictionary (l value? eq?))
; the keys should probably be another parameter (exercise)
 
(define (initialize p eq) (make-dictionary '() p eq))
(define (put d k v)
  (make-dictionary (cons (cons k v) (dictionary-l d))
                   (dictionary-value? d)
                   (dictionary-eq? d)))
(define (rem d k)
  (make-dictionary
   (let loop ([l (dictionary-l d)])
     (cond
       [(null? l) l]
       [(eq? (caar l) k) (loop (cdr l))]
       [else (cons (car l) (loop (cdr l)))]))
   (dictionary-value? d)
   (dictionary-eq? d)))
(define (count d) (length (dictionary-l d)))
(define (value-for d k) (cdr (assq k (dictionary-l d))))
(define (has? d k) (pair? (assq k (dictionary-l d))))
(define (not-has? d) (lambda (k) (not (has? d k))))
; end of implementation
 
; interface
(provide
 (contract-out
  ; predicates
  [dictionary? (-> any/c boolean?)]
  ; basic queries
  ; how many items are in the dictionary?
  [count       (-> dictionary? natural-number/c)]
  ; does the dictionary define key k?
  [has?        (->d ([d dictionary?] [k symbol?])
                    ()
                    [result boolean?]
                    #:post-cond
                    ((zero? (count d)) . ⇒ . (not result)))]
  ; what is the value of key k in this dictionary?
  [value-for   (->d ([d dictionary?]
                     [k (and/c symbol? (lambda (k) (has? d k)))])
                    ()
                    [result (dictionary-value? d)])]
  ; initialization
  ; post condition: for all k in symbol, (has? d k) is false.
  [initialize  (->d ([p contract?] [eq (p p . -> . boolean?)])
                    ()
                    [result (and/c dictionary? (compose zero? count))])]
  ; commands
  ; Mitchell and McKim say that put shouldn't consume Void (null ptr)
  ; for v. We allow the client to specify a contract for all values
  ; via initialize. We could do the same via a key? parameter
  ; (exercise). add key k with value v to this dictionary
  [put         (->d ([d dictionary?]
                     [k (and/c symbol? (not-has? d))]
                     [v (dictionary-value? d)])
                    ()
                    [result dictionary?]
                    #:post-cond
                    (and (has? result k)
                         (= (count d) (- (count result) 1))
                         ([dictionary-eq? d] (value-for result k) v)))]
  ; remove key k from this dictionary
  [rem         (->d ([d dictionary?]
                     [k (and/c symbol? (lambda (k) (has? d k)))])
                    ()
                    [result (and/c dictionary? not-has?)]
                    #:post-cond
                    (= (count d) (+ (count result) 1)))]))
; end of interface

测试:

#lang racket
(require rackunit rackunit/text-ui "3.rkt")
 
(define d0 (initialize (flat-contract integer?=))
(define d (put (put (put d0 'a 2) 'b 2) 'c 1))
 
(run-tests
 (test-suite
  "dictionaries"
  (test-equal? "value for" 2 (value-for d 'b))
  (test-false "has?" (has? (rem d 'b) 'b))
  (test-equal? "count" 3 (count d))
  (test-case "contract check for put: symbol?"
             (define d0 (initialize (flat-contract integer?=))
             (check-exn exn:fail:contract? (lambda () (put d0 "a" 2))))))

7.7.4 一个队列

#lang racket
 
; Note: this queue doesn't implement the capacity restriction
; of Mitchell and McKim's queue but this is easy to add.
 
; a contract utility
(define (all-but-last l) (reverse (cdr (reverse l))))
(define (eq/c x) (lambda (y) (eq? x y)))
 
; implementation
(define-struct queue (list p? eq))
 
(define (initialize p? eq) (make-queue '() p? eq))
(define items queue-list)
(define (put q x)
  (make-queue (append (queue-list q) (list x))
              (queue-p? q)
              (queue-eq q)))
(define (count s) (length  (queue-list s)))
(define (is-empty? s) (null? (queue-list s)))
(define not-empty? (compose not is-empty?))
(define (rem s)
  (make-queue (cdr (queue-list s))
              (queue-p? s)
              (queue-eq s)))
(define (head s) (car (queue-list s)))
 
; interface
(provide
 (contract-out
  ; predicate
  [queue?     (-> any/c boolean?)]
 
  ; primitive queries
  ; Imagine providing this 'query' for the interface of the module
  ; only. Then in Racket there is no reason to have count or is-empty?
  ; around (other than providing it to clients). After all items is
  ; exactly as cheap as count.
  [items      (->d ([q queue?]) () [result (listof (queue-p? q))])]
 
  ; derived queries
  [count      (->d ([q queue?])
                   ; We could express this second part of the post
                   ; condition even if count were a module "attribute"
                   ; in the language of Eiffel; indeed it would use the
                   ; exact same syntax (minus the arrow and domain).
                   ()
                   [result (and/c natural-number/c
                                  (=/c (length (items q))))])]
 
  [is-empty?  (->d ([q queue?])
                   ()
                   [result (and/c boolean?
                                  (eq/c (null? (items q))))])]
 
  [head       (->d ([q (and/c queue? (compose not is-empty?))])
                   ()
                   [result (and/c (queue-p? q)
                                  (eq/c (car (items q))))])]
  ; creation
  [initialize (-> contract?
                  (contract? contract? . -> . boolean?)
                  (and/c queue? (compose null? items)))]
 
  ; commands
  [put        (->d ([oldq queue?] [i (queue-p? oldq)])
                   ()
                   [result
                    (and/c
                     queue?
                     (lambda (q)
                       (define old-items (items oldq))
                       (equal? (items q) (append old-items (list i)))))])]
 
  [rem        (->d ([oldq (and/c queue? (compose not is-empty?))])
                   ()
                   [result
                    (and/c queue?
                           (lambda (q)
                             (equal? (cdr (items oldq)) (items q))))])]))
; end of interface

测试:

#lang racket
(require rackunit rackunit/text-ui "5.rkt")
 
(define s (put (put (initialize (flat-contract integer?=) 2) 1))
 
(run-tests
 (test-suite
  "queue"
  (test-true
   "empty"
   (is-empty? (initialize (flat-contract integer?=)))
  (test-true "put" (queue? s))
  (test-equal? "count" 2 (count s))
  (test-true "put exn"
             (with-handlers ([exn:fail:contract? (lambda _ #t)])
               (put (initialize (flat-contract integer?)) 'a)
               #f))
  (test-true "remove" (queue? (rem s)))
  (test-equal? "head" 2 (head s))))

 

7.8 建立新合约

合约在内部作为函数来表示,这个函数接受关于合约的信息(归咎于谁、源程序位置等等)并产生执行合约的推断(本着Dana Scott的精神)。

一般意义上,一个推断是接受一个任意值的一个函数,并返回满足相应合约的一个值。例如,只接受整数的一个推断对应于合约(flat-contract integer?),同时可以这样编写:

(define int-proj
  (λ (x)
    (if (integer? x)
        x
        (signal-contract-violation))))

作为第二个例子,接受整数上的一元函数的一个推断看起来像这样:

(define int->int-proj
  (λ (f)
    (if (and (procedure? f)
             (procedure-arity-includes? f 1))
        (λ (x) (int-proj (f (int-proj x))))
        (signal-contract-violation))))

虽然这些推断具有恰当的错误行为,但它们还不太适合作为合约使用,因为它们不容纳归咎也不提供良好的错误消息。为了适应这些,合约不只使用简单的推断,而是使用接受一个归咎对象(blame object)的函数将被归咎双方的名字封装起来,以及合约建立的源代码位置和合约名称的记录。然后,它们可以依次传递这些信息给raise-blame-error来发出一个良好的错误信息。

这里是这两个推断中的第一个,被重写以在合约系统中使用:

(define (int-proj blame)
  (λ (x)
    (if (integer? x)
        x
        (raise-blame-error
         blame
         x
         '(expected: "<integer>" given: "~e")
         x))))

新的论据指明了谁将因为正数和负数的合约违约被归咎。

在这个系统中,合约总是建立在双方之间。一方称为服务器,根据这个合约提供一些值;另一方称为客户端,也根据这个合约接受这些值。服务器称为主动位置,客户端称为被动位置。因此,对于仅在整数合约的情况下,唯一可能出错的是所提供的值不是一个整数。因此,永远只有主动的一方(服务器)才能获得归咎。raise-blame-error函数总是归咎主动的一方。

与我们的函数合约的推断的比较:

(define (int->int-proj blame)
  (define dom (int-proj (blame-swap blame)))
  (define rng (int-proj blame))
  (λ (f)
    (if (and (procedure? f)
             (procedure-arity-includes? f 1))
        (λ (x) (rng (f (dom x))))
        (raise-blame-error
         blame
         f
         '(expected "a procedure of one argument" given: "~e")
         f))))

在这种情况下,唯一明确的归咎涵盖了一个提供给合约的非过程或一个这个不接受一个参数的过程的情况。与整数推断一样,这里的归咎也在于这个值的生成器,这就是为什么raise-blame-error传递没有改变的blame。

对于定义域和值域的检查被委托给了int-proj函数,它在int->int-proj函数的前面两行提供其参数。这里的诀窍是,即使int->int-proj函数总是归咎于它所认为的主动方,我们可以通过在给定的归咎对象(blame object)上调用blame-swap来互换归咎方,用被动方更换主动方,反之亦然。

然而,这种技术并不仅仅是一个让这个例子工作的廉价技巧。主动方和被动方的反转是一个函数行为方式的自然结果。也就是说,想象在两个模块之间的一个程序里的值流。首先,一个模块(服务器)定义了一个函数,然后那个模块被另一个模块(客户端)所依赖。到目前为止,这个函数本身必须从原始出发,提供模块给这个需求模块。现在,假设需求模块调用这个函数,为它提供一个参数。此时,值流逆转。这个参数正在从需求模块回流到提供的模块!这个客户端正在“提供”参数给服务器,并且这个服务器正在作为客户端接收那个值。最后,当这个函数产生一个结果时,那个结果在原始方向上从服务器回流到客户端。因此,定义域上的合约倒转了主动的和被动的归咎方,就像值流逆转一样。

我们可以利用这个领悟来概括函数合约并构建一个函数,它接受任意两个合约并为它们之间的函数返回一个合约。

这一推断也走的更远而且在一个合约违反被检测到时使用blame-add-context来改进错误信息。

(define (make-simple-function-contract dom-proj range-proj)
  (λ (blame)
    (define dom (dom-proj (blame-add-context blame
                                             "the argument of"
                                             #:swap? #t)))
    (define rng (range-proj (blame-add-context blame
                                               "the range of")))
    (λ (f)
      (if (and (procedure? f)
               (procedure-arity-includes? f 1))
          (λ (x) (rng (f (dom x))))
          (raise-blame-error
           blame
           f
           '(expected "a procedure of one argument" given: "~e")
           f)))))

虽然这些推断得到了合约库的支持并且可以用来构建新合约,但是这个合约库也为了更有效的推断支持一个不同的API。具体来说,一个后负推断(late neg projection)接受一个不带反面归咎的信息的归咎对象,然后按照这个顺序返回一个函数,它既接受合约约定的值也接受该被动方的名称。这个返回函数接着依次根据合约返回值。看起来像这样重写int->int-proj以使用这个API:

(define (int->int-proj blame)
  (define dom-blame (blame-add-context blame
                                       "the argument of"
                                       #:swap? #t))
  (define rng-blame (blame-add-context blame "the range of"))
  (define (check-int v to-blame neg-party)
    (unless (integer? v)
      (raise-blame-error
       to-blame #:missing-party neg-party
       v
       '(expected "an integer" given: "~e")
       v)))
  (λ (f neg-party)
    (if (and (procedure? f)
             (procedure-arity-includes? f 1))
        (λ (x)
          (check-int x dom-blame neg-party)
          (define ans (f x))
          (check-int ans rng-blame neg-party)
          ans)
        (raise-blame-error
         blame #:missing-party neg-party
         f
         '(expected "a procedure of one argument" given: "~e")
         f))))

这种类型的合约的优点是,blame参数能够在合同边界的服务器一边被提供,而且这个结果可以被用于每个不同的客户端。在较简单的情况下,一个新的归咎对象必须为每个客户端被创建。

最后一个问题在这个合约能够与剩余的合约系统一起使用之前任然存在。在上面的函数中,这个合约通过为f创建一个包装函数来实现,但是这个包装器函数与equal?不协作,它也不让运行时系统知道这里有一个结果函数与输入函数f之间的联系。

为了解决这两个问题,我们应该使用监护(chaperones)而不是仅仅使用λ来创建包装器函数。这里是这个被重写以使用监护的int->int-proj函数:

(define (int->int-proj blame)
  (define dom-blame (blame-add-context blame
                                       "the argument of"
                                       #:swap? #t))
  (define rng-blame (blame-add-context blame "the range of"))
  (define (check-int v to-blame neg-party)
    (unless (integer? v)
      (raise-blame-error
       to-blame #:missing-party neg-party
       v
       '(expected "an integer" given: "~e")
       v)))
  (λ (f neg-party)
    (if (and (procedure? f)
             (procedure-arity-includes? f 1))
        (chaperone-procedure
         f
         (λ (x)
           (check-int x dom-blame neg-party)
           (values (λ (ans)
                     (check-int ans rng-blame neg-party)
                     ans)
                   x)))
        (raise-blame-error
         blame #:missing-party neg-party
         f
         '(expected "a procedure of one argument" given: "~e")
         f))))

如上所述的推断,但适合于其它,你可能制造的新类型的值,可以与合约库原语一起使用。具体来说,我们能够使用make-chaperone-contract来构建它:

(define int->int-contract
  (make-contract
   #:name 'int->int
   #:late-neg-projection int->int-proj))

并且接着将其与一个值相结合并得到一些合约检查。

(define/contract (f x)
  int->int-contract
  "not an int")
 
> (f #f)

f: contract violation;

 expected an integer

  given: #f

  in: the argument of

      int->int

  contract from: (function f)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:5:0

> (f 1)

f: broke its own contract;

 promised an integer

  produced: "not an int"

  in: the range of

      int->int

  contract from: (function f)

  blaming: (function f)

   (assuming the contract is correct)

  at: eval:5:0

7.8.1 合约结构属性

对于一次性合约来说make-chaperone-contract函数是可以的,但通常你想制定许多不同的合约,仅在某些方面不同。做到这一点的最好方法是使用一个struct,带有prop:contractprop:chaperone-contractprop:flat-contract

例如,假设我们想制定接受一个值域合约和一个定义域合约的->合约的一个简单表。我们应该定义一个带有两个字段的结构并使用build-chaperone-contract-property来构建我们需要的监护合约属性。

(struct simple-arrow (dom rng)
  #:property prop:chaperone-contract
  (build-chaperone-contract-property
   #:name
   (λ (arr) (simple-arrow-name arr))
   #:late-neg-projection
   (λ (arr) (simple-arrow-late-neg-proj arr))))

要像integer?和#f那样对值进行自动强制,我们需要调用coerce-chaperone-contract(注意这个拒绝模拟合约并对扁平合约不予坚持;要去做那些事情中的任何一件,而不是调用coerce-contractcoerce-flat-contract)。

(define (simple-arrow-contract dom rng)
  (simple-arrow (coerce-contract 'simple-arrow-contract dom)
                (coerce-contract 'simple-arrow-contract rng)))

去定义simple-arrow-name是直截了当的;它需要返回一个表示合约的S表达式:

(define (simple-arrow-name arr)
  `(-> ,(contract-name (simple-arrow-dom arr))
       ,(contract-name (simple-arrow-rng arr))))

并且我们能够使用我们前面定义的一个广义的推断来定义这个推断,这次使用监护(chaperones):

(define (simple-arrow-late-neg-proj arr)
  (define dom-ctc (get/build-late-neg-projection (simple-arrow-dom arr)))
  (define rng-ctc (get/build-late-neg-projection (simple-arrow-rng arr)))
  (λ (blame)
    (define dom+blame (dom-ctc (blame-add-context blame
                                                  "the argument of"
                                                  #:swap? #t)))
    (define rng+blame (rng-ctc (blame-add-context blame "the range of")))
    (λ (f neg-party)
      (if (and (procedure? f)
               (procedure-arity-includes? f 1))
          (chaperone-procedure
           f
           (λ (arg)
             (values
              (λ (result) (rng+blame result neg-party))
              (dom+blame arg neg-party))))
          (raise-blame-error
           blame #:missing-party neg-party
           f
           '(expected "a procedure of one argument" given: "~e")
           f)))))
(define/contract (f x)
  (simple-arrow-contract integer? boolean?)
  "not a boolean")
 
> (f #f)

f: contract violation

  expected: integer?

  given: #f

  in: the argument of

      (-> integer? boolean?)

  contract from: (function f)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:12:0

> (f 1)

f: broke its own contract

  promised: boolean?

  produced: "not a boolean"

  in: the range of

      (-> integer? boolean?)

  contract from: (function f)

  blaming: (function f)

   (assuming the contract is correct)

  at: eval:12:0

7.8.2 使所有警告和报警一致

这里有一些对一个simple-arrow-contract没有添加的合约的可选部分。在这一节中,我们通过所有的例子来展示它们是如何实现的。

首先是一个一阶检查。这是被or/c使用来确定那一个高阶参数合约在它看到一个值时去使用。下面是我们简单箭头合约的函数。

(define (simple-arrow-first-order ctc)
  (λ (v) (and (procedure? v)
              (procedure-arity-includes? v 1))))

如果这个值确实不满足合约,它接受一个值并返回#f,并且如返回#t,只要我们能够辨别,这个值满足合约,只是检查值的一阶属性。

其次是随机生成。合约库中的随机生成分为两部分:随机生成满足合约的值的能力以及运用匹配这个给定合约的值的能力,希望发现其中的错误(并也试图使它们产生令人感兴趣的值以在生成期间被用于其它地方)。

为了运用合约,我们需要实现一个被给定一个arrow-contract结构的函数和一些辅助函数。它应该返回两个值:一个接受合约值并运用它们的函数;外加运用进程总会产生的一个值列表。在我们简单合约的情况,我们知道我们总能产生值域的值,只要我们能够生成定义域的值(因为我们能够仅调用这个函数)。因此,这里有一个匹配build-chaperone-contract-property的合约的exercise参数的函数:

(define (simple-arrow-contract-exercise arr)
  (define env (contract-random-generate-get-current-environment))
  (λ (fuel)
    (define dom-generate
      (contract-random-generate/choose (simple-arrow-dom arr) fuel))
    (cond
      [dom-generate
       (values
        (λ (f) (contract-random-generate-stash
                env
                (simple-arrow-rng arr)
                (f (dom-generate))))
        (list (simple-arrow-rng arr)))]
      [else
       (values void '())])))

如果定义域合约可以被生成,那么我们知道我们能够通过运用做一些好的事情。在这种情况下,我们返回一个过程,它用我们从定义域生成的东西调用f(匹配这个合约函数),并且我们也在环境中隐藏这个结果值。我们也返回(simple-arrow-rng arr)来表明运用总会产生那个合约的某些东西。

如果我们不能做到,那么我们只简单地返回一个函数,它不运用(void)和空列表(表示我们不会生成任何值)。

然后,为了生成与这个合约相匹配的值,我们定义一个在给定合约和某些辅助函数时成为一个随机函数的函数。为了帮助它成为一个更有效的测试函数,我们可以运用它接受的任何参数,同时也将它们保存到生成环境中,但前提是我们可以生成值域合约的值。

(define (simple-arrow-contract-generate arr)
  (λ (fuel)
    (define env (contract-random-generate-get-current-environment))
    (define rng-generate
      (contract-random-generate/choose (simple-arrow-rng arr) fuel))
    (cond
      [rng-generate
       (λ ()
         (λ (arg)
           (contract-random-generate-stash env (simple-arrow-dom arr) arg)
           (rng-generate)))]
      [else
       #f])))

当这个随机生成将某个东西拉出环境时,它需要能够判断一个被传递给contract-random-generate-stash的值是否是一个试图生成的合约的候选对象。当然,合约传递给contract-random-generate-stash的是一个精确的匹配,那么它就能够使用它。但是,如果这个合约更强(意思是它接受更少的值),它也能够使用这个价值。

为了提供这个功能,我们实现这个函数:

(define (simple-arrow-first-stronger? this that)
  (and (simple-arrow? that)
       (contract-stronger? (simple-arrow-dom that)
                           (simple-arrow-dom this))
       (contract-stronger? (simple-arrow-rng this)
                           (simple-arrow-rng that))))

这个函数接受thisthat,两个合约。它保证this将是我们的简单箭头合约之一,因为我们正在用简单箭头合约实现供应这个函数。但这个that参数也许是任何合约。如果同样比较定义域和值域,这个函数检查以弄明白是否that也是一个简单箭头合约。当然,那里还有其它的合约,我们也可以检查(例如,使用->->*的合约构建),但我们并不需要。如果这个更强的函数不知道答案但如果它返回#t,它被允许返回#f,那么这个合约必须真正变得更强。

既然我们有实现了的所有部分,我们需要传递它们给build-chaperone-contract-property,这样合约系统就开始使用它们了:

(struct simple-arrow (dom rng)
  #:property prop:custom-write contract-custom-write-property-proc
  #:property prop:chaperone-contract
  (build-chaperone-contract-property
   #:name
   (λ (arr) (simple-arrow-name arr))
   #:late-neg-projection
   (λ (arr) (simple-arrow-late-neg-proj arr))
   #:first-order simple-arrow-first-order
   #:stronger simple-arrow-first-stronger?
   #:generate simple-arrow-contract-generate
   #:exercise simple-arrow-contract-exercise))
(define (simple-arrow-contract dom rng)
  (simple-arrow (coerce-contract 'simple-arrow-contract dom)
                (coerce-contract 'simple-arrow-contract rng)))

我们还添加了一个prop:custom-write属性以便这个合约正确打印,例如:

> (simple-arrow-contract integer? integer?)

(-> integer? integer?)

 

(因为合约库不能依赖于

#lang racket/generic

但仍然希望提供一些帮助以便于使用正确的打印机,我们使用prop:custom-write。)

 

既然那些已经完成,我们就能够使用新功能。这里有一个随机函数,它由合约库生成,使用我们的simple-arrow-contract-generate函数:

(define a-random-function
  (contract-random-generate
   (simple-arrow-contract integer? integer?)))
 
> (a-random-function 0)

0

> (a-random-function 1)

0

这里是是合约系统怎么能在使用简单箭头合约的函数中立刻自动发现缺陷(bug):

(define/contract (misbehaved-f f)
  (-> (simple-arrow-contract integer? boolean?any)
  (f "not an integer"))
 
> (contract-exercise misbehaved-f)

misbehaved-f: broke its own contract

  promised: integer?

  produced: "not an integer"

  in: the argument of

      the 1st argument of

      (-> (-> integer? boolean?) any)

  contract from: (function misbehaved-f)

  blaming: (function misbehaved-f)

   (assuming the contract is correct)

  at: eval:25:0

并且如果我们没有实现simple-arrow-first-order,那么or/c就不能够辨别这个程序中使用哪一个or/c分支:

(define/contract (maybe-accepts-a-function f)
  (or/c (simple-arrow-contract real? real?)
        (-> real? real? real?)
        real?)
  (if (procedure? f)
      (if (procedure-arity-includes f 1)
          (f 1132)
          (f 11 2))
      f))
 
> (maybe-accepts-a-function sqrt)

maybe-accepts-a-function: contract violation

  expected: real?

  given: #<procedure:sqrt>

  in: the argument of

      a part of the or/c of

      (or/c

       (-> real? real?)

       (-> real? real? real?)

       real?)

  contract from:

      (function maybe-accepts-a-function)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:27:0

> (maybe-accepts-a-function 123)

123

 

7.9 问题

7.9.1 合约和eq?

作为一般规则,向程序中添加一个合约既应该使程序的行为保持不变,也应该标志出一个合约违反。并且这对于Racket合约几乎是真实的,只有一个例外:eq?

eq?过程被设计为快速且不提供太多的确保方式,除非它返回true,这意味着这两个值在所有方面都是相同的。在内部,这被实现为在一个底层的指针相等,因此它揭示了有关Racket如何被实现的信息(以及合约如何被实现的信息)。

eq?进行合约交互是糟糕的,因为函数合约检查被内部实现为包装器函数。例如,考虑这个模块:

#lang racket
 
(define (make-adder x)
  (if (= 1 x)
      add1
      (lambda (y) (+ x y))))
(provide (contract-out
          [make-adder (-> number? (-> number? number?))]))

除当它的输入是1时它返回Racket的add1外,它输出通常被柯里化为附加函数的make-adder函数。

你可能希望这样:

(eq? (make-adder 1)
     (make-adder 1))

应该返回#t,但它却没有。如果该合约被改为any/c(或者甚至是(-> number? any/c)),那eq?调用将返回#t。

教训:不要对有合约的值使用eq?

7.9.2 合约边界和define/contract

define/contract建立的合约边界,它创建了一个嵌套的合约边界,有时是不直观的。当多个函数或其它带有合约的值相互作用时尤其如此。例如,考虑这两个相互作用的函数:

> (define/contract (f x)
    (-> integer? integer?)
    x)
> (define/contract (g)
    (-> string?)
    (f "not an integer"))
> (g)

f: contract violation

  expected: integer?

  given: "not an integer"

  in: the 1st argument of

      (-> integer? integer?)

  contract from: (function f)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:2:0

人们可能期望这个函数g将因为违反其带f的合约条件而被归咎。如果f和g是直接建立合约的对方,归咎于g就是对的。然而,它们不是。相反,f和g之间的访问是通过封闭模块的顶层被协调的。

更确切地说,f和模块的顶层有(-> integer? integer?)合约协调它们的相互作用,g和顶层有(-> string?)协调它们的相互作用,但是f和g之间没有直接的合约,这意味着在g的主体内对f的引用实际上是模块职责的顶层,而不是g的。换句话说,函数f已经被用在g与顶层之间没有合约的方式赋予g,因此顶层被归咎。

如果我们想在g和顶层之间增加一个合约,我们可以使用define/contract的#:freevar申明并看到预期的归咎:

> (define/contract (f x)
    (-> integer? integer?)
    x)
> (define/contract (g)
    (-> string?)
    #:freevar f (-> integer? integer?)
    (f "not an integer"))
> (g)

f: contract violation

  expected: integer?

  given: "not an integer"

  in: the 1st argument of

      (-> integer? integer?)

  contract from: top-level

  blaming: (function g)

   (assuming the contract is correct)

  at: eval:6:0

教训:如果带合约的两个值应相互作用,在模块边界上将它们放置在具有合约的分开的模块中或使用#:freevar。

7.9.3 存在的合约和判断

很像上面的这个eq?例子,#:∃合约能够改变一个程序的行为。

具体来说,null?判断(和许多其它判断)为#:∃合约返回#f,同时那些合同中的一个改变为any/c意味着null?现在可能反而返回#t,任何不同行为的结果依赖于这个布尔值可以怎样在程序中流动。

 

 #lang racket/exists  package: base

 

要解决上述问题,racket/exists库行为就像racket,但当给定#:∃合约时判断会发出错误信号。

教训:不要使用基于#:∃合约的判断,但是如果你并不确定,用racket/exists在是安全的。

7.9.4 定义递归合约

当定义一个自参考合约时,很自然地去使用define。例如,人们可能试图在像这样的流上编写一个合约:

> (define stream/c
    (promise/c
     (or/c null?
           (cons/c number? stream/c))))

stream/c: undefined;

 cannot reference an identifier before its definition

  in module: top-level

不幸的是,这不会工作,因为stream/c的值在被定义之前就被需要。换句话说,所有的组合器都渴望对它们的参数求值,即使它们不接受这些值。

相反,使用

(define stream/c
  (promise/c
   (or/c
    null?
    (cons/c number? (recursive-contract stream/c)))))

recursive-contract的使用延迟对标识符stream/c的求值,直到合约被首先检查之后,足够长以确保stream/c被定义。

也参见《检查数据结构的特性》。

7.9.5 混合set!contract-out

假定变量通过contract-out输出的合约库没有被分配,但没有执行它。因此,如果你试图set!这些变量,你可能会感到惊讶。考虑下面的例子:

> (module server racket
    (define (inc-x!) (set! x (+ x 1)))
    (define x 0)
    (provide (contract-out [inc-x! (-> void?)]
                           [x integer?])))
> (module client racket
    (require 'server)
  
    (define (print-latest) (printf "x is ~s\n" x))
  
    (print-latest)
    (inc-x!)
    (print-latest))
> (require 'client)

x is 0

x is 0

尽管x的值已经被增加(并且在模块x内可见),两个对print-latest的调用打印0。

为了解决这个问题,输出访问器函数,而不是直接输出变量,像这样:

#lang racket
 
(define (get-x) x)
(define (inc-x!) (set! x (+ x 1)))
(define x 0)
(provide (contract-out [inc-x! (-> void?)]
                       [get-x (-> integer?)]))

教训:这是一个我们将在一个以后版本中讨论的缺陷。

 

输入和输出

一个Racket端口对应一个流的Unix概念(不要与racket/stream的流混淆)。

一个Racket端口(port)代表一个数据源或数据池,诸如一个文件、一个终端、一个TCP连接或者一个内存字符串。端口提供顺序的访问,在那里数据能够被分批次地读或写,而不需要数据被一次性接受或生成。更具体地,一个输入端口(input port)代表一个程序能从中读取数据的一个源,一个输出端口(output port)代表一个程序能够向其中写入数据的一个池。

 

8.1 端口的种类

不同的函数创建不同类型的端口,这里有一些例子:

  • 文件(Files):open-output-file函数打开供写入的一个文件,而open-input-file打开供读取的一个文件。

     

    Examples:

    > (define out (open-output-file "data"))
    > (display "hello" out)
    > (close-output-port out)
    > (define in (open-input-file "data"))
    > (read-line in)

    "hello"

    > (close-input-port in)

     

    如果一个文件已经存在,那open-output-file默认情况下引发一个异常。提供一个如#:exists 'truncate或#:exists 'update的选项来重写或更新这个文件。

     

    Examples:

    > (define out (open-output-file "data" #:exists 'truncate))
    > (display "howdy" out)
    > (close-output-port out)

     

    而不是不得不用关闭调用去匹配打开调用,绝大多数Racket程序员会使用call-with-input-file和call-with-output-file函数接收一个函数去调用以实施预期的操作。这个函数作为端口的唯一参数,它为操作被自动打开与关闭。

     

    Examples:

    > (call-with-output-file "data"
                              #:exists 'truncate
                              (lambda (out)
                                (display "hello" out)))
    > (call-with-input-file "data"
                            (lambda (in)
                              (read-line in)))

    "hello"

     

  • 字符串(Strings):open-output-string函数创建一个将数据堆入一个字符串的一个端口,并且get-output-string提取累加字符串。open-input-string函数创建一个端口用于从字符串读取。

     

    Examples:

    > (define p (open-output-string))
    > (display "hello" p)
    > (get-output-string p)

    "hello"

    > (read-line (open-input-string "goodbye\nfarewell"))

    "goodbye"

     

  • TCP连接(TCP Connections):tcp-connect函数为一个TCP通信的客户端一侧既创建了一个输入端口也创建了一个输出端口。tcp-listen函数创建一个服务器,它通过tcp-accept接受连接。

     

    Examples:

    > (define server (tcp-listen 12345))
    > (define-values (c-in c-out) (tcp-connect "localhost" 12345))
    > (define-values (s-in s-out) (tcp-accept server))
    > (display "hello\n" c-out)
    > (close-output-port c-out)
    > (read-line s-in)

    "hello"

    > (read-line s-in)

    #<eof>

     

  • 进程管道(Process Pipes):subprocess函数运行在操作系统级的一个新的进程并返回与对应子进程的stdin、stdout和stderr的端口。(这首先的三个参数可以将某些现有端口直接连接到子进程,而不是创建新端口。)

     

    Examples:

    > (define-values (p stdout stdin stderr)
        (subprocess #f #f #f "/usr/bin/wc" "-w"))
    > (display "a b c\n" stdin)
    > (close-output-port stdin)
    > (read-line stdout)

    "       3"

    > (close-input-port stdout)
    > (close-input-port stderr)

     

  • 内部管道(Internal Pipes):make-pipe函数返回作为管道末端的两个端口。这种类型的管道属于Racket内部的,并且与用于不同进程之间通信的OS级管道无关。

     

    Examples:

    > (define-values (in out) (make-pipe))
    > (display "garbage" out)
    > (close-output-port out)
    > (read-line in)

    "garbage"

 

8.2 默认端口

对于大多数简单的I/O函数,这个目标端口是一个可选参数,并且这个默认值是当前输入端口(current input port)。此外,错误信息被写入当前错误端口(current error port),这是一个输出端口。current-input-portcurrent-output-portcurrent-error-port函数返回对应的当前端口。

 

Examples:
> (display "Hi")

Hi

> (display "Hi" (current-output-port)) ; 同样

Hi

 

如果你在终端启动racket程序,那么当前输入、输出及错误端口都被连接到终端。更一般地,它们被连接到系统级的stdin、stdout和stderr。在本指南中,示例用紫色显示写入stdout的输出,用红色斜体显示写入stderr的输出。

 

Examples:
(define (swing-hammer)
  (display "Ouch!" (current-error-port)))
 
> (swing-hammer)

Ouch!

 

当前端口函数实际上是参数(parameters),它代表它们的值能够用parameterize设置。

参见《动态绑定:parameterize》以获得对parameters的一个说明。

 

Example:
> (let ([s (open-output-string)])
    (parameterize ([current-error-port s])
      (swing-hammer)
      (swing-hammer)
      (swing-hammer))
    (get-output-string s))

"Ouch!Ouch!Ouch!"

 

8.3 读写Racket数据

就像贯穿始终的《内置的数据类型》,Racket提供三种方式打印一个内建值的一个实例:

  • print, 它以用于打印一个REPL结果的相同方式打印一个值;以及

  • write, 它以在输出上的read产生值的这样一种方式打印一个值;以及

  • display, 它趋向于将一个值缩小到它的字符或字节内容——至少对于那些主要关于字符或字节的数据类型——否则它会回到与write相同的输出。

这里有一些每个使用的例子:

 

> (print 1/2)

1/2

> (print #\x)

#\x

> (print "hello")

"hello"

> (print #"goodbye")

#"goodbye"

> (print '|pea pod|)

'|pea pod|

> (print '("i" pod))

'("i" pod)

> (print write)

#<procedure:write>

 
> (write 1/2)

1/2

> (write #\x)

#\x

> (write "hello")

"hello"

> (write #"goodbye")

#"goodbye"

> (write '|pea pod|)

|pea pod|

> (write '("i" pod))

("i" pod)

> (write write)

#<procedure:write>

 
> (display 1/2)

1/2

> (display #\x)

x

> (display "hello")

hello

> (display #"goodbye")

goodbye

> (display '|pea pod|)

pea pod

> (display '("i" pod))

(i pod)

> (display write)

#<procedure:write>

 

总的来说,print对应Racket语法的表达层,write对应阅读层,display大致对应字符层。

printf函数支持数据与文本的简单格式。在printf支持的格式字符串中,~a display下一个参数,~s write下一个参数,而~v print下一个参数。

 

Examples:

(define (deliver who when what)
  (printf "Items ~a for shopper ~s: ~v" who when what))
 
> (deliver '("list") '("John") '("milk"))

Items (list) for shopper ("John"): '("milk")

 

使用write后,与display或print不同的是,许多数据的表可以通过read重新读入。被print的相同值也能被read解析,但是这个结果也许有额外的引号表,因为一个print的表意味着类似于一个表达式那样被读入。

 

Examples:

> (define-values (in out) (make-pipe))
> (write "hello" out)
> (read in)

"hello"

> (write '("alphabet" soup) out)
> (read in)

'("alphabet" soup)

> (write #hash((a . "apple") (b . "banana")) out)
> (read in)

'#hash((a . "apple") (b . "banana"))

> (print '("alphabet" soup) out)
> (read in)

''("alphabet" soup)

> (display '("alphabet" soup) out)
> (read in)

'(alphabet soup)

 

8.4 数据类型和序列化

预制的(prefab)结构类型(查看《预制结构类型》)自动支持序列化(serialization):它们可被写入一个输出流同时一个副本可以从输入流中读回:

> (define-values (in out) (make-pipe))
> (write #s(sprout bean) out)
> (read in)

'#s(sprout bean)

struct创建的其它结构类型,提供较预制的结构类型更多的抽象,通常write既使用#<....>记号(对于不透明结构类型)也使用#(....)矢量记号(对于透明结构类型)。不论在那种情况下这个结果都不能作为结构类型的一个实例被读回。

> (struct posn (x y))
> (write (posn 1 2))

#<posn>

> (define-values (in out) (make-pipe))
> (write (posn 1 2) out)
> (read in)

pipe::1: read: bad syntax `#<`

> (struct posn (x y) #:transparent)
> (write (posn 1 2))

#(struct:posn 1 2)

> (define-values (in out) (make-pipe))
> (write (posn 1 2) out)
> (define v (read in))
> v

'#(struct:posn 1 2)

> (posn? v)

#f

> (vector? v)

#t

serializable-struct表定义一个结构类型,它能够被serialize为一个值,这个值可使用write被打印并通过read读入。serialize的结果可被deserialize为原始结构类的一个实例。序列化表和函数通过racket/serialize库提供。

 

Examples:
> (require racket/serialize)
> (serializable-struct posn (x y) #:transparent)
> (deserialize (serialize (posn 1 2)))

(posn 1 2)

> (write (serialize (posn 1 2)))

((3) 1 ((#f . deserialize-info:posn-v0)) 0 () () (0 1 2))

> (define-values (in out) (make-pipe))
> (write (serialize (posn 1 2)) out)
> (deserialize (read in))

(posn 1 2)

 

除了被struct绑定的名字外,serializable-struct绑定一个具有反序列化信息的标识,并且它会自动从一个模块上下文provide这个反序列化标识。当一个值被反序列化时这个反序列化标识被反射地访问。

 

8.5 字节、字符和编码

类似read-line、read、display和write的函数都根据字符(character)(它对应于Unicode标量值)工作。概念上来说,它们根据read-char和write-char被实现。

更初级一点,端口读和写字节(byte)而不是字符(character)。函数read-byte与write-byte读和写原始字节。其它函数,比如read-bytes-line,建立在字节操作的顶层而不是字符操作。

事实上,read-char和write-char函数概念上根据read-byte和write-byte被实现。当一个单一字节的值小于128时,那么它对应于一个ASCII字符。任何其它的字节被视为一个UTF-8序列的一部分,其中UTF-8是以字节为单位的编码Unicode标量值的一个特殊标准方式(它具有ASCII字符作为它们自身编码的优良属性)。此外,一个单个read-char可能调用read-byte多次,并且一个单个write-char可能生成多个输出字节。

read-char和write-char操作总使用一个UTF-8编码。如果你有一个使用一个不同编码的文本流,或者如果你想在一个不同编码中生成一个文本流,使用reencode-input-port或reencode-output-port。reencode-input-port函数从一个你指定为一个UTF-8流的编码中转换一个输入流;以这种方式,read-char明白UTF-8编码,即使这个源文件使用了一个不同的编码。但要小心,那个read-byte也明白这个重编码数据,而不是原始字节流。

 

8.6 I/O模式

如果你想处理一个文件的单个行,那么你可以用in-lines使用for

> (define (upcase-all in)
    (for ([l (in-lines in)])
      (display (string-upcase l))
      (newline)))
> (upcase-all (open-input-string
               (string-append
                "Hello, World!\n"
                "Can you hear me, now?")))

HELLO, WORLD!

CAN YOU HEAR ME, NOW?

如果你想确定是否“hello”出现在一个文件中,那你可以搜索独立的行,但是更简便的方法是对这个流简单应用一个正则表达式(参见《正则表达式》):

> (define (has-hello? in)
    (regexp-match? #rx"hello" in))
> (has-hello? (open-input-string "hello"))

#t

> (has-hello? (open-input-string "goodbye"))

#f

如果你想拷贝一个端口至另一个,使用来自racket/port的copy-port,它能够在大量数据可用时有效转移大的块,但如果可以的话也立即转移小数据块。:

> (define o (open-output-string))
> (copy-port (open-input-string "broom") o)
> (get-output-string o)

"broom"

 

正则表达式

本章是[Sitaram05]的一个修改版本。

一个正则表达式(regexp)值封装一个被一个字符串或字节字符串(bytestring)描述的模式。当你调用像regexp-match那样的函数时,正则表达式匹配器尝试对(一部分)其它的字符串或字节字符串匹配这个模式,我们将其称为文本字符串(text string)。文本字符串被视为原始文本,而不会视为一个模式。

在《Racket参考》中的“正则表达式(regexp)”部分提供有更多关于正则表达式的内容。

 

9.1 编写正则表达式模式

一个字符串或字节字符串(byte string)可以被直接用作一个正则表达式(regexp)模式,也可以用#rx来形成一个字面上的正则表达式值来做前缀。例如,#rx"abc"是一个基于字符串的正则表达式值,并且#rx"abc"是一个基于字节字符串的正则表达式值。或者,一个字符串或字节字符串可以用#px做前缀,就像在#px"abc"中,给一个稍微扩展的字符串内的模式的语法。

在一个正则表达式模式的大多数字符都表示在文本字符串中自相匹配。因此,该模式#rx"abc"匹配在继承中包含字符a、b和c的一个字符串。其它字符充当元字符(metacharacters),而且许多字符序列充当元序列(metasequences)。也就是说,它们指定的东西不是它们字面本身。例如,在模#rx"a.c"中,字符a和c代表它们自己,但元字符.可以匹配任何字符。因此,该模式#rx"a.c"在继承中匹配一个a、任意字符以及c。

当我们想要在一个Racket字符串或正则表达式原义里的一个字面原义的\,我们必须将它转义以便它出现在所有字符串中。Racket字符串使用\作为转义字符,所以我们用两个\结束:一个Racket字符串\转义正则表达式\,它接着转义.。另一个将要在Racket字符串里转义的字符是"。

如果我们需要匹配字符.本身,我们可以在它前面加上一个\来转义。字符序列\.就是一个元序列(metasequence),因为它不匹配它本身而只是.。所以,连续匹配a、.和c,我们使用正则表达式模#rx"a\\.c"。双\字符是一个Racket字符串技巧,不是正则表达式模式本身。

regexp-quote函数接受一个字符串或字节字符串并产生一个正则表达式值。当你构建一个模式来匹配多个字符串时用regexp,因为一个模式在它可以被用在一个匹配之前被编译成了一个正则表达式值。这个pregexp函数类似于regexp,除了使用扩展语法之外。正则表达式值作为带#rx或#px的字面形式被编译一次并且当它们被读取时都这样。

regexp-quote函数接受任意的字符串并返回一个模式匹配原始字符串。特别是,在输入字符串中的字符,可以作为正则表达式元字符用一个反斜杠转义,所以只有它们自己使他们安全地匹配。

> (regexp-quote "cons")

"cons"

> (regexp-quote "list?")

"list\\?"

regexp-quote函数在从一个混合的正则表达式字符串和字面的字符串构建一个完整的正则表达式是有用的。

 

9.2 匹配正则表达式模式

regexp-match-positions函数接受一个正则表达模式和一个文本字符串,如果这个正则表达式匹配(某部分)这个文本字符串则返回一个匹配,或这如果这个正则表达式不匹配这个字符串则返回#f。一个成功的匹配产生一个索引序对(index pairs)列表。

 

Examples:
> (regexp-match-positions #rx"brain" "bird")

#f

> (regexp-match-positions #rx"needle" "hay needle stack")

'((4 . 10))

 

在第二个例子中,整数4和10确定匹配的子串。这个4是起始(包含)索引,同时这个10是匹配子字符串的结尾(不包含)索引:

> (substring "hay needle stack" 4 10)

"needle"

第一个例子中,regexp-match-positions的返回列表只包含一个索引序对,并且那个索引序对代表由正则表达式匹配整个字符串。当我们论述了子模式(subpatterns)后,我们将明白为什么一个单独的匹配操作可以产生一个子匹配(submatch)列表。

regexp-match-positions函数接受可选的第三和第四个参数,他们在这个匹配应该发生之中指定文本字符串的指标。

> (regexp-match-positions
   #rx"needle"
   "his needle stack -- my needle stack -- her needle stack"
   20 39)

'((23 . 29))

注意,返回指标仍然与完整的文字符串相对应。

regexp-match函数类似于regexp-match-positions,但它不是返回索引序对,它返回这个匹配的子字符串:

> (regexp-match #rx"brain" "bird")

#f

> (regexp-match #rx"needle" "hay needle stack")

'("needle")

regexp-match在字节字符串表达式中使用时,结果是一个匹配的字节子字符串:

> (regexp-match #rx#"needle" #"hay needle stack")

'(#"needle")

一个字节字符串正则表达式可以被应用到一个字符串,而且一个字符串正则表达式可以应用到一个字节字符串。在这两种情况下,结果都是一个字节字符串。在内部,所有的正则表达式匹配是以字节为单位,并且一个字符串正则表达式被扩展到一个匹配字符的UTF-8编码的正则表达式。为最大限度地提高效率,使用字节字符串匹配代替字符串匹配,因为匹配字节直接避开了UTF-8编码。

如果你有在端口中的数据,这里无需首先将其读取到字符串中。像regexp-match这样的函数可以在端口上直接匹配:

> (define-values (i o) (make-pipe))
> (write "hay needle stack" o)
> (close-output-port o)
> (regexp-match #rx#"needle" i)

'(#"needle")

regexp-match?函数类似于regexp-match-positions,但只简单地返回一个指示是否匹配成功的布尔值:

> (regexp-match? #rx"brain" "bird")

#f

> (regexp-match? #rx"needle" "hay needle stack")

#t

regexp-split函数接受两个参数,一个正则表达式模式和一个文本字符串,并返回一个文本字符串的子串列表;这个模式识别分隔子字符串的分隔符。

> (regexp-split #rx":" "/bin:/usr/bin:/usr/bin/X11:/usr/local/bin")

'("/bin" "/usr/bin" "/usr/bin/X11" "/usr/local/bin")

> (regexp-split #rx" " "pea soup")

'("pea" "soup")

如果第一个参数匹配空字符串,那么返回所有的单字符子字符串的列表。

> (regexp-split #rx"" "smithereens")

'("" "s" "m" "i" "t" "h" "e" "r" "e" "e" "n" "s" "")

因此,识别一个或多个空格作为分隔符,注意使用正则表达#rx" +",而不是#rx" *"。

> (regexp-split #rx" +" "split pea     soup")

'("split" "pea" "soup")

> (regexp-split #rx" *" "split pea     soup")

'("" "s" "p" "l" "i" "t" "" "p" "e" "a" "" "s" "o" "u" "p" "")

regexp-replace函数用另一个字符串替换这个文本字符串匹配的部分。第一个参数是模式,第二个参数是文本字符串,第三个参数是要插入的字符串或者一个将匹配转换为插入字符串的过程。

> (regexp-replace #rx"te" "liberte" "ty")

"liberty"

> (regexp-replace #rx"." "racket" string-upcase)

"Racket"

如果该模式没有出现在这个文本字符串中,返回的字符串与这个文本字符串相同。

regexp-replace*函数通过这个插入的字符串在这个文本字符串中代替所有相匹配的内容:

> (regexp-replace* #rx"te" "liberte egalite fraternite" "ty")

"liberty egality fratyrnity"

> (regexp-replace* #rx"[ds]" "drracket" string-upcase)

"Drracket"

 

9.3 基本申明

这个判断(assertions) ^和$分别标识这个文本字符串的开头和结尾。它们确保在文本字符串的一个或其它尾部相邻的正则表达式匹配:

> (regexp-match-positions #rx"^contact" "first contact")

#f

以上正则表达式匹配失败是因为contact没有出现在文本字符串的开头。在

> (regexp-match-positions #rx"laugh$" "laugh laugh laugh laugh")

'((18 . 23))

中,正则表达式匹配最后的laugh。

这个元序列\b判断一个字的边界存在,但这个元序列只能与#px语法一起工作。在

> (regexp-match-positions #px"yack\\b" "yackety yack")

'((8 . 12))

里,yackety中的这个yack不结束于字边界,所以它并不匹配。第二yack在字边界结束,所以匹配。

元序列\B(也只有#px)对\b有相反的影响;它判断一个字边界不存在。在

> (regexp-match-positions #px"an\\B" "an analysis")

'((3 . 5))

里,这个不在一个字边界结束的an被匹配。

 

9.4 字符和字符类

通常,在正则表达式中的一个字符匹配文本字符串中的相同字符。有时使用一个正则表达式元序列(metasequence)来引用一个单个字符是有必要的或方便的。例如,这个元序列\.匹配句点字符。

这个元字符(metacharacter).匹配任意字符(除了在多行模式(multi-line mode)中的换行,参见《回廊》(Cloisters)):

> (regexp-match #rx"p.t" "pet")

'("pet")

上面的模式也匹配pat、pit、pot、put和p8t,但不匹配peat或pfffft。

一个字符类(character class)匹配来自一组字符中的任意一个字符。对这种情况的一个典型的格式是括号字符类(bracketed character class)[...],它匹配任意一个来自包含在括号内的非空序列的字符。因此,#rx"p[aeiou]t"匹配pat、pet、pit、pot、put,别的都不匹配。

在括号内,一个-介于两个字符之间指定字符之间的Unicode范围。例如,#rx"ta[b-dgn-p]"匹配tab、tac、tad、tag、tan、tao和tap。

在左括号后的一个初始^将反转通过剩下的内容指定的集合;也就是说,它指定的这个字符集排除括号中标识的字符集。例如,#rx"do[^g]"匹配所有以 do开始但不是dog的三字符序列。

注意括号之内的元字符^,它在括号里边的意义与在外边的意义截然不同。大多数其它的元字符(.、*、+、?,等等)当在括号之内时不再是元字符,即使你一直也许一直不予承认以求得内心平静。仅当它在括号内,并且当它既不是括号之间的第一个字符也不是最后一个字符时,一个-是一个元字符。

括号内的字符类不能包含其它被括号包裹的字符类(虽然它们包含字符类的某些其它类型,见下)。因此,在一个被括起来的字符类里的一个[不必是一个元字符;它可以代表自身。比如,#rx"[a[b]"匹配a、[和b。

此外,由于空括号字符类是不允许的,一个]立即出现在开左括号后也不必是一个元字符。比如,#rx"[]ab]"匹配]、a和b。

9.4.1 常用的字符类

在#px语法里,一些标准的字符类可以方便地表示为元序列以代替明确的括号内表达式:\d匹配一个数字(与[0-9]一样);\s匹配一个ASCII空白字符;而\w匹配可以是一个“字(word)”的一部分的一个字符。

遵循正则表达式惯例,我们确定”字“字符为[A-Za-z0-9_],虽然这些对一个可能会看重一个”字“的Racket使用者来说过于严格。

这些元序列的这个大写版本代表对应字符类的倒转:\D匹配一个非数字,\S匹配一个非空格字符,而\W匹配一个非“字”字符。

在把这些元序列放进一个Racket字符串里时,记得要包含一个双反斜杠:

> (regexp-match #px"\\d\\d"
   "0 dear, 1 have 2 read catch 22 before 9")

'("22")

这些字符类可以使用进一个括号表达式中。比如,#px"[a-z\\d]"匹配一个小写字母或一个数字。

9.4.2 POSIX字符类

一个POSIX(可移植性操作系统接口)字符类是表[:...:]的一种特殊的元序列(metasequence),这个表只能用在 #px语法中的一个括号表达式内。这个POSIX类支持

  • [:alnum:] — ASCII字母和数字

  • [:alpha:] — ASCII字母

  • [:ascii:] — ASCII字符

  • [:blank:] — ASCII 等宽空格:空格和tab

  • [:cntrl:] — “控制(control)”字符:ASCII 0到32

  • [:digit:] — ASCII码数字,同\d

  • [:graph:] — 使用墨水的ASCII字符

  • [:lower:] — ASCII小写字母

  • [:print:] — ASCII墨水用户加等宽空白

  • [:space:] — ASCII空白, 同\s

  • [:upper:] — ASCII大写字母

  • [:word:] — ASCII字母和_,同\w

  • [:xdigit:] — ASCII十六进制数字

例如,这个#px"[[:alpha:]_]"匹配一个字母或下划线

> (regexp-match #px"[[:alpha:]_]" "--x--")

'("x")

> (regexp-match #px"[[:alpha:]_]" "--_--")

'("_")

> (regexp-match #px"[[:alpha:]_]" "--:--")

#f

这个POSIX类符号只(only)适用于在一个带括号的表达式内。比如[:alpha:],当不在一个带括号的表达式内时,不会被当做字母类读取。确切地说,它是(来自以前的准则)包含字符::、a、l、p、h的字符类。

> (regexp-match #px"[:alpha:]" "--a--")

'("a")

> (regexp-match #px"[:alpha:]" "--x--")

#f

 

9.5 量词

这个量词(quantifier) *、 +和 ?分别匹配:零个或多个,一个或多个,以及零个或一个前面子模式的实例。

> (regexp-match-positions #rx"c[ad]*r" "cadaddadddr")

'((0 . 11))

> (regexp-match-positions #rx"c[ad]*r" "cr")

'((0 . 2))

> (regexp-match-positions #rx"c[ad]+r" "cadaddadddr")

'((0 . 11))

> (regexp-match-positions #rx"c[ad]+r" "cr")

#f

> (regexp-match-positions #rx"c[ad]?r" "cadaddadddr")

#f

> (regexp-match-positions #rx"c[ad]?r" "cr")

'((0 . 2))

> (regexp-match-positions #rx"c[ad]?r" "car")

'((0 . 3))

在#px语法里,你可以使用括号来指定比用*、+、?更精细的调整量:

  • 这个量词{m}精确匹配前面子模式的m实例;m必须是一个非负整数。

  • 这个量词{m,n}最少匹配m且最多匹配n个实例。m和n是非负整数,m小于或等于n。你可以省略一个数字或两个数字都省略,在这种情况下默认m为0,n为无穷大。

很明显,+和?是{1,}和{0,1}的缩写,*是{,}的缩写,这个和{0,}一样。

> (regexp-match #px"[aeiou]{3}" "vacuous")

'("uou")

> (regexp-match #px"[aeiou]{3}" "evolve")

#f

> (regexp-match #px"[aeiou]{2,3}" "evolve")

#f

> (regexp-match #px"[aeiou]{2,3}" "zeugma")

'("eu")

到目前为止所描述的量词都是贪婪的(greedy):它们匹配最大数量的实例,这样会导致一个对整个模式的总体匹配。

> (regexp-match #rx"<.*>" "<tag1> <tag2> <tag3>")

'("<tag1> <tag2> <tag3>")

为了使这些量词成为非贪婪的(non-greedy),给它们追加一个?。非贪婪量词匹配最小数量的实例需要确保整体匹配。

> (regexp-match #rx"<.*?>" "<tag1> <tag2> <tag3>")

'("<tag1>")

非贪婪量词分别为:*?、+?、??、{m}?、{m,n}?。注意元字符?的这两种使用。

 

9.6 

簇(Clustering)——圈占于括号内(...)——识别封闭的子模式(subpattern)作为一个单一的实体。它导致匹配器去捕获子匹配(submatch),或者字符串的一部分匹配这个子模式,除了整体匹配除外:

> (regexp-match #rx"([a-z]+) ([0-9]+), ([0-9]+)" "jan 1, 1970")

'("jan 1, 1970" "jan" "1" "1970")

簇也导致一个后面的量词把整个封闭模式作为一个实体处理:

> (regexp-match #rx"(pu )*" "pu pu platter")

'("pu pu " "pu ")

返回的匹配项数量总是等于在正则表达式中指定的子模式数量,即使一个特定的子模式碰巧匹配不止一个子字符串或根本没有子串。

> (regexp-match #rx"([a-z ]+;)*" "lather; rinse; repeat;")

'("lather; rinse; repeat;" " repeat;")

在这里,这个*量化的子模式匹配三次,但这是被返回的最后一个匹配项。

对一个量化的子模式来说不匹配也是可能的,即使是整个模式匹配。在这种情况下,这个失败的子匹配被#f体现。

> (define date-re
    ; 匹配“月年”或“月日年”
    ; 子模式匹配天,如果目前
    #rx"([a-z]+) +([0-9]+,)? *([0-9]+)")
> (regexp-match date-re "jan 1, 1970")

'("jan 1, 1970" "jan" "1," "1970")

> (regexp-match date-re "jan 1970")

'("jan 1970" "jan" #f "1970")

9.6.1 反向引用

子匹配可用于插入程序regexp-replaceregexp-replace*的字符串参数。这个插入的字符串可以使用\n做为反向引用(backreference)来反向引用第n个匹配项,它是匹配第n个子模式的子字符串。一个\0引用整个匹配,并且它也可以被指定为\&。

> (regexp-replace #rx"_(.+?)_"
    "the _nina_, the _pinta_, and the _santa maria_"
    "*\\1*")

"the *nina*, the _pinta_, and the _santa maria_"

> (regexp-replace* #rx"_(.+?)_"
    "the _nina_, the _pinta_, and the _santa maria_"
    "*\\1*")

"the *nina*, the *pinta*, and the *santa maria*"

> (regexp-replace #px"(\\S+) (\\S+) (\\S+)"
    "eat to live"
    "\\3 \\2 \\1")

"live to eat"

在插入字符串中使用\\来指定一个字面反斜杠。同样,\$代表一个空字符串,并且对从一个紧随其后的数字来分离一个反向引用\n是有用的。

反向引用也可以用在一个#px模式内部来引用返回给这个模式里的一个已经匹配的子模式。\n代表第n个子匹配的一个精确重复。注意这个\0,它在一个插入的字符串中是有用的,在正则表达式模式内部毫无意义,因为整个正则表达式不匹配而无法对它反向引用。

> (regexp-match #px"([a-z]+) and \\1"
                "billions and billions")

'("billions and billions" "billions")

注意,这个反向引用不是简单的一个以前子模式的重复。而是一个已经被子模式匹配的特定子字符串的重复。

在上面的例子中,反向引用只能匹配billions。它不会匹配millions,即使这个子模式重新回到——([a-z]+)——这样做会没有问题:

> (regexp-match #px"([a-z]+) and \\1"
                "billions and millions")

#f

下面的示例在一个数字字符串中标记所有立即重复的模式:

> (regexp-replace* #px"(\\d+)\\1"
    "123340983242432420980980234"
    "{\\1,\\1}")

"12{3,3}40983{24,24}3242{098,098}0234"

下面的示例修正重叠字:

> (regexp-replace* #px"\\b(\\S+) \\1\\b"
    (string-append "now is the the time for all good men to "
                   "to come to the aid of of the party")
    "\\1")

"now is the time for all good men to come to the aid of the party"

9.6.2 非捕捉簇

它通常需要指定一个簇(通常用于量化)但没有触发子匹配信息的捕捉。这种簇被称为非捕捉(non-capturing)。为了创建一个非捕捉簇,使用(?:代替(作为这个簇开启器。

在下面的例子中,一个非捕捉簇消除了一个给定UNIX路径名的“目录”部分,并一个捕捉簇识别出基本名。

但不要用正则表达式解析路径。使用诸如split-path之类的函数来代替。

> (regexp-match #rx"^(?:[a-z]*/)*([a-z]+)$"
                "/usr/local/bin/racket")

'("/usr/local/bin/racket" "racket")

9.6.3 回廊

非捕捉簇的?和:之间的位置称为回廊(cloister)。你可以在此处放置修改器,以使簇的子模式(subpattern)得到特殊处理。修饰符i使子模式不敏感地匹配大小写:

回廊(cloister)是一个有用的术语,如果最终可爱,来自Perl的住持创造的词语。

> (regexp-match #rx"(?i:hearth)" "HeartH")

'("HeartH")

修饰符m使子模式subpattern)在多行模式(multi-line mode)匹配,在.的位置不匹配换行符,^仅在一个新行后可以匹配,而$仅在一个新行前可以匹配。

> (regexp-match #rx"." "\na\n")

'("\n")

> (regexp-match #rx"(?m:.)" "\na\n")

'("a")

> (regexp-match #rx"^A plan$" "A man\nA plan\nA canal")

#f

> (regexp-match #rx"(?m:^A plan$)" "A man\nA plan\nA canal")

'("A plan")

你可以在回廊里放置多个修饰符:

> (regexp-match #rx"(?mi:^A Plan$)" "a man\na plan\na canal")

'("a plan")

在修饰符前的一个减号反转它的意义。因此,你可以在子簇(subcluster)中使用-i以翻转由封闭簇导致的案例不敏感。

> (regexp-match #rx"(?i:the (?-i:TeX)book)"
                "The TeXbook")

'("The TeXbook")

上述正表达式将允许任何针对the和book的包装,但它坚持认为TeX没有不同的包装。

 

9.7 替补

你可以通过用|分隔它们来指定替补(alternate)子模式(subpatterns)的列表。在最近的封闭簇里|分隔子模式(或在整个模式字符串里,假如没有封闭括号)。

> (regexp-match #rx"f(ee|i|o|um)" "a small, final fee")

'("fi" "i")

> (regexp-replace* #rx"([yi])s(e[sdr]?|ing|ation)"
                   (string-append
                    "analyse an energising organisation"
                    " pulsing with noisy organisms")
                   "\\1z\\2")

"analyze an energizing organization pulsing with noisy organisms"

不过注意,如果你想使用簇仅仅是指定替补子模式列表,却不想指定匹配项,那么使用(?:代替(。

> (regexp-match #rx"f(?:ee|i|o|um)" "fun for all")

'("fo")

注意替补的一个重要事情是,最左匹配替补不管长短。因此,如果一个替补是后一个替补的前缀,后者可能没有机会匹配。

> (regexp-match #rx"call|call-with-current-continuation"
                "call-with-current-continuation")

'("call")

若要让较长的替补进行匹配,请将其放在较短的替补之前:

> (regexp-match #rx"call-with-current-continuation|call"
                "call-with-current-continuation")

'("call-with-current-continuation")

在任何情况下,对整个正则表达式的整体匹配始终优先于整体的不匹配。在下面例子中,较长的替补仍然获胜,因为其首选的较短前缀无法产生整体匹配。

> (regexp-match
   #rx"(?:call|call-with-current-continuation) constrained"
   "call-with-current-continuation constrained")

'("call-with-current-continuation constrained")

 

9.8 回溯

我们已经看到贪婪量词匹配最大次数,但最重要的是整体匹配成功。考虑以下内容

> (regexp-match #rx"a*a" "aaaa")

'("aaaa")

这个正则表达式包括两个子正则表达式:a*后跟a。子正则表达式a*不能匹配文本字符串aaaa里的所有的四个a,即使*是一个贪婪量词也一样。它可能仅匹配前面的三个,剩下最后一个留给第二子正则表达式。这样确保完整的正则表达式匹配成功。

正则表达式匹配器通过一个称为回溯(backtracking)的过程实现来这个。匹配器暂时允许贪婪量词匹配所有四个a,但当整体匹配处于危险中时,它回溯(backtracks)到一个不那么贪婪的三个a的匹配。如果这也失败了,与以下调用一样

> (regexp-match #rx"a*aa" "aaaa")

'("aaaa")

匹配器回溯得更远。只有当所有可能的回溯尝试都没有成功时,才承认整体失败。

回溯并不局限于贪婪量词。非贪婪量词尽可能少地匹配实例,并逐步回溯到越来越多的实例,以获得整体匹配。替补中也有回溯,因为当局部成功的向左替补未能产生整体匹配时,会尝试更向右的替补。

有时禁用回溯是有效的。例如,我们可能希望作出选择,或者我们知道尝试替补是徒劳的。一个非回溯正则表达式是括在(?>...)里的。

> (regexp-match #rx"(?>a+)." "aaaa")

#f

在这个调用里,子正则表达式?>a+贪婪地匹配所有四个a,并且剥夺了回溯的机会。因此,整体匹配被拒绝。于是,这个正则表达式的效果是匹配一个或多个a,后跟一些绝对不是a的内容。

 

9.9 前寻与后寻

你的模式中可以有前寻或后寻以确保子模式发生或不发生。这些“环顾”主张是通过将选中的子模式放入一个前导字符为:?=(用于主动前寻),?!(被动前寻),?<=(主动后寻),?<!(被动后寻)。请注意,主张中的子模式不会在最终结果中生成匹配;它只允许或不允许其余的匹配。

9.9.1 前寻

带?=的主动前寻检查其子模式是否可以立即与文本字符串中当前位置的左侧匹配。

> (regexp-match-positions #rx"grey(?=hound)"
    "i left my grey socks at the greyhound")

'((28 . 32))

正则表达式#rx"grey(?=hound)"匹配grey,但仅仅如果它后面紧跟着hound时成立。因此,文本字符串中的第一个grey不匹配。

被动后寻?!检查其子模式是否可能立即与左侧匹配。

> (regexp-match-positions #rx"grey(?!hound)"
    "the gray greyhound ate the grey socks")

'((27 . 31))

正则表达式#rx"grey(?!hound)"匹配grey,但前提是后面不跟有hound。因此,grey仅仅在socks之前才匹配。

9.9.2 后寻

带?<=的主动后寻检查其子模式是否可以立即与文本字符串中当前位置的左侧匹配。

> (regexp-match-positions #rx"(?<=grey)hound"
    "the hound in the picture is not a greyhound")

'((38 . 43))

正则表达式#rx"(?<=grey)hound"匹配hound,但前提是前面是grey。

带?<!的被动后寻检查其子模式是否可能立即与左侧匹配。

> (regexp-match-positions #rx"(?<!grey)hound"
    "the greyhound in the picture is not a hound")

'((38 . 43))

正则表达式#rx"(?<!grey)hound"匹配hound,但前提是前面没有grey。

在不混淆的情况下,前寻和后寻可以很方便。

 

9.10 一个扩展示例

下面是一个扩展的例子,来自Friedl的《精通正则表达式(Mastering Regular Expressions)》,第189页,它涵盖了本章中描述的许多特性。问题是要设计一个正则表达式,它将匹配任何且仅匹配IP地址或点分的四个四位数:四个由三个点分隔的数字,每个数字介于0和255之间。

首先,我们定义一个与0到255匹配的子正则表达式n0-255:

> (define n0-255
    (string-append
     "(?:"
     "\\d|"        ;  0 through 9
     "\\d\\d|"     ;  00 through 99
     "[01]\\d\\d|" ; 000 through 199
     "2[0-4]\\d|"  ; 200 through 249
     "25[0-5]"     ; 250 through 255
     ")"))

请注意n0-255将前缀列为首选替代项,这是我们在替补要注意的。但是,由于我们打算显式地锚定这个子正则表达式以强制进行整体匹配,所以交替的顺序并不重要。

前两个替补简单地得到所有的单位和双位数字。因为允许0填充,所以我们需要同时匹配1和01。我们在得到三位数的数字时需要小心,因为必须排除255以上的数字。因此,我们交替使用000到199,然后是200到249,最后是250到255。

IP地址是一个由四个个n0-255组成的字符串,用三个点分隔。

> (define ip-re1
    (string-append
     "^"        ; 前面什么都没有
     n0-255     ; 第一个n0-255,
     "(?:"      ; 接着是子模式
     "\\."      ; 被一个点跟着
     n0-255     ; 一个 n0-255,
     ")"        ; 它被
     "{3}"      ; 恰好重复三遍
     "$"))
; 后边什么也没有

让我们试试看:

> (regexp-match (pregexp ip-re1) "1.2.3.4")

'("1.2.3.4")

> (regexp-match (pregexp ip-re1) "55.155.255.265")

#f

这很好,除此之外我们还有

> (regexp-match (pregexp ip-re1) "0.00.000.00")

'("0.00.000.00")

所有零序列都不是有效的IP地址!前寻以救援。在开始匹配ip-re1之前,我们前寻以确保我们没有所有的零。我们可以使用主动前寻来确保有一个数字不是零。

> (define ip-re
    (pregexp
     (string-append
       "(?=.*[1-9])" ; ensure there's a non-0 digit
       ip-re1)))

或者我们可以使用被动前寻来确保前面的内容不只是由零和点组成。

> (define ip-re
    (pregexp
     (string-append
       "(?![0.]*$)" ; 不只是零点和点
                    ; (注:.不是在[...]里面的匹配器)
       ip-re1)))

正则表达式ip-re将匹配所有且仅匹配有效的IP地址。

> (regexp-match ip-re "1.2.3.4")

'("1.2.3.4")

> (regexp-match ip-re "0.0.0.0")

#f

 

10 异常与控制

Racket提供了一组特别丰富的控制操作——不仅是用于提高和捕捉异常的操作,还包括抓取和恢复计算部分的操作。

 

10.1 异常

每当发生运行时错误时,就会引发异常(exception)。除非捕获异常,然后通过打印与异常相关联的消息来处理,然后从计算中逃逸。

> (/ 1 0)

/: division by zero

> (car 17)

car: contract violation

  expected: pair?

  given: 17

若要捕获异常,请使用with-handlers表:

(with-handlers ([predicate-expr handler-expr] ...)
  body ...+)

在处理器中的每个predicate-expr确定一种异常,它由with-handlers表捕获,代表异常的值传递给处理器程序由handler-expr生成。handler-expr的结果即with-handlers表达式的结果。

例如,零做除数错误创建了exn:fail:contract:divide-by-zero结构类型:

> (with-handlers ([exn:fail:contract:divide-by-zero?
                   (lambda (exn) +inf.0)])
    (/ 1 0))

+inf.0

> (with-handlers ([exn:fail:contract:divide-by-zero?
                   (lambda (exn) +inf.0)])
    (car 17))

car: contract violation

  expected: pair?

  given: 17

error函数是引起异常的一种方法。它打包一个错误信息和其它信息进入exn:fail结构:

> (error "crash!")

crash!

> (with-handlers ([exn:fail? (lambda (exn) 'air-bag)])
    (error "crash!"))

'air-bag

exn:fail:contract:divide-by-zero和exn:fail结构类型是exn结构类型的子类型。核心表和核心函数引起的异常总是创建exn的或其子类的一个实例,但异常不必通过结构表示。raise函数允许你创建任何值作为异常:

> (raise 2)

uncaught exception: 2

> (with-handlers ([(lambda (v) (equal? v 2)) (lambda (v) 'two)])
    (raise 2))

'two

> (with-handlers ([(lambda (v) (equal? v 2)) (lambda (v) 'two)])
    (/ 1 0))

/: division by zero

在一个with-handlers表里的多个predicate-expr让你在不同的途径处理各种不同的异常。判断按顺序进行尝试,如果没有匹配,则将异常传播到封闭上下文中。

> (define (always-fail n)
    (with-handlers ([even? (lambda (v) 'even)]
                    [positive? (lambda (v) 'positive)])
      (raise n)))
> (always-fail 2)

'even

> (always-fail 3)

'positive

> (always-fail -3)

uncaught exception: -3

> (with-handlers ([negative? (lambda (v) 'negative)])
   (always-fail -3))

'negative

使用(lambda (v) #t)作为判断捕获所有异常,当然:

> (with-handlers ([(lambda (v) #t) (lambda (v) 'oops)])
    (car 17))

'oops

然而,捕获所有异常通常是个坏主意。如果用户在一个终端窗口键入Ctl-C或者在DrRacket点击停止按钮(Stop)中断计算,那么通常exn:break异常不会被捕获。仅仅会抓取具有代表性的错误,使用exn:fail?作为判断:

> (with-handlers ([exn:fail? (lambda (v) 'oops)])
    (car 17))

'oops

> (with-handlers ([exn:fail? (lambda (v) 'oops)])
    (break-thread (current-thread)) ; simulate Ctl-C
    (car 17))

user break

 

10.2 提示和中止

当一个异常被引发时,控制将从一个任意深度的求值上下文逃逸到异常被捕获的位置——或者如果没有捕捉到异常,那么所有的出路都会消失:

> (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (/ 1 0)))))))

/: division by zero

但如果控制逃逸“所有的出路”,为什么REPL在一个错误被打印之后能够继续运行?你可能会认为这是因为REPL把每一个互动封装进了with-handlers表里,它抓取了所有的异常,但这确实不是原因。

实际的原因是,REPL用一个提示(prompt)封装了互动,有效地用一个逃逸位置标记求值上下文。如果一个异常没有被捕获,那么关于异常的信息被打印,然后求值中止(aborts)到最近的封闭提示。更确切地说,每个提示有提示标签(prompt tag),并有指定的默认提示标签(default prompt tag),未捕获的异常处理程序用来中止。

call-with-continuation-prompt函数用一个给定的提示标签设置提示,然后在提示符下对一个给定的铛(thunk)求值。default-continuation-prompt-tag函数返回默认提示标记。abort-current-continuation函数转义到具有给定提示标签的最近的封闭提示符。

> (define (escape v)
    (abort-current-continuation
     (default-continuation-prompt-tag)
     (lambda () v)))
> (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (escape 0)))))))

0

> (+ 1
     (call-with-continuation-prompt
      (lambda ()
        (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (escape 0))))))))
      (default-continuation-prompt-tag)))

1

在上面的escape中,值v被封装在一个过程中,该过程在转义到封闭提示符后被调用。

提示(prompts)和中止(aborts)看起来非常像异常处理和引发。事实上,提示和中止本质上是一种更原始的异常形式,与with-handlersraise都是按提示执行和中止。更原始形式的权力与操作符名称中的“延续(continuation)”一词有关,我们将在下一节中讨论。

 

10.3 延续

延续(continuation)是一个值,该值封装了表达式的求值上下文。call-with-composable-continuation函数从当前函数调用和运行到最近的外围提示捕获当前延续(current continuation)。(记住,每个REPL互动都是隐含地封装在一个提示中。)

例如,在下面内容里

(+ 1 (+ 1 (+ 1 0)))

在求值0的位置,表达式上下文包含三个嵌套的加法表达式。我们可以通过更改0来获取上下文,然后在返回0之前获取延续:

> (define saved-k #f)
> (define (save-it!)
    (call-with-composable-continuation
     (lambda (k) ; k is the captured continuation
       (set! saved-k k)
       0)))
> (+ 1 (+ 1 (+ 1 (save-it!))))

3

保存在save-k中的延续封装程序上下文(+ 1 (+ 1 (+ 1 ?))),?代表插入结果值的位置——因为在save-it!被调用时这是表达式上下文。延续被封装从而其行为类似于函数(lambda (v) (+ 1 (+ 1 (+ 1 v)))):

> (saved-k 0)

3

> (saved-k 10)

13

> (saved-k (saved-k 0))

6

通过call-with-composable-continuation捕获的延续是动态确定的,没有语法。例如,用

> (define (sum n)
    (if (zero? n)
        (save-it!)
        (+ n (sum (sub1 n)))))
> (sum 5)

15

在saved-k里延续成为(lambda (x) (+ 5 (+ 4 (+ 3 (+ 2 (+ 1 x)))))):

> (saved-k 0)

15

> (saved-k 10)

25

在Racket(或Scheme)中较传统的延续运算符是call-with-current-continuation,它通常缩写为call/cc。这是像call-with-composable-continuation,但应用捕获的延续在还原保存的延续前首先中止(对于当前提示)。此外,Scheme系统传统上支持程序启动时的单个提示符,而不是通过call-with-continuation-prompt允许新提示。在Racket中延续有时被称为分隔的延续(delimited continuations),因为一个程序可以引入新定义的提示,并且作为call-with-composable-continuation捕获的延续有时被称为组合的延续(composable continuations),因为他们没有一个内置的中止。

作为一个延续是多么有用的例子,请参见《 更多:用Racket进行系统编程(More: Systems Programming with Racket)》。对于具体的控制操作符,它有比这里描述的原语更恰当的名字,请参见racket/control部分。

 

11 迭代和推导

用于语法形式的for家族支持对序列(sequences)进行迭代。列表、向量、字符串、字节字符串、输入端口和散列表都可以用作序列,像in-range的构造函数可以提供更多类型的序列。

for的变种以不同的方式累积迭代结果,但它们都具有相同的语法形态。 现在简化了,for的语法是

(for ([id sequence-expr] ...)
  body ...+)

for循环遍历由sequence-expr生成的序列。 对于序列的每个元素,for将元素绑定到id,然后副作用求值body

 

Examples:
> (for ([i '(1 2 3)])
    (display i))

123

> (for ([i "abc"])
    (printf "~a..." i))

a...b...c...

> (for ([i 4])
    (display i))

0123

 

forfor/list变体更像Racket。它将body结果累积到列表中,而不是仅仅副作用求值body。 在更多的技术术语中,for/list实现了列表理解(list comprehension)

 

Examples:
> (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*变体可以嵌套迭代,而不是并行运行。 forfor*的更多变体以不同的方式产生积累body结果。 在所有这些变体中,包含迭代的判断可以同时包含绑定。

不过,在for的变体细节之前,最好是先查看生成有趣示例的序列生成器的类型。

11.1 序列构造器

in-range函数生成数值序列,给定可选的起始数字(默认为0),序列结束前的数字和可选的步长(默认为1)。 直接使用非负整数k作为序列是对(in-range k)的简写。

 

Examples:
> (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,没有上限。for循环只使用in-naturals将永远不会终止,除非正文表达引发异常或以其它方式退出。

 

Example:
> (for ([i (in-naturals)])
    (if (= i 10)
        (error "too much!")
        (display i)))

0123456789

too much!

 

stop-before函数和stop-after函数构造给定序列和判断的新的序列。这个新序列就像这个给定的序列,但是在判断返回true的第一个元素之前或之后立即被截断。

 

Example:
> (for ([i (stop-before "abc def"
                        char-whitespace?)])
    (display i))

abc

 

in-listin-vectorin-string这样的序列构造器只是简单地使用列表(list)、向量(vector)和字符串(string)作为序列。和in-range一样,这些构造器在给定错误类型的值时会引发异常,并且由于它们会避免运行时调度来确定序列类型,因此可以实现更高效的代码生成; 有关更多信息,请参阅迭代性能

 

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

 

11.2 forfor*

更完整的for语法是

(for (clause ...)
  body ...+)
 
clause   =   [id sequence-expr]
    |   #:when boolean-expr
    |   #:unless boolean-expr

当多个[id sequence-expr]子句在一个for表里提供时,相应的序列并行遍历:

> (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产生一个真值时它允许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子句是类似的,但仅当boolean-expr产生非值时对body求值。

11.3 for/listfor*/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")

使用for/list的#:when修剪行为比for更有用。而对for来说直接的when表通常是满足需要的,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的求值放入一个新构造的向量而不是列表:

> (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/vectorfor*/vector表也允许构造向量的长度。由此产生的迭代可以比直接的for/vectorfor*/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")

如果提供了长度,当向量被填充或被请求完成时迭代停止,而无论哪个先来。如果所提供的长度超过请求的迭代次数,则向量中的剩余位置被初始化为make-vector的缺省参数。

11.5 for/andfor/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*/andfor*/or表提供与嵌套迭代相同的功能。

11.6 for/firstfor/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*/firstfor*/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/foldfor*/fold

for/fold表是合并迭代结果的一种非常通用的方法。由于必须在开始时声明累积变量,它的语法与原来的for语法略有不同:

(for/fold ([accum-id init-expr...)
          (clause ...)
  body ...+)

在简单的情况下,仅提供[accum-id init-expr],那么for/fold的结果是accum-id的最终值,并启动了init-expr的值。在clausebodyaccum-id可参照获得其当前值,并且最后的body为下一次迭代的提供accum-id值。

 

Examples:
> (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-idfor/fold的表达式本身给结果产生多值。

 

Example:
> (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 多值序列

同样,函数或表达式可以生成多个值,序列的单个迭代可以生成多个元素。例如,作为序列的哈希表生成两个迭代的两个值:一个键和一个值。

同样方式,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 打断迭代

更完整的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为真时,for中的所有序列都将停止。处在body内,除了当boolean-expr是真时,#:break对序列有一样的效果,并且它也阻止随后的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 迭代性能

理想情况下,作为递归函数调用,for迭代的运行速度应该与手工编写的循环一样快。然而,手写循环通常是针对特定类型的数据,如列表。在这种情况下,手写循环直接使用选择器,比如carcdr,而不是处理所有序列表并分派给合适的迭代器。

当足够的信息反复提供给迭代序列时,for表可以提供手写循环的性能。具体来说,子句应具有下列fast-clause表之一:

 

  fast-clause   =   [id fast-seq]
    |   [(idfast-seq]
    |   [(id idfast-indexed-seq]
    |   [(id ...) fast-parallel-seq]

 

 

  fast-seq   =   (in-range expr)
    |   (in-range expr expr)
    |   (in-range expr expr expr)
    |   (in-naturals)
    |   (in-naturals expr)
    |   (in-list 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)

 

 

Examples:
> (time (for ([i (in-range 100000)])
          (for ([elem (in-list '(a b c d e f g h))]) ; 快
            (void))))

cpu time: 2 real time: 2 gc time: 0

> (time (for ([i (in-range 100000)])
          (for ([elem '(a b c d e f g h)])           ; 慢
            (void))))

cpu time: 2 real time: 2 gc time: 0

> (time (let ([seq (in-list '(a b c d e f g h))])
          (for ([i (in-range 100000)])
            (for ([elem seq])                        ; 慢
              (void)))))

cpu time: 19 real time: 19 gc time: 0

 

上面的语法是不完整的,因为提供良好性能的语法模式集是可扩展的,就像序列值集合一样。序列构造器的文档应该说明直接使用for子句(clause)的性能优势。

 

12 模式匹配

match表支持对任意Racket值的模式匹配,而不是像regexp-match那样的函数,将正则表达式与字符及字节序列比较(参见正则表达式)。

(match target-expr
  [pattern expr ...+] ...)

match表获取target-expr的结果并试图按顺序匹配每个pattern。一旦它找到一个匹配,对相应的expr序列求值以得到匹配(match)表的结果。如果pattern包括模式变量(pattern variables),他们被当作通配符,并且在expr里的每个变量被绑定给的被匹配的输入片段。

大多数Racket的字面表达式可以用作模式:

> (match 2
    [1 'one]
    [2 'two]
    [3 'three])

'two

> (match #f
    [#t 'yes]
    [#f 'no])

'no

> (match "apple"
    ['apple 'symbol]
    ["apple" 'string]
    [#f 'boolean])

'string

conslistvector这样的构造器,可以用于创建模式,以匹配配对、列表和向量:

> (match '(1 2)
    [(list 0 1) 'one]
    [(list 1 2) 'two])

'two

> (match '(1 . 2)
    [(list 1 2) 'list]
    [(cons 1 2) 'pair])

'pair

> (match #(1 2)
    [(list 1 2) 'list]
    [(vector 1 2) 'vector])

'vector

struct绑定的一个构造器也可以用作一个模式构造器:

> (struct shoe (size color))
> (struct hat (size style))
> (match (hat 23 'bowler)
   [(shoe 10 'white) "bottom"]
   [(hat 23 'bowler) "top"])

"top"

不带引号的,在一个模式中的非构造器标识是模式变量(pattern variables),它在结果表达式中被绑定,除了_,它不绑定(因此,这通常是作为一个泛称(笼统描述)):

> (match '(1)
    [(list x) (+ x 1)]
    [(list x y) (+ x y)])

2

> (match '(1 2)
    [(list x) (+ x 1)]
    [(list x y) (+ x y)])

3

> (match (hat 23 'bowler)
    [(shoe sz col) sz]
    [(hat sz stl) sz])

23

> (match (hat 11 'cowboy)
    [(shoe sz 'black) 'a-good-shoe]
    [(hat sz 'bowler) 'a-good-hat]
    [_ 'something-else])

'something-else

省略号,写作...,就像在列表或向量模式中的克莱尼星号(Kleene star):前面的子模式可以用于对列表或向量元素的任意数量的连续元素的任意次匹配。如果后跟省略号的子模式包含一个模式变量,这个变量会匹配多次,并在结果表达式里被绑定到一个匹配列表中:

> (match '(1 1 1)
    [(list 1 ...) 'ones]
    [_ 'other])

'ones

> (match '(1 1 2)
    [(list 1 ...) 'ones]
    [_ 'other])

'other

> (match '(1 2 3 4)
    [(list 1 x ... 4) x])

'(2 3)

> (match (list (hat 23 'bowler) (hat 22 'pork-pie))
    [(list (hat sz styl) ...) (apply + sz)])

45

省略号可以嵌套以匹配嵌套的重复,在这种情况下,模式变量可以绑定到匹配列表的列表中:

> (match '((! 1) (! 2 2) (! 3 3 3))
    [(list (list '! x ......) x])

'((1) (2 2) (3 3 3))

quasiquote表(见《准引用:quasiquote和‘》以获取更多关于它的信息)也可以用来建立模式。而一个通常的准引用(quasiquote)表的非引用部分意味着普通的racket求值,这里非引用部分意味着回到普通模式匹配。

因此,在下面的例子中,with表达模式是模式并且它被改写成应用表达式,在第一个实例里用准引用作为一个模式,在第二个实例里准引用构建一个表达式。

> (match `{with {x 1} {+ x 1}}
    [`{with {,id ,rhs} ,body}
     `{{lambda {,id} ,body} ,rhs}])

'((lambda (x) (+ x 1)) 1)

有关更多模式表的信息,请参见racket/match。

match-letmatch-lambda的表支持位置模式,否则必须是标识。例如,match-letlet归纳为解构绑定(destructing bind):

> (match-let ([(list x y z) '(1 2 3)])
    (list z y x))

'(3 2 1)

有关这些附加表的信息,请参见racket/match。

 

13 类和对象

本章基于一篇论文[Flatt06]。

一个类(class)表达式表示一类值,就像一个lambda表达式一样:

(class superclass-expr decl-or-expr ...)

superclass-expr确定为新类的基类。每个decl-or-expr既是一个声明,关系到对方法、字段和初始化参数,也是一个表达式,每次求值就实例化类。换句话说,与方法之类的构造器不同,类具有与字段和方法声明交错的初始化表达式。

按照惯例,类名以%结束。内置根类是object%。下面的表达式用公共方法get-size、grow和eat创建一个类:

(class object%
  (init size)                ; 初始化参数
 
  (define current-size size) ; 字段
 
  (super-new)                ; 基类初始化
 
  (define/public (get-size)
    current-size)
 
  (define/public (grow amt)
    (set! current-size (+ amt current-size)))
 
  (define/public (eat other-fish)
    (grow (send other-fish get-size))))

当通过new表实例化类时,size的初始化参数必须通过一个命名参数提供:

(new (class object% (init size) ....) [size 10])

当然,我们还可以命名类及其实例:

(define fish% (class object% (init size) ....))
(define charlie (new fish% [size 10]))

在fish%的定义中,current-size是一个以size值初始化参数开头的私有字段。像size这样的初始化参数只有在类实例化时才可用,因此不能直接从方法引用它们。与此相反,current-size字段可用于方法。

在class中的(super-new)表达式调用基类的初始化。在这种情况下,基类是object%,它没有带初始化参数也没有执行任何工作;必须使用super-new,因为一个类总必须总是调用其基类的初始化。

初始化参数、字段声明和表达式如(super-new)可以以类(class)中的任何顺序出现,并且它们可以与方法声明交织在一起。类中表达式的相对顺序决定了实例化过程中的求值顺序。例如,如果一个字段的初始值需要调用一个方法,它只有在基类初始化后才能工作,然后字段声明必须放在super-new调用后。以这种方式排序字段和初始化声明有助于规避不可避免的求值。方法声明的相对顺序对求值没有影响,因为方法在类实例化之前被完全定义。

13.1 方法

fish%中的三个define/public声明都引入了一种新方法。声明使用与Racket函数相同的语法,但方法不能作为独立函数访问。调用fish%对象的grow方法需要send表:

> (send charlie grow 6)
> (send charlie get-size)

16

在fish%中,自方法可以被像函数那样调用,因为方法名在作用域中。例如,fish%中的eat方法直接调用grow方法。在类中,试图以除方法调用以外的任何方式使用方法名会导致语法错误。

在某些情况下,一个类必须调用由基类提供但不能被重写的方法。在这种情况下,类可以使用带this的send来访问该方法:

(define hungry-fish% (class fish% (super-new)
                       (define/public (eat-more fish1 fish2)
                         (send this eat fish1)
                         (send this eat fish2))))
 
 

另外,类可以声明一个方法使用inherit(继承)的存在,该方法将方法名引入到直接调用的作用域中:

(define hungry-fish% (class fish% (super-new)
                       (inherit eat)
                       (define/public (eat-more fish1 fish2)
                         (eat fish1) (eat fish2))))
 
 

在inherit声明中,如果fish%没有提供一个eat方法,那么在对 hungry-fish%类表的求值中会出现一个错误。与此相反,用(send this ....),直到eat-more方法被调和send表被求值前不会发出错误信号。因此,inherit是首选。

send的另一个缺点是它比inherit效率低。一个方法的请求通过send调用寻找在运行时在目标对象的类的方法,使send类似于java方法调用接口。相反,基于inherit的方法调用使用一个类的方法表中的偏移量,它在类创建时计算。

为了在从方法类之外调用方法时实现与继承方法调用类似的性能,程序员必须使用generic(泛型)表,它生成一个特定类和特定方法的generic方法,用send-generic调用:

(define get-fish-size (generic fish% get-size))
 
> (send-generic charlie get-fish-size)

16

> (send-generic (new hungry-fish% [size 32]) get-fish-size)

32

> (send-generic (new object%) get-fish-size)

generic:get-size: target is not an instance of the generic's

class

  target: (object)

  class name: fish%

粗略地说,表单将类和外部方法名转换为类方法表中的位置。如上一个例子所示,通过泛型方法发送检查它的参数是泛型类的一个实例。

是否在class内直接调用方法,通过泛型方法,或通过send,方法以通常的方式重写工程:

(define picky-fish% (class fish% (super-new)
                      (define/override (grow amt)
 
                        (super grow (* 3/4 amt)))))
(define daisy (new picky-fish% [size 20]))
 
> (send daisy eat charlie)
> (send daisy get-size)

32

在picky-fish%的grow方法是用define/override声明的,而不是 define/public,因为grow是作为一个重写的申明的意义。如果grow已经用define/public声明,那么在对类表达式求值时会发出一个错误,因为fish%已经提供了grow。

使用define/override也允许通过super调用调用重写的方法。例如,grow在picky-fish%实现使用super代理给基类的实现。

13.2 初始化参数

因为picky-fish%申明没有任何初始化参数,任何初始化值在(new picky-fish% ....)里提供都被传递给基类的初始化,即传递给fish%。子类可以在super-new调用其基类时提供额外的初始化参数,这样的初始化参数会优先于参数提供给new。例如,下面的size-10-fish%类总是产生大小为10的鱼:

(define size-10-fish% (class fish% (super-new [size 10])))
 
> (send (new size-10-fish%) get-size)

10

就size-10-fish%来说,用new提供一个size初始化参数会导致初始化错误;因为在super-new里的size优先,size提供给new没有目标申明。

如果class表声明一个默认值,则初始化参数是可选的。例如,下面的default-10-fish%类接受一个size的初始化参数,但如果在实例里没有提供值那它的默认值是10:

(define default-10-fish% (class fish%
                           (init [size 10])
                           (super-new [size size])))
 
> (new default-10-fish%)

(object:default-10-fish% ...)

> (new default-10-fish% [size 20])

(object:default-10-fish% ...)

在这个例子中,super-new调用传递它自己的size值作为size初始化初始化参数传递给基类。

13.3 内部和外部名称

在default-10-fish%中size的两个使用揭示了类成员标识符的双重身份。当size是new或super-new中的一个括号对的第一标识符,size是一个外部名称(external name),象征性地匹配到类中的初始化参数。当size作为一个表达式出现在default-10-fish%中,size是一个内部名称(internal name),它是词法作用域。类似地,对继承的eat方法的调用使用eat作为内部名称,而一个eat的send的使用作为一个外部名称。

class表的完整语法允许程序员为类成员指定不同的内部和外部名称。由于内部名称是本地的,因此可以重命名它们,以避免覆盖或冲突。这样的改名不总是必要的,但重命名缺乏的解决方法可以是特别繁琐。

13.4 接口

接口对于检查一个对象或一个类实现一组具有特定(隐含)行为的方法非常有用。接口的这种使用有帮助的,即使没有静态类型系统(那是java有接口的主要原因)。

Racket中的接口通过使用interface表创建,它只声明需要去实现的接口的方法名称。接口可以扩展其它接口,这意味着接口的实现会自动实现扩展接口。

(interface (superinterface-expr ...) id ...)

为了声明一个实现一个接口的类,必须使用class*表代替class:

(class* superclass-expr (interface-expr ...) decl-or-expr ...)

例如,我们不必强制所有的fish%类都是源自于fish%,我们可以定义fish-interface并改变fish%类来声明它实现了fish-interface:

(define fish-interface (interface () get-size grow eat))
(define fish% (class* object% (fish-interface) ....))

如果fish%的定义不包括get-size、grow和eat方法,那么在class*表求值时会出现错误,因为实现fish-interface接口需要这些方法。

is-a?判断接受一个对象作为它的第一个参数,同时类或接口作为它的第二个参数。当给了一个类,无论对象是该类的实例或者派生类的实例,is-a?都执行检查。当给一个接口,无论对象的类是否实现接口,is-a?都执行检查。另外,implementation?判断检查给定类是否实现给定接口。

13.5 Final、Augment和Inner

在java中,一个class表的方法可以被指定为最终的(final),这意味着一个子类不能重写方法。一个最终方法是使用public-final或override-final申明,取决于声明是为一个新方法还是一个重写实现。

在允许与不允许任意完全重写的两个极端之间,类系统还支持Beta类型的可扩展(augmentable)方法。一个带pubment声明的方法类似于public,但方法不能在子类中重写;它仅仅是可扩充。一个pubment方法必须显式地使用inner调用一个扩展(如果有);一个子类使用pubment扩展方法,而不是使用override。

一般来说,一个方法可以在类派生的扩展模式和重写模式之间进行切换。augride方法详述表明了一个扩展,这里这个扩展本身在子类中是可重写的的方法(虽然这个基类的实现不能重写)。同样,overment重写一个方法并使得重写的实现变得可扩展。

13.6 控制外部名称的范围

java的访问修饰符(如受保护的(protected))扮演的一个角色类似于define-member-name,但不像java,访问控制Racket的机制是基于词法范围,不能继承层次结构。

正如内部和外部名称所指出的,类成员既有内部名称,也有外部名称。成员定义在本地绑定内部名称,此绑定可以在本地重命名。与此相反,外部名称默认情况下具有全局范围,成员定义不绑定外部名称。相反,成员定义指的是外部名称的现有绑定,其中成员名绑定到成员键(member key);一个类最终将成员键映射到方法、字段和初始化参数。

回头看hungry-fish%类(class)表达式:

(define hungry-fish% (class fish% ....
                       (inherit eat)
                       (define/public (eat-more fish1 fish2)
                         (eat fish1) (eat fish2))))

在求值过程中hungry-fish%类和fish%类指相同的eat的全局绑定。在运行时,在hungry-fish%中调用eat是通过共享绑定到eat的方法键和fish%中的eat方法相匹配。

对外部名称的默认绑定是全局的,但程序员可以用define-member-name表引入外部名称绑定。

(define-member-name id member-key-expr)

特别是,通过使用(generate-member-key)作为member-key-expr,外部名称可以为一个特定的范围局部化,因为生成的成员键范围之外的访问。换句话说,define-member-name给外部名称一种私有包范围,但从包中概括为Racket中的任意绑定范围。

例如,下面的fish%类和pond%类通过一个get-depth方法配合,只有这个配合类可以访问:

(define-values (fish% pond%) ; 两个相互递归类
  (let ()
    (define-member-name get-depth (generate-member-key))
    (define fish%
      (class ....
        (define my-depth ....)
        (define my-pond ....)
        (define/public (dive amt)
        (set! my-depth
              (min (+ my-depth amt)
                   (send my-pond get-depth))))))
    (define pond%
      (class ....
        (define current-depth ....)
        (define/public (get-depth) current-depth)))
    (values fish% pond%)))

外部名称在名称空间中,将它们与其它Racket名称分隔开。这个单独的命名空间被隐式地用于send中的方法名、在new中的初始化参数名称,或成员定义中的外部名称。特殊表 member-name-key提供对任意表达式位置外部名称的绑定的访问:(member-name-key id)在当前范围内生成id的成员键绑定。

成员键值主要用于define-member-name表。通常,(member-name-key id)捕获id的方法键,以便它可以在不同的范围内传递到define-member-name的使用。这种能力证明推广混合是有用的,作为接下来的讨论。

13.7 混合

因为class(类)是一种表达表,而不是如同在Smalltalk和java里的一个顶级的声明,一个class表可以嵌套在任何词法范围内,包括lambda(λ)。其结果是一个混合(mixin),即一个类的扩展,是相对于它的基类的参数化。

例如,我们可以参数化picky-fish%类来覆盖它的基类从而定义picky-mixin:

(define (picky-mixin %)
  (class % (super-new)
    (define/override (grow amt) (super grow (* 3/4 amt)))))
(define picky-fish% (picky-mixin fish%))

Smalltalk风格类和Racket类之间的许多小的差异有助于混合的有效利用。特别是,define/override的使用使得picky-mixin期望一个类带有一个grow方法更明确。如果picky-mixin应用于一个没有grow方法的类,一旦应用picky-mixin则会发出一个错误的信息。

同样,当应用混合时使用inherit(继承)执行“方法存在(method existence)”的要求:

(define (hungry-mixin %)
  (class % (super-new)
    (inherit eat)
    (define/public (eat-more fish1 fish2)
      (eat fish1)
      (eat fish2))))

mixin的优势是,我们可以很容易地将它们结合起来以创建新的类,其共享的实现不适合一个继承层次——没有多继承相关的歧义。装配picky-mixin和hungry-mixin来为“hungry”创造一个类,但“picky fish”是直接的:

(define picky-hungry-fish%
  (hungry-mixin (picky-mixin fish%)))

关键词初始化参数的使用是混合的易于使用的重点。例如,picky-mixin和hungry-mixin可以通过合适的eat方法和grow方法增加任何类,因为它们在super-new表达式里没有指定初始化参数也没有添加东西:

(define person%
  (class object%
    (init name age)
    ....
    (define/public (eat food) ....)
    (define/public (grow amt) ....)))
(define child% (hungry-mixin (picky-mixin person%)))
(define oliver (new child% [name "Oliver"] [age 6]))

最后,对类成员的外部名称的使用(而不是词法作用域标识)使得混合使用很方便。添加picky-mixin到person%运行,因为这个名字eat和grow匹配,在fish%和person%里没有任何eat和grow的优先申明可以是同样的方法。当成员名称意外冲突后,此特性是一个潜在的缺陷;一些意外冲突可以通过限制外部名称作用域来纠正,就像在《控制外部名称的范围(Controlling the Scope of External Names)》所讨论的那样。

13.7.1 混合和接口

使用implementation?,picky-mixin可能需求其基类实现grower-interface,这可以是由fish%和person%实现:

(define grower-interface (interface () grow))
(define (picky-mixin %)
  (unless (implementation? % grower-interface)
    (error "picky-mixin: not a grower-interface class"))
  (class % ....))

带混合的接口的另一个使用是通过混合产生标签类,这样,混合的实例就可以被识别。也就是说,is-a?不能在一个表示为函数的混合上运行,但它可以识别一个接口(有点像一个特定的接口),它总是被混合所实现。例如,通过picky-mixin生成的类可以被picky-interface所标记,用is-picky?可以判断:

(define picky-interface (interface ()))
(define (picky-mixin %)
  (unless (implementation? % grower-interface)
    (error "picky-mixin: not a grower-interface class"))
  (class* % (picky-interface) ....))
(define (is-picky? o)
  (is-a? o picky-interface))

13.7.2 mixin表

为执行混合而编写lambda+class模式,包括对混合的定义域和值域接口的使用,类系统提供了一个mixin宏:

(mixin (interface-expr ...) (interface-expr ...)
  decl-or-expr ...)

interface-expr的第一个集合确定混合的定义域,第二个集合确定值域。就是说,扩展是一个函数,它测试是否一个给定的基类实现interface-expr的第一个序列,并产生一个类实现interface-expr的第二个序列。其它需求,比如基类继承方法存在,接着检查mixin表的class扩展。例如:

> (define choosy-interface (interface () choose?))
> (define hungry-interface (interface () eat))
> (define choosy-eater-mixin
    (mixin (choosy-interface) (hungry-interface)
      (inherit choose?)
      (super-new)
      (define/public (eat x)
        (cond
          [(choose? x)
           (printf "chomp chomp chomp on ~a.\n" x)]
          [else
           (printf "I'm not crazy about ~a.\n" x)]))))
> (define herring-lover%
    (class* object% (choosy-interface)
      (super-new)
      (define/public (choose? x)
        (regexp-match #px"^herring" x))))
> (define herring-eater% (choosy-eater-mixin herring-lover%))
> (define eater (new herring-eater%))
> (send eater eat "elderberry")

I'm not crazy about elderberry.

> (send eater eat "herring")

chomp chomp chomp on herring.

> (send eater eat "herring ice cream")

chomp chomp chomp on herring ice cream.

混合不仅重写方法,还引入公共方法,它们也可以扩展方法,引入扩展的方法,添加一个可重写的扩展,并添加一个可扩展的重写——所有这些类能完成的事情(参见《Final、Augment和Inner》部分)。

13.7.3 参数化的混合

正如在《控制外部名称的范围》中指出的,外部名称可以用define-member-name绑定。这个工具允许一个混合通过它所定义或使用的方法进行概括。例如,我们可以通过对eat的外部成员键的使用参数化hungry-mixin:

(define (make-hungry-mixin eat-method-key)
  (define-member-name eat eat-method-key)
  (mixin () () (super-new)
    (inherit eat)
    (define/public (eat-more x y) (eat x) (eat y))))

获得一个特定的hungry-mixin,我们必须应用这个函数给一个成员键,它指向一个适当的eat方法,我们可以用member-name-key获取:

((make-hungry-mixin (member-name-key eat))
 (class object% .... (define/public (eat x) 'yum)))

以上,我们应用hungry-mixin给一个匿名类,它提供eat,但我们也可以把它和一个提供chomp的类组合,而不是这样:

((make-hungry-mixin (member-name-key chomp))
 (class object% .... (define/public (chomp x) 'yum)))

13.8 特征

一个特征(trait)类似于一个混合(mixin),它封装了一组方法添加到一个类里。一个特征不同于一个混合,它自己的方法是可以用特征运算符操控的,比如trait-sum(合并这两个特征的方法)、trait-exclude(从一个特征中移除方法)以及trait-alias(添加一个带有新名字的方法的拷贝;它不重定向到对任何旧名字的调用)。

混合和特征之间的实际差别是两个特征可以组合,即使它们包括了共有的方法,而且即使两者的方法都可以合理地覆盖其它方法。在这种情况下,程序员必须明确地解决冲突,通常通过别名方法、排除方法、以及合并使用别名的新特性。

假设我们的fish%程序想要定义两个类扩展,spots和stripes,每个都包含get-color方法。fish的spot-color不应该重写stripe-color,反之亦然;相反,一个spots+stripes-fish%应结合两种颜色,如果spots和stripes是普通混合实现,那这是不可能的。然而,如果spots和stripes作为特征来实现,它们可以组合在一起。首先,我们在每个特征中给get-color起一个别名为一个不冲突的名称。第二,get-color方法从两者中移除,只有别名的特征被合并。最后,新特征用于创建一个类,它基于这两个别名引入自己的get-color方法,生成所需的spots+stripes扩展。

13.8.1 特征作为混合集

在Racket里实现特征的一个自然的方法是作为混合的一个集合,每个特征方法带一个混合。例如,我们可以尝试如下定义spots和stripes的特征,使用关联列表来表示集合:

(define spots-trait
  (list (cons 'get-color
               (lambda (%) (class % (super-new)
                             (define/public (get-color)
                               'black))))))
(define stripes-trait
  (list (cons 'get-color
              (lambda (%) (class % (super-new)
                            (define/public (get-color)
                              'red))))))

一个集合表现,如上面所述,允许trait-sum和trait-exclude做为简单操作;不幸的是,它不支持trait-alias运算符。虽然一个混合可以在关联表里复制,混合有一个固定的方法名称,例如,get-color,而且混合不支持方法重命名操作。支持trait-alias,我们必须在扩展方法名上参数化混合,同样地eat在参数化混合(参数化的混合)中进行参数化。

为了支持trait-alias操作,spots-trait应表示为:

(define spots-trait
  (list (cons (member-name-key get-color)
              (lambda (get-color-key %)
                (define-member-name get-color get-color-key)
                (class % (super-new)
                  (define/public (get-color) 'black))))))

当spots-trait中的get-color方法是让get-trait-color具有别名同时get-color方法被去除,由此产生的特性如下:

(list (cons (member-name-key get-trait-color)
            (lambda (get-color-key %)
              (define-member-name get-color get-color-key)
              (class % (super-new)
                (define/public (get-color) 'black)))))

我们用((trait->mixin T) C)给类C应用特征T并获得一个派生类。trait->mixin函数将用于混合的方法和部分C扩展的键提供给每个T的混合:

(define ((trait->mixin T) C)
  (foldr (lambda (m %) ((cdr m) (car m) %)) C T))

因此,当上述特性与其它特性结合并应用到类中时,get-color的使用将成为外部名称get-trait-color的引用。

13.8.2 特征里的继承与基类

特征的第一个实现支持trait-alias,它支持一个调用自身的特征方法,但是它不支持调用彼此的特征方法。特别是,假设spot-fish的市场价取决于它的斑点颜色的时候:

(define spots-trait
  (list (cons (member-name-key get-color) ....)
        (cons (member-name-key get-price)
              (lambda (get-price %) ....
                (class % ....
                  (define/public (get-price)
                    .... (get-color) ....))))))

在这种情况下,spots-trait的定义失败,因为get-color不在get-price混合范围之内。实际上,当特征应用于一个类时取决于混合程序的顺序,当get-price混合应用于类时,get-color方法可能不可获得。因此添加(inherit get-color)申明给get-price混合并未解决这个问题。

一种解决方案是要求在诸如get-price方法中使用(send this get-color)。这种更改是有效的,因为send总是延迟方法查找,直到对方法的调用被求值。然而,延迟查找比直接调用更为昂贵。更糟糕的是,它也延迟检查get-color方法是否存在。

第二种解决方案,实际上,并且有效的解决方案是改变特征编码。具体来说,我们把每个方法表示成一对混合:一个引入方法,另一个实现它。当一个特征应用于一个类,所有的引入方法混合首先被应用。然后实现方法混合可以使用inherit去直接访问任何引入的方法。

(define spots-trait
  (list (list (local-member-name-key get-color)
              (lambda (get-color get-price %) ....
                (class % ....
                  (define/public (get-color) (void))))
              (lambda (get-color get-price %) ....
                (class % ....
                  (define/override (get-color) 'black))))
        (list (local-member-name-key get-price)
              (lambda (get-price get-color %) ....
                (class % ....
                  (define/public (get-price) (void))))
              (lambda (get-color get-price %) ....
                (class % ....
                  (inherit get-color)
                  (define/override (get-price)
                    .... (get-color) ....))))))

有了这个特性编码, trait-alias添加一个带新名字的新方法,但它不会改变对旧方法的任何引用。

13.8.3 trait表

通用特征模式显然对程序员直接使用来说太复杂了,但很容易在trait宏中编译:

(trait trait-clause ...)

The ids in the optional inherit clause are available for direct reference in the method exprs, and they must be supplied either by other traits or the base class to which the trait is ultimately applied. 在可选的inherit从句中id对expr方法中的直接引用是有效的,并且它们必须被提供给其特征被最终应用的那一个,既被其它特征提供也被基类提供。

将此表与特征操作符结合使用,如trait-sum、trait-exclude、trait-alias和trait->mixin,我们能够根据需要实现spots-trait和stripes-trait。

(define spots-trait
  (trait
    (define/public (get-color) 'black)
    (define/public (get-price) ... (get-color) ...)))
 
(define stripes-trait
  (trait
    (define/public (get-color) 'red)))
 
(define spots+stripes-trait
  (trait-sum
   (trait-exclude (trait-alias spots-trait
                               get-color get-spots-color)
                  get-color)
   (trait-exclude (trait-alias stripes-trait
                               get-color get-stripes-color)
                  get-color)
   (trait
     (inherit get-spots-color get-stripes-color)
     (define/public (get-color)
       .... (get-spots-color) .... (get-stripes-color) ....))))

13.9 类合约

由于类是值,它们可以跨越合约边界,而且我们也可能想用合约保护给定类的一部分。要实现这个,使用class/c表。class/c表具有许多子表,它描述字段和方法两种类型的合约:有些通过实例化对象影响使用,有些影响子类。

13.9.1 外部类合约

在最简单的表中,class/c保护从合约类实例化的对象的公共字段和方法。还有一种object/c表可用于对特定对象的公共字段和方法的同样保护。获取animal%的以下定义,它使用公共字段作为其size属性:

(define animal%
  (class object%
    (super-new)
    (field [size 10])
    (define/public (eat food)
      (set! size (+ size (get-field size food))))))

对于任何实例化的animal%,访问size字段应该返回一个正数。另外,如果设置了size字段,则应该是一个正数被赋值。最后,eat方法应该接收一个参数,它是带一个正数size字段的对象。为了确保这些条件,我们将用一个适当的合约定义animal%类:

(define positive/c (and/c number? positive?))
(define edible/c (object/c (field [size positive/c])))
(define/contract animal%
  (class/c (field [size positive/c])
           [eat (->m edible/c void?)])
  (class object%
    (super-new)
    (field [size 10])
    (define/public (eat food)
      (set! size (+ size (get-field size food))))))

这里我们使用->m来描述eat的行为,因为我们不需要描述这个this参数的任何要求。既然我们有我们的合约类,就可以看出对size和eat的合约都是强制执行的:

> (define bob (new animal%))
> (set-field! size bob 3)
> (get-field size bob)

3

> (set-field! size bob 'large)

animal%: contract violation

  expected: positive/c

  given: 'large

  in: the size field in

      (class/c

       (eat

        (->m

         (object/c (field (size positive/c)))

         void?))

       (field (size positive/c)))

  contract from: (definition animal%)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:31:0

> (define richie (new animal%))
> (send bob eat richie)
> (get-field size bob)

13

> (define rock (new object%))
> (send bob eat rock)

eat: contract violation;

 no public field size

  in: the 1st argument of

      the eat method in

      (class/c

       (eat

        (->m

         (object/c (field (size positive/c)))

         void?))

       (field (size positive/c)))

  contract from: (definition animal%)

  contract on: animal%

  blaming: top-level

   (assuming the contract is correct)

  at: eval:31:0

> (define giant (new (class object% (super-new) (field [size 'large]))))
> (send bob eat giant)

eat: contract violation

  expected: positive/c

  given: 'large

  in: the size field in

      the 1st argument of

      the eat method in

      (class/c

       (eat

        (->m

         (object/c (field (size positive/c)))

         void?))

       (field (size positive/c)))

  contract from: (definition animal%)

  contract on: animal%

  blaming: top-level

   (assuming the contract is correct)

  at: eval:31:0

对于外部类合约有两个重要的警告。首先,当动态分派的目标是合约类的方法实施时,只有在合约边界内才实施外部方法合约。重写该实现,从而改变动态分派的目标,将意味着不再为被保护者强制执行该合约,因为访问该方法不再越过合约边界。与外部方法合约不同,外部字段合约对于子类的被保护者总是强制执行,因为字段不能被覆盖或屏蔽。

其次,这些合约不以任何方式限制animal%的子类。被子类继承和使用的字段和方法不被这些合约检查,并且通过super对基类方法的使用也不检查。下面的示例说明了这两个警告:

(define large-animal%
  (class animal%
    (super-new)
    (inherit-field size)
    (set! size 'large)
    (define/override (eat food)
      (display "Nom nom nom") (newline))))
 
> (define elephant (new large-animal%))
> (send elephant eat (new object%))

Nom nom nom

> (get-field size elephant)

animal%: broke its own contract

  promised: positive/c

  produced: 'large

  in: the size field in

      (class/c

       (eat

        (->m

         (object/c (field (size positive/c)))

         void?))

       (field (size positive/c)))

  contract from: (definition animal%)

  blaming: (definition animal%)

   (assuming the contract is correct)

  at: eval:31:0

13.9.2 内部类合约

注意,从elephant对象检索size字段归咎于animal%违反合约。这种归咎是正确的,但对animal%类来说是不公平的,因为我们还没有提供一种保护自己免受子类攻击的方法。为此我们添加内部类合约,它提供指令给子类以指明它们如何访问和重写基类的特征。外部类和内部类合约之间的区别在于是否允许类层次结构中较弱的合约,其不变性可能被子类内部破坏,但应通过实例化的对象强制用于外部使用。

作为可用的保护种类的简单示例,我们提供了一个针对animal%类的示例,它使用所有合适的表:

(class/c (field [size positive/c])
         (inherit-field [size positive/c])
         [eat (->m edible/c void?)]
         (inherit [eat (->m edible/c void?)])
         (super [eat (->m edible/c void?)])
         (override [eat (->m edible/c void?)]))

这个类合约不仅确保animal%类的对象像以前一样受到保护,而且确保animal%类的子类只在size字段中存储适当的值,并适当地使用animal%的size实现。这些合约表只影响类层次结构中的使用,并且只影响跨合约边界的方法调用。

这意味着,inherit只会影响到一个方法的子类使用直到子类重写方法,而override只影响从基类进入子类的方法的重写实现。由于这些仅影响内部使用,所以在使用这些类的对象时,override表不会自动将子类插入到合约中。此外,使用override仅是说得通,因此只能用于没有Beta风格增强的方法。下面的示例显示了这种差异:

(define/contract sloppy-eater%
  (class/c [eat (->m edible/c edible/c)])
  (begin
    (define/contract glutton%
      (class/c (override [eat (->m edible/c void?)]))
      (class animal%
        (super-new)
        (inherit eat)
        (define/public (gulp food-list)
          (for ([f food-list])
            (eat f)))))
    (class glutton%
      (super-new)
      (inherit-field size)
      (define/override (eat f)
        (let ([food-size (get-field size f)])
          (set! size (/ food-size 2))
          (set-field! size f (/ food-size 2))
          f)))))
> (define pig (new sloppy-eater%))
> (define slop1 (new animal%))
> (define slop2 (new animal%))
> (define slop3 (new animal%))
> (send pig eat slop1)

(object:animal% ...)

> (get-field size slop1)

5

> (send pig gulp (list slop1 slop2 slop3))

eat: contract violation

  expected: void?

  given: (object:animal% ...)

  in: the range of

      the eat method in

      (class/c

       (override (eat

                  (->m

                   (object/c

                    (field (size positive/c)))

                   void?))))

  contract from: (definition glutton%)

  contract on: glutton%

  blaming: (definition sloppy-eater%)

   (assuming the contract is correct)

  at: eval:47:0

除了这里的内部类合约表所显示的之外,这里有Beta风格的可扩展方法类似的表。inner表描述了这个子类,它被要求从一个给定的方法扩展。augment和augride告诉子类,该给定的方法是一种被增强的方法,并且对子类方法的任何调用将动态分配到基类中相应的实现。这样的调用将根据给定的合约进行检查。这两种表的区别在于augment的使用意味着子类可以增强给定的方法,而augride的使用表示子类必须重写当前增强。

这意味着并不是所有的表都可以同时使用。只有override、augment和augride中的表可用于给定的方法,并且如果给定的方法已经完成,这些表没有一个可以使用。此外, 仅在augride或override可以被指定时,super才可以被指定给一个给定的方法。同样,只有augment或augride可以被指定时,inner才可以被指定。

 

14 单元(组件)

单元(unit)把程序组织成可独立编译和可重用的组件(component)。一个单元类似于过程,因为这两个都是用于抽象的一级值。过程对表达式中的值进行抽象,而单元对定义集合中的名称进行抽象。正如一个过程被调用以对它的表达式求值,其表达式把实际的参数作为给它的正式参数,一个单元被调用(invoke)来对它的定义求值,这个定义给出其导入变量的实际引用。但是,与过程不同的是,在调用之前,一个单元的导入变量可以部分地与另一个之前调用(prior to invocation)单元的导出变量链接。链接将多个单元合并成单个复合单元。复合单元本身导入将传播到链接单元中未解决的导入变量的变量,并从链接单元中重新导出一些变量以进一步链接。

 

14.1 签名和单元

单元的接口用签名(signature)来描述。每个签名都使用define-signature来定义(通常在module(模块)中)。例如,下面的签名,放在一个"toy-factory-sig.rkt"的文件中,描述了一个组件的导出(export),它实现了一个玩具厂(toy factory):

按照惯例,签名名称用^结束。

"toy-factory-sig.rkt"

#lang racket
 
(define-signature toy-factory^
  (build-toys  ; (integer? -> (listof toy?))
   repaint     ; (toy? symbol? -> toy?)
   toy?        ; (any/c -> boolean?)
   toy-color)) ; (toy? -> symbol?)
 
(provide toy-factory^)

一个toy-factory^签名的实现是用define-unit来写的,它定义了一个名为toy-factory^的export(导出)从句:

按照惯例,单位名称用@结束。

"simple-factory-unit.rkt"

#lang racket
 
(require "toy-factory-sig.rkt")
 
(define-unit simple-factory@
  (import)
  (export toy-factory^)
 
  (printf "Factory started.\n")
 
  (define-struct toy (color) #:transparent)
 
  (define (build-toys n)
    (for/list ([i (in-range n)])
      (make-toy 'blue)))
 
  (define (repaint t col)
    (make-toy col)))
 
(provide simple-factory@)

toy-factory^签名也可以被一个单元引用,它需要一个玩具工厂来实施其它某些东西。在这种情况下,toy-factory^将以一个import(导入)从句命名。例如,玩具店可以从玩具厂买到玩具。(假设为了一个有趣的例子,商店只愿意出售特定颜色的玩具)。

"toy-store-sig.rkt"

#lang racket
 
(define-signature toy-store^
  (store-color     ; (-> symbol?)
   stock!          ; (integer? -> void?)
   get-inventory)) ; (-> (listof toy?))
 
(provide toy-store^)

"toy-store-unit.rkt"

#lang racket
 
(require "toy-store-sig.rkt"
         "toy-factory-sig.rkt")
 
(define-unit toy-store@
  (import toy-factory^)
  (export toy-store^)
 
  (define inventory null)
 
  (define (store-color) 'green)
 
  (define (maybe-repaint t)
    (if (eq? (toy-color t) (store-color))
        t
        (repaint t (store-color))))
 
  (define (stock! n)
    (set! inventory
          (append inventory
                  (map maybe-repaint
                       (build-toys n)))))
 
  (define (get-inventory) inventory))
 
(provide toy-store@)

请注意,"toy-store-unit.rkt"导入"toy-factory-sig.rkt",而不是"simple-factory-unit.rkt"。因此,toy-store@单元只依赖于玩具工厂的规格,而不是具体的实施。

 

14.2 调用单元

simple-factory@单元没有导入,因此可以使用invoke-unit直接调用它:

> (require "simple-factory-unit.rkt")
> (invoke-unit simple-factory@)

Factory started.

但是,invoke-unit表并不能使主体定义可用,因此我们不能在这家工厂制造任何玩具。define-values/invoke-unit表将签名的标识绑定到实现签名的一个单元(被调用的)提供的值:

> (define-values/invoke-unit/infer simple-factory@)

Factory started.

> (build-toys 3)

(list (toy 'blue) (toy 'blue) (toy 'blue))

由于simple-factory@导出toy-factory^签名,toy-factory^的每个标识都是由define-values/invoke-unit/infer表定义的。表名称的/infer部分表明,由声明约束的标识是从simple-factory@推断出来的。

在定义toy-factory^的标识后,我们还可以调用toy-store@,它导入toy-factory^以产生toy-store^:

> (require "toy-store-unit.rkt")
> (define-values/invoke-unit/infer toy-store@)
> (get-inventory)

'()

> (stock! 2)
> (get-inventory)

(list (toy 'green) (toy 'green))

同样,/infer部分define-values/invoke-unit/infer确定toy-store@导入toy-factory^,因此它提供与toy-factory^中的名称匹配的顶级绑定,如导入toy-store@。

 

14.3 链接单元

我们可以借助玩具工厂的合作使我们的玩具店玩具经济性更有效,不需要重新创建。相反,玩具总是使用商店的颜色来制造,而工厂的颜色是通过导入toy-store^来获得的:

"store-specific-factory-unit.rkt"

#lang racket
 
(require "toy-store-sig.rkt"
         "toy-factory-sig.rkt")
 
(define-unit store-specific-factory@
  (import toy-store^)
  (export toy-factory^)
 
  (define-struct toy () #:transparent)
 
  (define (toy-color t) (store-color))
 
  (define (build-toys n)
    (for/list ([i (in-range n)])
      (make-toy)))
 
  (define (repaint t col)
    (error "cannot repaint")))
 
(provide store-specific-factory@)

要调用store-specific-factory@,我们需要toy-store^绑定供及给单元。但是为了通过调用toy-store^来获得toy-store^的绑定,我们需要一个玩具工厂!单元实现是相互依赖的,我们不能在另一个之前调用那一个。

解决方案是将这些单元链接(link)在一起,然后调用组合单元。define-compound-unit/infer表将任意数量的单元链接成一个组合单元。它可以从相连的单元中进行导入和导出,并利用其它链接单元的导出来满足各单元的导入。

> (require "toy-factory-sig.rkt")
> (require "toy-store-sig.rkt")
> (require "store-specific-factory-unit.rkt")
> (define-compound-unit/infer toy-store+factory@
    (import)
    (export toy-factory^ toy-store^)
    (link store-specific-factory@
          toy-store@))

上边总的结果是一个单元toy-store+factory@,其导出既是toy-factory^也是toy-store^。从每个导入和导出的签名中推断出store-specific-factory@和toy-store@之间的联系。

这个单元没有导入,所以我们可以随时调用它:

> (define-values/invoke-unit/infer toy-store+factory@)
> (stock! 2)
> (get-inventory)

(list (toy) (toy))

> (map toy-color (get-inventory))

'(green green)

 

14.4 一级单元

define-unit表将defineunit表相结合,类似于(define (f x) ....)结合define,后跟带一个隐式的lambda的标识。

扩大简写,toy-store@的定义几乎可以写成

(define toy-store@
  (unit
   (import toy-factory^)
   (export toy-store^)
 
   (define inventory null)
 
   (define (store-color) 'green)
   ....))

这个扩展和define-unit的区别在于,toy-store@的导入和导出不能被推断出来。也就是说,除了将defineunit结合在一起,define-unit还将静态信息附加到定义的标识,以便静态地提供它的签名信息来define-values/invoke-unit/infer和其它表。

虽有丢失静态签名信息的缺点,unit可以与使用第一类值的其它表结合使用。例如,我们可以封装一个unit,它在一个 lambda中创建一个玩具商店来提供商店的颜色:

"toy-store-maker.rkt"

#lang racket
 
(require "toy-store-sig.rkt"
         "toy-factory-sig.rkt")
 
(define toy-store@-maker
  (lambda (the-color)
    (unit
     (import toy-factory^)
     (export toy-store^)
 
     (define inventory null)
 
     (define (store-color) the-color)
 
     ; 其余的和前面一样
 
     (define (maybe-repaint t)
       (if (eq? (toy-color t) (store-color))
           t
           (repaint t (store-color))))
 
     (define (stock! n)
       (set! inventory
             (append inventory
                     (map maybe-repaint
                          (build-toys n)))))
 
     (define (get-inventory) inventory))))
 
(provide toy-store@-maker)

要调用由toy-store@-maker创建的单元,我们必须使用define-values/invoke-unit,而不是/infer变量:

> (require "simple-factory-unit.rkt")
> (define-values/invoke-unit/infer simple-factory@)

Factory started.

> (require "toy-store-maker.rkt")
> (define-values/invoke-unit (toy-store@-maker 'purple)
    (import toy-factory^)
    (export toy-store^))
> (stock! 2)
> (get-inventory)

(list (toy 'purple) (toy 'purple))

define-values/invoke-unit表中,(import toy-factory^)行从当前的上下文中获取与toy-factory^中的名称匹配的绑定(我们通过调用simple-factory@)创建的名称),并将它们提供于导入toy-store@。(export toy-store^)从句表明toy-store@-maker产生的单元将导出toy-store^,并在调用该单元后定义该签名的名称。

为了把一个单元与toy-store@-maker链接起来,我们可以使用compound-unit表:

> (require "store-specific-factory-unit.rkt")
> (define toy-store+factory@
    (compound-unit
     (import)
     (export TF TS)
     (link [((TF : toy-factory^)) store-specific-factory@ TS]
           [((TS : toy-store^)) toy-store@ TF])))

这个compound-unit表将许多信息聚集到一个地方。link从句中的左侧TF和TS是绑定标识。标识TF基本上绑定到toy-factory^的元素作为由store-specific-factory@的实现。标识TS类似地绑定到toy-store^的元素作为由toy-store@的实现。同时,绑定到TS的元素作为提供给store-specific-factory@的导入,因为TS是随着store-specific-factory@的。绑定到TF的元素也同样提供给toy-store^。最后,(export TF TS)表明绑定到TF和TS的元素从复合单元导出。

上面的compound-unit表使用store-specific-factory@作为一个一级单元,尽管它的信息可以推断。除了在推理上下文中的使用外,每个单元都可以用作一个一级单元。此外,各种表让程序员弥合了推断的和一级的世界之间的间隔。例如,define-unit-binding将一个新的标识绑定到由任意表达式生成的单元;它静态地将签名信息与标识相关联,并动态地对表达式产生的一级单元进行签名检查。

 

14.5 完整的module签名和单元

在程序中使用的单元,模块如"toy-factory-sig.rkt"和"simple-factory-unit.rkt"是常见的。racket/signature和racket/unit模块的名称可以作为语言来避免大量的样板模块、签名和单元申明文本。

例如,"toy-factory-sig.rkt"可以写为

#lang racket/signature
 
build-toys  ; (integer? -> (listof toy?))
repaint     ; (toy? symbol? -> toy?)
toy?        ; (any/c -> boolean?)
toy-color   ; (toy? -> symbol?)

签名toy-factory^是自动从模块中提供的,它通过用^从文件名"toy-factory-sig.rkt"置换"-sig.rkt"后缀来推断。

同样,"simple-factory-unit.rkt"模块可以写为

#lang racket/unit
 
(require "toy-factory-sig.rkt")
 
(import)
(export toy-factory^)
 
(printf "Factory started.\n")
 
(define-struct toy (color) #:transparent)
 
(define (build-toys n)
  (for/list ([i (in-range n)])
    (make-toy 'blue)))
 
(define (repaint t col)
  (make-toy col))

单元simple-factory@是自动从模块中提供,它通过用@从文件名"simple-factory-unit.rkt"置换"-unit.rkt"后缀来推断。

 

14.6 单元合约

有两种用合约保护单元的方法。一种方法在编写新的签名时有用,另一种是当一个单元必须符合已经存在的签名时才可以处理这种情况。

14.6.1 给签名添加合约

当合约添加到签名时,实现该签名的所有单元都受到这些合约的保护。toy-factory^签名的以下版本添加了前面说明中写过的合约:

"contracted-toy-factory-sig.rkt"

#lang racket
 
(define-signature contracted-toy-factory^
  ((contracted
    [build-toys (-> integer? (listof toy?))]
    [repaint    (-> toy? symbol? toy?)]
    [toy?       (-> any/c boolean?)]
    [toy-color  (-> toy? symbol?)])))
 
(provide contracted-toy-factory^)

Now we take the previous implementation of simple-factory@ and implement this version of toy-factory^ instead: 现在我们采用以前实现的simple-factory@,并实现toy-factory^的这个版本:

"contracted-simple-factory-unit.rkt"

#lang racket
 
(require "contracted-toy-factory-sig.rkt")
 
(define-unit contracted-simple-factory@
  (import)
  (export contracted-toy-factory^)
 
  (printf "Factory started.\n")
 
  (define-struct toy (color) #:transparent)
 
  (define (build-toys n)
    (for/list ([i (in-range n)])
      (make-toy 'blue)))
 
  (define (repaint t col)
    (make-toy col)))
 
(provide contracted-simple-factory@)

和以前一样,我们可以调用我们新的单元并绑定导出,这样我们就可以使用它们。然而这次,滥用导出引起了相应的合约错误。

> (require "contracted-simple-factory-unit.rkt")
> (define-values/invoke-unit/infer contracted-simple-factory@)

Factory started.

> (build-toys 3)

(list (toy 'blue) (toy 'blue) (toy 'blue))

> (build-toys #f)

build-toys: contract violation

  expected: integer?

  given: #f

  in: the 1st argument of

      (-> integer? (listof toy?))

  contract from:

      (unit contracted-simple-factory@)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:34:0

> (repaint 3 'blue)

repaint: contract violation

  expected: toy?

  given: 3

  in: the 1st argument of

      (-> toy? symbol? toy?)

  contract from:

      (unit contracted-simple-factory@)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:34:0

14.6.2 给单元添加合约

不过,有时我们可能有一个单元,它必须符合一个已经存在的签名而不是符合合约。在这种情况下,我们可以创建一个带unit/c或使用define-unit/contract表的单元合约,它定义了一个已被单元合约包装的单元。

例如,这里有一个toy-factory@的版本,它仍然实现了规则toy-factory^,但它的导出得到了适当的合约保护。

"wrapped-simple-factory-unit.rkt"

#lang racket
 
(require "toy-factory-sig.rkt")
 
(define-unit/contract wrapped-simple-factory@
  (import)
  (export (toy-factory^
           [build-toys (-> integer? (listof toy?))]
           [repaint    (-> toy? symbol? toy?)]
           [toy?       (-> any/c boolean?)]
           [toy-color  (-> toy? symbol?)]))
 
  (printf "Factory started.\n")
 
  (define-struct toy (color) #:transparent)
 
  (define (build-toys n)
    (for/list ([i (in-range n)])
      (make-toy 'blue)))
 
  (define (repaint t col)
    (make-toy col)))
 
(provide wrapped-simple-factory@)
> (require "wrapped-simple-factory-unit.rkt")
> (define-values/invoke-unit/infer wrapped-simple-factory@)

Factory started.

> (build-toys 3)

(list (toy 'blue) (toy 'blue) (toy 'blue))

> (build-toys #f)

wrapped-simple-factory@: contract violation

  expected: integer?

  given: #f

  in: the 1st argument of

      (unit/c

       (import)

       (export (toy-factory^

                (build-toys

                 (-> integer? (listof toy?)))

                (repaint (-> toy? symbol? toy?))

                (toy? (-> any/c boolean?))

                (toy-color (-> toy? symbol?))))

       (init-depend))

  contract from:

      (unit wrapped-simple-factory@)

  blaming: top-level

   (assuming the contract is correct)

  at: <collects>/racket/unit.rkt

> (repaint 3 'blue)

wrapped-simple-factory@: contract violation

  expected: toy?

  given: 3

  in: the 1st argument of

      (unit/c

       (import)

       (export (toy-factory^

                (build-toys

                 (-> integer? (listof toy?)))

                (repaint (-> toy? symbol? toy?))

                (toy? (-> any/c boolean?))

                (toy-color (-> toy? symbol?))))

       (init-depend))

  contract from:

      (unit wrapped-simple-factory@)

  blaming: top-level

   (assuming the contract is correct)

  at: <collects>/racket/unit.rkt

 

14.7 unitmodule的比较

作为模块化的表,unit(单元)是对module(模块)的补充:

  • module表主要用于管理通用命名空间。例如,它允许一个代码片段是专指来自racket/base的car运算——其中一个提取内置序对数据类型的一个实例的第一个元素——而不是任何其它带有car名字的函数。也就是说,module构造允许你引用你想要的这个绑定。

  • unit表是参数化的带相对于大多数运行时值的任意种类的代码片段。例如,它允许一个代码片段与一个接受单个参数的car函数一起工作,其中特定函数在稍后通过将片段连接到另一个参数被确定。也就是说,unit结构允许你引用满足某些规范的一个绑定。

除了别的,lambdaclass表还允许对稍后选择的值进行代码参数化。原则上,其中任何一项都可以以其他任何方式执行。在实践中,每个表都提供了某些便利——例如允许重写方法或者特别是对值的特别简单的应用——使它们适合不同的目的。

从某种意义上说,module表比其它表更为基础。毕竟,没有module提供的命名空间管理,程序片段不能可靠地引用lambdaclassunit表。同时,由于名称空间管理与单独的扩展和编译密切相关,module边界以独立的编译边界结束,在某种程度上阻止了片段之间的相互依赖关系。出于类似的原因,module不将接口与实现分开。

module本身几乎可以运行时,然而独立编译的部分必须相互引用时,或当你想要在接口(interface)(即,需要在扩展和编译时间被知道的部分)和实现(implementation)(即,运行时部分)之间有一个更强健的隔离时,使用unit。更普遍使用unit的情况是,当你需要在函数、数据类型和类上参数化代码时,以及当参数代码本身提供定义以和其它参数代码链接时。

 

15 反射和动态求值

Racket是一个动态(dynamic)的语言。它提供了许多用于加载、编译、甚至在运行时构造新代码的工具。

 

15.1 eval

这个例子在一个模块内或在DrRacket的定义窗口内将不会工作,但它会工作在交互窗口中,原因在《名称空间》的末尾讲解。

eval函数构成一个表达式或一个定义(如“引用(quoted)”表或语法对象(syntax object))的描述并且对它进行求值:

> (eval '(+ 1 2))

3

eval函数的强大在于可以动态构造一个表达式:

> (define (eval-formula formula)
    (eval `(let ([x 2]
                 [y 3])
             ,formula)))
> (eval-formula '(+ x y))

5

> (eval-formula '(+ (* x y) y))

9

当然,如果我们只是想用给出x和y的值求值表达式,我们不需要eval。更直接的方法是使用一级函数:

> (define (apply-formula formula-proc)
    (formula-proc 2 3))
> (apply-formula (lambda (x y) (+ x y)))

5

> (apply-formula (lambda (x y) (+ (* x y) y)))

9

然而,如果像(+ x y)和(+ (* x y) y)这样的表达式是从用户提供的文件中读取,那么eval可能是适当的。同样,REPL读取由用户输入的表达式,并使用eval求值。

一样地,在整个模块中eval经常直接或间接地被使用。例如,程序可以在定义域中用dynamic-require读取一个模块,这本质上是一个封装在eval中的动态加载模块的代码。

15.1.1 本地域

eval函数不能看到上下文中被调用的局部绑定。例如,调用在一个非引用的let表中的eval以对一个公式求值不会使得值x和y可见:

> (define (broken-eval-formula formula)
    (let ([x 2]
          [y 3])
      (eval formula)))
> (broken-eval-formula '(+ x y))

x: undefined;

 cannot reference an identifier before its definition

  in module: top-level

eval函数不能看到x和y的绑定,正是因为它是一个函数,并且Racket是词法作用域的语言。想象一下如果eval被实现为

(define (eval x)
  (eval-expanded (macro-expand x)))

那么在eval-expanded被调用的这个点上,x最近的绑定是表达式求值,不是broken-eval-formula中的let绑定。词法范围防止这样的困惑和脆弱的行为,从而防止eval表看到上下文中被调用的局部绑定。

你可以想象,即使通过eval不能看到broken-eval-formula中的局部绑定,这里实际上必须是一个x到2和y到3的数据结构映射,以及你想办法得到那些数据结构。事实上,没有这样的数据结构存在;编译器可以自由地在编译时替换带有2的x的每一个使用,因此在运行时的任何具体意义上都不存在x的局部绑定。即使变量不能通过常量折叠消除,通常也可以消除变量的名称,而保存局部值的数据结构与从名称到值的映射不一样。

15.1.2 名称空间

由于eval不能从它调用的上下文中看到绑定,另一种机制是需要确定动态可获得的绑定。一个名称空间(namespace)是一个一级的值,它封装了用于动态求值的可获得绑定。

通俗地说,术语名称空间(namespace)有时交替使用环境(environment)范围(scope)。在Racket里,术语名称空间(namespace)有更具体的、动态的意义,并且它不应该和静态的词汇概念混淆。

某些函数,如eval,接受一个可选的名称空间参数。通常,动态操作所使用的名称空间是current-namespace参数所确定的当前名称空间(current namespace)

evalREPL中使用时,当前名称空间是REPL使用于求值表达式中的一个。这就是为什么下面的互动设计成功通过eval访问x的原因:

> (define x 3)
> (eval 'x)

3

相反,尝试以下简单的模块并直接在DrRacket里或提供文件作为命令行参数给racket运行它:

#lang racket
 
(eval '(cons 1 2))

这个失败是因为初始的当前名称空间是空的。当你在交互模式下运行racket(见《交互模式》)时,初始的名称空间是用racket模块的导出初始化的,但是当你直接运行一个模块时,初始的名称空间开始为空。

在一般情况下,通过任何名称空间的安装来使用eval一个坏主意。相反,应明确地为调用而创建一个名称空间并安装它来求值:

#lang racket
 
(define ns (make-base-namespace))
(eval '(cons 1 2) ns) ; works

make-base-namespace函数创建一个名称空间,它是用racket/base的导出来初始化的。下一章节《操纵名称空间》提供了关于创建和配置名称空间的更多信息。

15.1.3 名称空间和模块

如同let绑定,词法范围意味着eval不能自动看到一个调用它的module(模块)的定义。然而,和let绑定不同的是,Racket提供了一种将模块反射到一个名称空间(namespace)的途径。

module->namespace函数接受一个引用的模块路径(module path),并生成一个名称空间,用于对表达式和定义求值,就像它们出现在module主体中一样:

> (module m racket/base
    (define x 11))
> (require 'm)
> (define ns (module->namespace ''m))
> (eval 'x ns)

11

''m中的双引号是因为'm是引用一个交互声明模块的模块路径,所以''m是路径的引用表。

module->namespace函数对来自于模块之外的模块是最有用的,在这里模块的全名是已知的。然而,在module表内,模块的全名可能并不知道,因为它可能取决于在最终加载时模块来源位于何处。

module内,使用define-namespace-anchor声明模块上的反射钩子,并使用namespace-anchor->namespace在模块的名称空间中滚动:

#lang racket
 
(define-namespace-anchor a)
(define ns (namespace-anchor->namespace a))
 
(define x 1)
(define y 2)
 
(eval '(cons x y) ns) ; produces (1 . 2)

 

15.2 操纵名称空间

一个名称空间(namespace)封装两条信息:

  • 从标识到绑定的一个映射。例如,一个名称空间可以将标识lambda映射到lambda表。一个“空”的名称空间是一个映射之一,它映射每个标识到一个未初始化的顶层变量。

  • 从模块名称到模块声明和实例的一个映射。

第一个映射是用于给一个顶层上下文中的表达式求值,如(eval '(lambda (x) (+ x 1)))中的。第二个映射是用于定位模块,例如通过dynamic-require。对(eval '(require racket/base))的调用通常使用两部分:标识映射确定require的绑定;如果它原来的意思是require,那么模块映射用于定位racket/base模块。

从核心Racket运行系统的角度来看,所有求值都是反射性的。执行从初始的名称空间包含一些原始的模块,并进一步由命令行上或在REPL提供指定加载的文件和模块。顶层require表和define表调整标识映射,模块声明(通常根据require表加载)调整模块映射。

15.2.1 创建和安装名称空间

函数make-empty-namespace创建一个新的空名称空间。由于名称空间确实是空的,所以它不能首先用来对任何顶级表达式求值——甚至不能对(require racket)求值。特别地,

(parameterize ([current-namespace (make-empty-namespace)])
  (namespace-require 'racket))

失败,因为名称空间不包括建立racket的原始模块。

为了使名称空间有用,必须从现有名称空间中附加一些模块。附加模块通过来自现有的名称空间映射的传递性复制条目(模块及它的所有导入)来调整模块名称的映射到实例。通常情况下,作为附加原始模块的替代——其名称和组织有可能会变化——附加一个高级模块,如racket或racket/base。

make-base-empty-namespace函数提供一个空的名称空间,除非附加了racket/base。从名称空间的绑定的标识部分没有映射的意义讲,生成的名称空间仍然是“空的”;只有模块映射已经填充。然而,通过初始的模块映射,可以加载更多模块。

一个用make-base-empty-namespace创建的名称空间适合于许多基本的动态任务。例如,假设my-dsl库实现了特定定义域的语言,你希望在其中执行来自用户指定文件的命令。一个用make-base-empty-namespace的名称空间足以启动:

(define (run-dsl file)
  (parameterize ([current-namespace (make-base-empty-namespace)])
    (namespace-require 'my-dsl)
    (load file)))

注意,current-namespaceparameterize不影响像在parameterize主体中的namespace-require那样的标识的含义。这些标识从封闭上下文(可能是一个模块)获得它们的含义。只有对代码具有动态性的表达式,如load的文件的内容,通过parameterize影响。

在上面的例子中,微妙的一点是使用(namespace-require 'my-dsl)代替(eval '(require my-dsl))。后者不会运行,因为eval需要对在名称空间中的require获得含义,并且名称空间的标识映射最初是空的。与此相反,namespace-require函数直接将给定的模块导入当前名称空间。从(namespace-require 'racket/base)运行。从(namespace-require 'racket/base)将为require引入绑定并使后续的(eval '(require my-dsl))运行。上面的更好,不仅仅是因为它更紧凑,还因为它避免了引入不属于特定领域语言的绑定。

15.2.2 跨名称空间共享数据和代码

如果模块不需要附加新的名称空间,则将重新加载并实例化它们。例如,racket/base不包括racket/class,加载racket/class又将创造一个不同的类数据类型:

> (require racket/class)
> (class? object%)

#t

> (class?
   (parameterize ([current-namespace (make-base-empty-namespace)])
     (namespace-require 'racket/class) ; loads again
     (eval 'object%)))

#f

对于动态加载的代码需要与其上下文共享更多代码和数据的情况,使用namespace-attach-module函数。 传递给namespace-attach-module的第一个参数是一个从中描绘模块实例的源名称空间;在某些情况下,当前名称空间对于包含需要共享的模块来说是已知的:

> (require racket/class)
> (class?
   (let ([ns (make-base-empty-namespace)])
     (namespace-attach-module (current-namespace)
                              'racket/class
                              ns)
     (parameterize ([current-namespace ns])
       (namespace-require 'racket/class) ; uses attached
       (eval 'object%))))

#t

然而,在一个模块中,define-namespace-anchornamespace-anchor->empty-namespace的组合提供了一种更可靠的获取源名称空间的方法:

#lang racket/base
 
(require racket/class)
 
(define-namespace-anchor a)
 
(define (load-plug-in file)
  (let ([ns (make-base-empty-namespace)])
    (namespace-attach-module (namespace-anchor->empty-namespace a)
                             'racket/class
                              ns)
    (parameterize ([current-namespace ns])
      (dynamic-require file 'plug-in%))))

namespace-attach-module绑定的锚将模块的运行时间与加载模块的名称空间(可能与当前名称空间不同)连接在一起。在上面的示例中,由于封闭模块需要racket/class,由namespace-anchor->empty-namespace生成的名称空间肯定包含了一个racket/class的实例。此外,该实例与导入模块的一个相同,从而类数据类型共享。

 

15.3 脚本求值和使用load

从历史上看,Lisp实现没有提供模块系统。相反,大的程序是由基本的脚本REPL来求值一个特定顺序的程序片段。虽然事实证明REPL脚本是构建程序和库的糟糕方法,但有时它仍然是一个有用的功能。

通过load用宏定义语言扩展[Flatt02]来描述程序交互性特别差。

load函数通过从文件中一个接一个地read(读取)S表达式并把它们传递给eval来运行一个REPL脚本。如果一个文件"place.rkts"包含以下内容

(define city "Salt Lake City")
(define state "Utah")
(printf "~a, ~a\n" city state)

那么,它可以加载进REPL

> (load "place.rkts")

Salt Lake City, Utah

> city

"Salt Lake City"

然而,由于load使用eval,像下面的模块一般不会运行——基于在《名称空间》中描述的相同原因:

#lang racket
 
(define there "Utopia")
 
(load "here.rkts")

对求值"here.rkts"的上下文的当前名称空间可能是空的;在任何情况下,你不能从"here.rkts"取得there。同样,在"here.rkts"里的任何定义对模块里的使用不会变得可见;毕竟,load是动态发生的,而在模块中标识的引用是从词法上解决,因此是静态的。

不像evalload不接受一个名称空间的参数。为了提供用于load的名称空间,设置current-namespace参数。下面的例子用racket/base模块的绑定对"here.rkts"中的表达式求值:

#lang racket
 
(parameterize ([current-namespace (make-base-namespace)])
  (load "here.rkts"))

你甚至可以用namespace-anchor->namespace来访问封闭模块的绑定以进行动态求值。在下面的例子中,当"here.rkts"被load(加载)时,它既可以指there,也可以指racket的绑定:

#lang racket
 
(define there "Utopia")
 
(define-namespace-anchor a)
(parameterize ([current-namespace (namespace-anchor->namespace a)])
  (load "here.rkts"))

不过,如果"here.rkts"定义任意的标识,这个定义不能在外围模块中直接(即静态地)引用。

racket/load模块语言不同于racket或racket/base。一个模块使用racket/load动态对待其所有上下文,通过模块主体里的每一个表去eval(使用被racket初始化的名称空间)。因此,在模块主体中使用evalload可以看到直接主体表相同的动态名称空间。例如,如果"here.rkts"包含以下内容

(define here "Morporkia")
(define (go!) (set! here there))

那么运行

#lang racket/load
 
(define there "Utopia")
 
(load "here.rkts")
 
(go!)
(printf "~a\n" here)

打印“Utopia”。

使用racket/load的缺点包括减少错误检查、工具支持和性能。例如,以下程序

#lang racket/load
 
(define good 5)
(printf "running\n")
good
bad

DrRacket的语法检查(Check Syntax)工具不能告诉第二个good是对第一个的参考,而对bad的非绑定参考仅在运行时报告而不在语法上拒绝。

 

16 

宏(macro)是一种语法表,它有一个关联的转换器,它将原有的表展开为现有的表。换句话说,宏是Racket编译器的扩展。racket/base和racket的大部分句法表实际上是宏,展开成一小部分核心结构。

像许多语言一样,Racket提供基于模式的宏,使得简单的转换易于实现并可靠使用。Racket还支持任意在Racket中实现或在Racket中宏展开变体中实现的宏转换器。

(对于自下而上的Racket宏的介绍,你可以参考:《宏的担忧》)

 

16.1 基于模式的宏

基于模式的宏将任何与模式匹配的代码替换为使用与模式部分匹配的原始语法的一部分的展开。

16.1.1 define-syntax-rule

创建宏的最简单方法是使用define-syntax-rule

(define-syntax-rule pattern template)

作为一个运行的例子,思考这个swap宏,它将交换值存储在两个变量中。可以使用define-syntax-rule实现如下:

宏在这个意义上是“非Racket的”,它涉及到变量上的副作用——但宏的重点是让你添加一些其它语言设计师可能不认可的语法表。

(define-syntax-rule (swap x y)
  (let ([tmp x])
    (set! x y)
    (set! y tmp)))

define-syntax-rule表绑定一个与单个模式匹配的宏。模式必须总是以一个开放的括号开头,后面跟着一个标识,这个标识在这个例子中是swap。在初始的标识之后,其它标识是宏模式变量),可以匹配宏使用中的任何内容。因此,这个宏匹配这个表(swap form1 form2)给任何form1form2

对match来说宏模式变量与模式变量是类似的。参见《模式匹配》。

define-syntax-rule中的模式之后是摸板。模板用于替代与模式匹配的表,但模板中的模式变量的每个实例都替换为宏使用模式变量匹配的部分。例如,在

(swap first last)

里,模式变量x匹配first,y匹配last,于是展开成为

(let ([tmp first])
  (set! first last)
  (set! last tmp))

16.1.2 词法范围

假设我们使用swap宏来交换名为tmp和other的变量:

(let ([tmp 5]
      [other 6])
  (swap tmp other)
  (list tmp other))

上述表达式的结果应为(6 5)。然而,这种swap的使用的直接展开为

(let ([tmp 5]
      [other 6])
  (let ([tmp tmp])
    (set! tmp other)
    (set! other tmp))
  (list tmp other))

其结果是(5 6)。问题在于,这个直接的展开搞混了上下文中的tmp,那里swap与宏摸板中的tmp被使用。

Racket不会为了swap的上述使用生成直接的展开。相反,它会这样生成内容

(let ([tmp 5]
      [other 6])
  (let ([tmp_1 tmp])
    (set! tmp other)
    (set! other tmp_1))
  (list tmp other))

正确的结果为(6 5)。同样,在示例中

(let ([set! 5]
      [other 6])
  (swap set! other)
  (list set! other))

其展开是

(let ([set!_1 5]
      [other 6])
  (let ([tmp_1 set!_1])
    (set! set!_1 other)
    (set! other tmp_1))
  (list set!_1 other))

因此局部set!绑定不会干扰宏模板引入的赋值。

换句话说,Racket的基于模式的宏自动维护词法范围,所以宏的实现者可以思考宏中的变量引用以及在同样的途径中作为函数和函数调用的宏使用。

16.1.3 define-syntaxsyntax-rules

define-syntax-rule表绑定一个与单一模式匹配的宏,但Racket的宏系统支持从同一标识开始匹配多个模式的转换器。要编写这样的宏,程序员必须使用更通用的define-syntax表以及syntax-rules转换器表:

(define-syntax id
  (syntax-rules (literal-id ...)
    [pattern template]
    ...))

define-syntax-rule表本身就是一个宏,它用一个仅包含一个模式和模板的syntax-rules表展开成define-syntax

例如,假设我们希望一个rotate宏将swap概括为两个或三个标识,从而

(let ([red 1] [green 2] [blue 3])
  (rotate red green)      ; swaps
  (rotate red green blue) ; rotates left
  (list red green blue))

生成(1 3 2)。我们可以使用syntax-rules实现 rotate:

(define-syntax rotate
  (syntax-rules ()
    [(rotate a b) (swap a b)]
    [(rotate a b c) (begin
                     (swap a b)
                     (swap b c))]))

表达式(rotate red green)与syntax-rules表中的第一个模式相匹配,因此展开到(swap red green)。表达式(rotate red green blue)与第二个模式匹配,所以它展开到(begin (swap red green) (swap green blue))。

16.1.4 序列的匹配

一个更好的rotate宏将允许任意数量的标识,而不是只有两个或三个标识。匹配任何数量的标识的rotate的使用,我们需要一个模式表,它有点像克林闭包(Kleene star)。在一个Racket宏模式中,一个闭包(star)被写成...

为了用...实现rotate,我们需要一个基元(base case)来处理单个标识,以及一个归纳案例以处理多个标识:

(define-syntax rotate
  (syntax-rules ()
    [(rotate a) (void)]
    [(rotate a b c ...) (begin
                          (swap a b)
                          (rotate b c ...))]))

当在一种模式中像c这样的模式变量被...跟着的时候,它在模板中必须也被...跟着。模式变量有效地匹配一个零序列或多个表,并在模板中以相同的顺序被替换。

到目前为止,rotate的两种版本都有点效率低下,因为成对交换总是将第一个变量的值移动到序列中的每个变量,直到达到最后一个变量为止。更有效的rotate将第一个值直接移动到最后一个变量。我们可以用...模式使用辅助宏去实现更有效的变体:

(define-syntax rotate
  (syntax-rules ()
    [(rotate a c ...)
     (shift-to (c ... a) (a c ...))]))
 
(define-syntax shift-to
  (syntax-rules ()
    [(shift-to (from0 from ...) (to0 to ...))
     (let ([tmp from0])
       (set! to from) ...
       (set! to0 tmp))]))

在shift-to宏里,模板里的...后跟着(set! to from),它导致(set! to from)表达式被重复多次,来使用to和from中匹配的每个标识序列。(to和from匹配的数量必须相同,否则这个宏展开就会因一个错误而失败。)

16.1.5 标识宏

根据我们的宏定义,swap或rotate标识必须在开括号之后使用,否则会报告语法错误:

> (+ swap 3)

eval:2:0: swap: bad syntax

  in: swap

一个标识宏(identifier macro)是一个模式匹配宏,当它被自己使用时不使用括号。例如,我们可以把val定义为一个展开到(get-val)的标识宏,这样(+ val 3)将展开到(+ (get-val) 3)。

> (define-syntax val
    (lambda (stx)
      (syntax-case stx ()
        [val (identifier? (syntax val)) (syntax (get-val))])))
> (define-values (get-val put-val!)
    (let ([private-val 0])
      (values (lambda () private-val)
              (lambda (v) (set! private-val v)))))
> val

0

> (+ val 3)

3

val宏使用syntax-case,它可以定义更强大的宏,这个在《混合模式和表达式:syntax-case》中讲解。现在,知道定义宏是必要的就足够了,在lambda中使用了syntax-case,而且其模板必须用明确的syntax构造器包装。最后,syntax-case从句可以指定模式后面的附加保护条件。

我们的val宏使用identifier?条件确保在括号中val一定不使用。反之,宏引一个发语法错误:

> (val)

eval:8:0: val: bad syntax

  in: (val)

16.1.6 set!转化器

使用上面的val宏,我们仍然必须调用put-val!更改存储值。然而,直接在val上使用set!会更方便。当val用于set!时援引宏,我们用make-set!-transformer创建一个赋值转换器。我们还必须声明set!作为syntax-case原语列表中的原语。

> (define-syntax val2
    (make-set!-transformer
     (lambda (stx)
       (syntax-case stx (set!)
         [val2 (identifier? (syntax val2)) (syntax (get-val))]
         [(set! val2 e) (syntax (put-val! e))]))))
> val2

0

> (+ val2 3)

3

> (set! val2 10)
> val2

10

16.1.7 宏生成宏

假设我们有许多标识,像val和val2,我们想重定向给访问函数和变位函数,像get-val和put-val!。我们希望可以这样写:

(define-get/put-id val get-val put-val!)

自然地,我们可以实现define-get/put-id为一个宏:

> (define-syntax-rule (define-get/put-id id get put!)
    (define-syntax id
      (make-set!-transformer
       (lambda (stx)
         (syntax-case stx (set!)
           [id (identifier? (syntax id)) (syntax (get))]
           [(set! id e) (syntax (put! e))])))))
> (define-get/put-id val3 get-val put-val!)
> (set! val3 11)
> val3

11

define-get/put-id是一个宏生成宏

16.1.8 展开的例子:按引用调用函数

我们可以使用模式匹配宏将一个表添加到Racket中,以定义一阶按引用调用函数。当通过按引用调用函数主体转变它的正式参数,这个转变应用到变量,它作为函数调用中的实参提供。

例如,如果define-cbr类似于define,除了定义按引用调用函数,也可以这样

(define-cbr (f a b)
  (swap a b))
 
(let ([x 1] [y 2])
  (f x y)
  (list x y))

生成(2 1)。

我们会通过有函数调用支持的对参数的访问器和转换器执行按引用调用函数,而不是直接提供参数值。特别是,对于上面的函数f,我们将生成

(define (do-f get-a get-b put-a! put-b!)
  (define-get/put-id a get-a put-a!)
  (define-get/put-id b get-b put-b!)
  (swap a b))

并将函数调用(f x y)重定向到

(do-f (lambda () x)
      (lambda () y)
      (lambda (v) (set! x v))
      (lambda (v) (set! y v)))

显然,define-cbr是一个宏生成宏,它绑定f到一个宏,这个宏展开到do-f的调用,换句话说,(define-cbr (f a b) (swap a b))需要去生成这个定义

(define-syntax f
  (syntax-rules ()
    [(id actual ...)
     (do-f (lambda () actual)
           ...
           (lambda (v)
             (set! actual v))
           ...)]))

同时,define-cbr需要使用f本体去定义do-f,第二部分略微更复杂些,所以我们把它的大部委托给一个define-for-cbr辅助模块,它可以让我们足够简单地编写define-cbr:

(define-syntax-rule (define-cbr (id arg ...) body)
  (begin
    (define-syntax id
      (syntax-rules ()
        [(id actual (... ...))
         (do-f (lambda () actual)
               (... ...)
               (lambda (v)
                 (set! actual v))
               (... ...))]))
    (define-for-cbr do-f (arg ...)
      () ; 下面的解释……
      body)))

我们剩下的任务是定义define-for-cbr以便它转换

(define-for-cbr do-f (a b) () (swap a b))

给上边的这个函数定义do-f两个功能定义。大部分的工作是为每个参数生成一个define-get/put-id声明,a和b,并把它们放在本体之前。通常,对于在模式和模板中的...来说,那是很容易的任务,但这次这里有一个捕获:我们需要生成这些名字get-a和put-a!以及get-b和put-b!,这个模式语言没有办法提供基于现有标识的综合标识。

事实证明,词法范围给了我们解决这个问题的方法。诀窍是为函数中的每个参数迭代一次define-for-cbr的展开,这就是为什么define-for-cbr用一个在参数列表后面明显无效的()作为开始的原因。除了要处理的参数外,我们还需要跟踪迄今为止所看到的所有参数以及为每个参数生成的get和put名称。在处理完所有的标识之后,我们就拥有了所有需要的名称。

这是define-for-cbr的定义:

(define-syntax define-for-cbr
  (syntax-rules ()
    [(define-for-cbr do-f (id0 id ...)
       (gens ...) body)
     (define-for-cbr do-f (id ...)
       (gens ... (id0 get put)) body)]
    [(define-for-cbr do-f ()
       ((id get put) ...) body)
     (define (do-f get ... put ...)
       (define-get/put-id id get put) ...
       body)]))

逐步地,展开如下:

(define-for-cbr do-f (a b)
  () (swap a b))
=> (define-for-cbr do-f (b)
     ([a get_1 put_1]) (swap a b))
=> (define-for-cbr do-f ()
     ([a get_1 put_1] [b get_2 put_2]) (swap a b))
=> (define (do-f get_1 get_2 put_1 put_2)
     (define-get/put-id a get_1 put_1)
     (define-get/put-id b get_2 put_2)
     (swap a b))

在get_1、get_2、put_1和put_2上的“下标(subscript)”通过宏展开插入到保留词法范围,因为每次迭代define-for-cbr生成的get不应绑定不同迭代生成的get。换句话说,我们本质上是在欺骗宏展开器以为我们生成新名字,但该技术说明了基于模式的宏具有自动词法范围的惊人力量。

最后这个表达式最终展开为

(define (do-f get_1 get_2 put_1 put_2)
  (let ([tmp (get_1)])
    (put_1 (get_2))
    (put_2 tmp)))

它实现了按名称调用函数f。

接下来,总结一下,我们可以只用三个基于模式的小巧宏添加按引用调用函数到Racket中:define-cbr、define-for-cbr和define-get/put-id。

 

16.2 通用宏转换器

define-syntax表为标识创建一个转换器绑定(transformer binding),这是一个可以在编译时使用的绑定,同时展开表达式以在运行时进行求值。与转换器绑定相关联的编译时值可以是任何东西;如果它是一个参数的过程,则绑定用作宏,而过程是 宏转换器(macro transformer)。

 

16.2.1 语法对象

宏转换器(即源和替换表)的输入和输出被表示为语法对象(syntax object)。语法对象包含符号、列表和常量值(如数字),它们基本上与表达式的quote表相对应。例如,表达式描述为(+ 1 2)包含符号'+和数字1和2,都在列表中。除了引用的内容之外,语法对象还将源位置和词汇绑定信息与表的每个部分关联起来。在报告语法错误时使用源位置信息(例如),词汇绑定信息允许宏系统维护词法范围。为了适应这种额外的信息,表达式(+ 1 2)的表示不仅是'(+ 1 2),而是将'(+ 1 2)打包成了语法对象。

要创建字面语法对象,使用syntax表:

> (syntax (+ 1 2))

#<syntax:eval:1:0 (+ 1 2)>

在同样的方式,'省略了quote,#'省略了syntax:

> #'(+ 1 2)

#<syntax:eval:1:0 (+ 1 2)>

只包含符号的语法对象是标识语法对象(identifier syntax object)。Racket提供了一些特定于标识语法对象的附加操作,包括identifier?操作以检查标识。最值得注意的是,free-identifier=?确定两个标识是否引用相同的绑定:

> (identifier? #'car)

#t

> (identifier? #'(+ 1 2))

#f

> (free-identifier=? #'car #'cdr)

#f

> (free-identifier=? #'car #'car)

#t

> (require (only-in racket/base [car also-car]))
> (free-identifier=? #'car #'also-car)

#t

要在语法对象中看到列表、符号、数字等,使用syntax->datum:

> (syntax->datum #'(+ 1 2))

'(+ 1 2)

syntax-e函数类似于syntax->datum,但它将源位置和词汇上下文信息单层解包,留下将自己的信息打包为语法对象的子表单:

> (syntax-e #'(+ 1 2))

'(#<syntax:eval:1:0 +> #<syntax:eval:1:0 1> #<syntax:eval:1:0 2>)

syntax-e函数总是在通过符号、数值和其它字面值所表示的子表周围留下语法对象打包器。它唯一解包额外子表是当解包一个序对时,在这种情况下,序对的cdr可以递归解包,取决于语法对象的构造方式。

当然,syntax->datum的反义词是datum->syntax。除了像'(+ 1 2)这样的数据外,datum->syntax还需要一个现有的语法对象来贡献它的词汇上下文,并且可以选择另一个语法对象来贡献它的源位置:

> (datum->syntax #'lex
                 '(+ 1 2)
                 #'srcloc)

#<syntax:eval:1:0 (+ 1 2)>

在上面的例子中,#'lex的词法上下文用于新的语法对象,而#'srcloc的源位置则被使用。

当datum->syntax的第二个(即,“datum”)参数包含语法对象时,这些语法对象将原封不动地保存在结果中。那就是,用syntax-e解构的结果最终产生了给予datum->syntax的这个语法对象。

 

16.2.2 宏转化器程序

一个参数的任何过程都可以是宏转换器。事实证明,syntax-rules表是一个展开为过程表的宏。例如,如果直接对syntax-rules表求值(而不是放在define-syntax表的右侧),结果就是一个过程:

> (syntax-rules () [(nothing) something])

#<procedure>

可以使用lambda直接编写自己的宏转换器过程,而不是使用syntax-rules。过程的参数是表示源表的语法对象,过程的结果必须是表示替换表的语法对象:

> (define-syntax self-as-string
    (lambda (stx)
      (datum->syntax stx
                     (format "~s" (syntax->datum stx)))))
> (self-as-string (+ 1 2))

"(self-as-string (+ 1 2))"

传递给宏转换器的源表表示在应用程序位置(即在启动表达式的括号之后)使用其标识的表达式,或者,如果它用作表达式位置而不是应用程序位置,则它本身表示标识。syntax-rules产生的过程如果其参数本身对应于标识的使用,则会引发语法错误,这就是为什么syntax-rules不实现一个标识宏的原因。

> (self-as-string (+ 1 2))

"(self-as-string (+ 1 2))"

> self-as-string

"self-as-string"

define-syntax表支持与define的函数一样的快捷语法,因此下面的self-as-string定义等同于显式地使用lambda的那个定义:

> (define-syntax (self-as-string stx)
    (datum->syntax stx
                   (format "~s" (syntax->datum stx))))
> (self-as-string (+ 1 2))

"(self-as-string (+ 1 2))"

 

16.2.3 混合模式和表达式:syntax-case

通过syntax-rules生成的程序在内部使用syntax-e来解构语法对象,并使用datum->syntax来构造结果。syntax-rules表没有提供一种方法来从模式匹配和模板构建模式中跳转到任意的Racket表达式中。

syntax-case表允许混合模式匹配、模板构造和任意表达式:

(syntax-case stx-expr (literal-id ...)
  [pattern expr]
  ...)

与syntax-rules不同,syntax-case表不产生过程。相反,它从一个stx-expr表达式决定的语法对象来匹配pattern。另外,每个syntax-case从句有一个pattern和一个expr,而不是pattern和template。在expr里,syntax表——通常用#'缩写——转换为模板构造方式;如果一个从句的expr以#'开始,那么我们就会有一个类似于syntax-rules的表:

> (syntax->datum
   (syntax-case #'(+ 1 2) ()
    [(op n1 n2) #'(- n1 n2)]))

'(- 1 2)

我们可以使用syntax-case来编写swap宏,而不是使用define-syntax-rule或syntax-rules:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y) #'(let ([tmp x])
                    (set! x y)
                    (set! y tmp))]))

使用syntax-case的一个优点是,我们可以给swap提供更好的错误报告。例如,用swap的define-syntax-rule定义,之后(swap x 2)在set!条件中产生了语法错误,因为2不是一个标识。我们可以改进swap的syntax-case实现来显式地检查子表:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y)
     (if (and (identifier? #'x)
              (identifier? #'y))
         #'(let ([tmp x])
             (set! x y)
             (set! y tmp))
         (raise-syntax-error #f
                             "not an identifier"
                             stx
                             (if (identifier? #'x)
                                 #'y
                                 #'x)))]))

通过这个定义,(swap x 2)提供了一个源自swap而不是set!的语法错误。

在上述swap的定义里,#'x和#'y是模板,即使它们不是作为宏转换器的结果。这个例子说明了如何使用模板来访问输入语法的片段,在这种情况下可以检查片段的表。同时,#'x或#'y的匹配项用于调用raise-syntax-error,于是语法错误信息可以直接指到非标识的源位置。

 

16.2.4 with-syntax和generate-temporaries

既然syntax-case允许我们用任意的Racket表达式计算,我们可以更简单地解决我们在编写define-for-cbr(参见《展开的例子:按引用调用函数》)中的一个问题,在这里我们需要根据序列id ...生成一组名称:

(define-syntax (define-for-cbr stx)
  (syntax-case stx ()
    [(_ do-f (id ...) body)
     ....
       #'(define (do-f get ... put ...)
           (define-get/put-id id get put) ...
           body) ....]))

代替上面的....我们需要绑定get ...和put ...到生成标识的列表。我们不能使用let绑定get和put,因为我们需要绑定那个计数作为模式变量,而不是普通的局部变量。with-syntax表允许我们绑定模式变量:

(define-syntax (define-for-cbr stx)
  (syntax-case stx ()
    [(_ do-f (id ...) body)
     (with-syntax ([(get ...) ....]
                   [(put ...) ....])
       #'(define (do-f get ... put ...)
           (define-get/put-id id get put) ...
           body))]))

现在我们需要一个表达式来代替....生成尽可能多的标识符,因为在原始模式中有id。由于这是一个常见任务,Racket提供了一个辅助函数,generate-temporaries,它接受一系列标识并返回一系列生成的标识:

(define-syntax (define-for-cbr stx)
  (syntax-case stx ()
    [(_ do-f (id ...) body)
     (with-syntax ([(get ...) (generate-temporaries #'(id ...))]
                   [(put ...) (generate-temporaries #'(id ...))])
       #'(define (do-f get ... put ...)
           (define-get/put-id id get put) ...
           body))]))

这种生成标识的方法通常比诱使宏展开器使用纯粹基于模式的宏生成名称更容易理解。

一般来说,with-syntax绑定左边是一个模式,就像在syntax-case中一样。事实上,with-syntax表只是一个部分由内向外翻转的syntax-case表。

 

16.2.5 编译和运行时阶段

随着宏集变得越来越复杂,你可能需要编写你自己的辅助函数,像generate-temporaries。例如,提供良好的语法错误消息,swap、rotate和define-cbr都应该检查源表中的某些子表是否是标识。我们可以使用check-ids函数在任何地方执行此检查:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y) (begin
                  (check-ids stx #'(x y))
                  #'(let ([tmp x])
                      (set! x y)
                      (set! y tmp)))]))
 
(define-syntax (rotate stx)
  (syntax-case stx ()
    [(rotate a c ...)
     (begin
       (check-ids stx #'(a c ...))
       #'(shift-to (c ... a) (a c ...)))]))

check-ids函数可以使用syntax->list函数包装列表的语法对象转换成语法对象列表:

(define (check-ids stx forms)
  (for-each
   (lambda (form)
     (unless (identifier? form)
       (raise-syntax-error #f
                           "not an identifier"
                           stx
                           form)))
   (syntax->list forms)))

然而,如果以这种方式定义swap和check-ids,则它不起作用:

> (let ([a 1] [b 2]) (swap a b))

check-ids: undefined;

 cannot reference an identifier before its definition

  in module: top-level

问题是check-ids被定义为一个运行时表达式,但是swap试图在编译时使用它。在交互模式中,编译时和运行时是交错的,但它们不会在模块主体内交错,而且它们也不会在预编译的模块之间交错。为了帮助所有这些模式一致地对待代码,Racket将不同阶段的绑定空间分隔开来。

要定义可在编译时被引用的check-ids函数,使用begin-for-syntax

(begin-for-syntax
  (define (check-ids stx forms)
    (for-each
     (lambda (form)
       (unless (identifier? form)
         (raise-syntax-error #f
                             "not an identifier"
                             stx
                             form)))
     (syntax->list forms))))

使用此语法定义,swap就可以运行了:

> (let ([a 1] [b 2]) (swap a b) (list a b))

'(2 1)

> (swap a 1)

eval:13:0: swap: not an identifier

  at: 1

  in: (swap a 1)

当将程序组织成模块时,你也许希望将辅助函数放在一个模块中,以供驻留在其它模块上的宏使用。在这种情况下,你可以使用define编写辅助函数:

"utils.rkt"

#lang racket
 
(provide check-ids)
 
(define (check-ids stx forms)
  (for-each
   (lambda (form)
     (unless (identifier? form)
       (raise-syntax-error #f
                           "not an identifier"
                           stx
                           form)))
   (syntax->list forms)))

然后,在实现宏模块中,使用(require (for-syntax "utils.rkt"))代替(require "utils.rkt")导入辅助函数:

#lang racket
 
(require (for-syntax "utils.rkt"))
 
(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y) (begin
                  (check-ids stx #'(x y))
                  #'(let ([tmp x])
                      (set! x y)
                      (set! y tmp)))]))

由于模块是单独编译的,不能有循环依赖项,因此可以在编译实现swap模块之前编译"utils.rkt"模块的运行时主体。因此,只要(require (for-syntax ....))将"utils.rkt"中的运行时定义显式转换为编译时,就可以使用它们来实现swap。

racket模块提供了syntax-casegenerate-temporarieslambdaif以及更多以用在运行时阶段和编译时阶段。这就是为什么我们既可以直接在racket的REPL中也可以在一个define-syntax表的右端使用syntax-case的原因。

相反,racket/base模块只在运行时阶段导出这些绑定。如果你更改了上面定义swap的模块,使其使用racket/base语言而不是racket,那么它不再工作。添加(require (for-syntax racket/base))导入syntax-case和更多内容进入编译时阶段,以便模块再次工作。

假设define-syntax用于在define-syntax表的右侧定义一个本地宏。在这种情况下,内部define-syntax的右侧位于元编译阶段级别(meta-compile phase level),也称为阶段级别2(phase level 2)。要将syntax-case导入到该阶段级别,你必须使用(require (for-syntax (for-syntax racket/base))),或者等效地使用(require (for-meta 2 racket/base))。例如,

#lang racket/base
(require  ;; This provides the bindings for the definition
          ;; of shell-game.
          ;;这为shell-game的定义提供了绑定。
          (for-syntax racket/base)
 
          ;; And this for the definition of
          ;; swap.
          ;;这是互换的定义。
          (for-syntax (for-syntax racket/base)))
 
(define-syntax (shell-game stx)
 
  (define-syntax (swap stx)
    (syntax-case stx ()
      [(_ a b)
       #'(let ([tmp a])
           (set! a b)
           (set! b tmp))]))
 
  (syntax-case stx ()
    [(_ a b c)
     (let ([a #'a] [b #'b] [c #'c])
       (when (= 0 (random 2)) (swap a b))
       (when (= 0 (random 2)) (swap b c))
       (when (= 0 (random 2)) (swap a c))
       #`(list #,#,#,c))]))
 
(shell-game 3 4 5)
(shell-game 3 4 5)
(shell-game 3 4 5)

反向阶段级别也存在。如果宏使用导入的for-syntax辅助函数,并且辅助函数返回由syntax生成的语法对象常量,那么语法中的标识将需要在阶段级别-1(phase level -1),也称为模板阶段级别(template phase level)进行绑定,以便在运行时阶段级别相对于定义宏的模块进行绑定。

例如,在下面的例子中没有语法变换器的swap-stx的辅助函数——它只是一个普通的函数——但它产生的语法对象得到拼接成shell-game的结果。因此,它包含的子模块需要在shell-game第1阶段用(require (for-syntax 'helper))导入。

但从swap-stx的角度,当shell-game返回的语法被求值时,其结果最终在第-1阶段求值。换句话说,一个负向阶段级别是一个从正方向来看相反的阶段级别:shell-game的第1阶段是swap-stx的第0阶段,所以shell-game的第0阶段是swap-stx的的-1阶段。这就是为什么这个例子不起作用的原因——'helper子模块在第-1阶段没有绑定。

#lang racket/base
(require (for-syntax racket/base))
 
(module helper racket/base
  (provide swap-stx)
  (define (swap-stx a-stx b-stx)
    #`(let ([tmp #,a-stx])
          (set! #,a-stx #,b-stx)
          (set! #,b-stx tmp))))
 
(require (for-syntax 'helper))
 
(define-syntax (shell-game stx)
  (syntax-case stx ()
    [(_ a b c)
     #`(begin
         #,(swap-stx #'#'b)
         #,(swap-stx #'#'c)
         #,(swap-stx #'#'c)
         (list a b c))]))
 
(define x 3)
(define y 4)
(define z 5)
(shell-game x y z)

为修复这个例子,我们添加(require (for-template racket/base))到'helper子模块。

#lang racket/base
(require (for-syntax racket/base))
 
(module helper racket/base
  (require (for-template racket/base)) ; binds `let` and `set!` at phase -1
                                       ;在第-1阶段绑定“let”和“set!”
  (provide swap-stx)
  (define (swap-stx a-stx b-stx)
    #`(let ([tmp #,a-stx])
          (set! #,a-stx #,b-stx)
          (set! #,b-stx tmp))))
 
(require (for-syntax 'helper))
 
(define-syntax (shell-game stx)
  (syntax-case stx ()
    [(_ a b c)
     #`(begin
         #,(swap-stx #'#'b)
         #,(swap-stx #'#'c)
         #,(swap-stx #'#'c)
         (list a b c))]))
 
(define x 3)
(define y 4)
(define z 5)
(shell-game x y z)
(shell-game x y z)
(shell-game x y z)

 

16.2.6 一般阶段级别

阶段(phase)可以被看作是在进程的管道中分离出计算的方法,其中一个进程产生下一个进程使用的代码。(例如,由预处理器进程、编译器和汇编程序组成的管道)。

设想为此启动两个Racket过程。如果忽略套接字和文件等进程间通信通道,进程将无法共享从一个进程的标准输出管道传输到另一个进程标准输入的文本以外的任何内容。类似地,Racket有效地允许一个模块的多个调用存在于同一个进程中,但按阶段分开。Racket强制执行这种阶段的分离,在这种情况下,不同的阶段除了通过宏展开协议之外,不能以任何方式进行通信,其中一个阶段的输出是下一个阶段使用的代码。

16.2.6.1 阶段和绑定

标识的每个绑定都存在于特定阶段。绑定与其阶段之间的链接由整数阶段级别(phase-level)表示。阶段级别0是用于“普通”(或“运行时”)定义的阶段,因此

(define age 5)

为age添加绑定到阶段级别0中。标识age可以用begin-for-syntax在更高的阶段级别定义:

(begin-for-syntax
  (define age 5))

使用单个begin-for-syntax包装器,age在阶段级别1定义。我们可以容易地在同一个模块或顶级命名空间中混合这两个定义,并且在不同的阶段级别上定义的两个age之间没有冲突:

> (define age 3)
> (begin-for-syntax
    (define age 9))

在阶段级别0的age绑定值为3,在阶段级别1的age绑定值为9。

语法对象将绑定信息捕获为一级值。因此,

#'age

是一个表示age绑定的语法对象,但由于有两个age(一个在阶段级别0,一个在阶段级别1),它捕获的是哪一个?事实上,Racket为#'age注入了所有阶段级别的词汇信息,所以答案是#'age同时捕捉了这两者。

#'age捕获的age的相关绑定是在最终使用#'age时确定的。例如,我们将#'age绑定到一个模式变量,以便在模板中使用它,然后eval对模板求值:我们在这里使用eval演示阶段,但请参见《反射和动态求值》了解有关eval的警告。

> (eval (with-syntax ([age #'age])
          #'(displayln age)))

3

结果是3,因为age用于0阶段级别。我们可以在begin-for-syntax内使用age再试一次:

> (eval (with-syntax ([age #'age])
          #'(begin-for-syntax
              (displayln age))))

9

在这种情况下,答案是9,因为我们在阶段级别1使用age,而不是0(即, begin-for-syntax在阶段级别1求值表达式)。所以,你可以看到我们从相同的语法对象开始#'age开始,我们可以用两种不同的方法使用它:在阶段级别0和在阶段级别1。

语法对象从第一次存在时起就具有词法上下文。模块提供的语法对象保留其词法上下文,因此它引用源模块上下文中的绑定,而不是其使用上下文。以下示例在阶段级别0定义了button,并将其绑定到0,而see-button则绑定了模块a中的button的语法对象:

> (module a racket
    (define button 0)
    (provide (for-syntax see-button))
  
    ; 为什么不使用(define see-button #'button)? 我们稍后解释。
    (define-for-syntax see-button #'button))
> (module b racket
    (require 'a)
    (define button 8)
    (define-syntax (m stx)
      see-button)
    (m))
> (require 'b)

0

在m宏的结果是see-button的值,它是#'button,带有模块a模块的词汇上下文。即使在b中有另一个button,第二个button不会混淆Racket,因为#'button的词汇上下文(绑定到see-button的值)是a。

请注意,通过define-for-syntax定义see-button,它绑定在阶段级别1。阶段级别1是必须的,因为m是一个宏,所以它的主体执行的阶段高于其定义的上下文。由于m是在阶段级别0定义的,因此其主体处于阶段级别1,所以由主体引用的任何绑定都必须在阶段级别1。

16.2.6.2 阶段和模块

一个阶段级别(phase level)是一个模块相关概念。当通过require从另一个模块导入时,Racket允许我们将导入的绑定变换为与原始绑定不同的阶段级别:

(require "a.rkt")                ; 不带阶段变换的导入
(require (for-syntax "a.rkt"))   ; 通过+1变换阶段
(require (for-template "a.rkt")) ; 通过-1变换阶段
(require (for-meta 5 "a.rkt"))   ; 通过+5变换阶段

也就是说,在require中使用for-syntax意味着来自该模块的所有绑定的阶段级别都会增加。在阶段级别0为define并用for-syntax导入的绑定成为阶段级别1的绑定:

> (module c racket
    (define x 0) ; 在阶段级别0定义
    (provide x))
> (module d racket
    (require (for-syntax 'c))
    ; 在阶段级别1的绑定,而不是0:
    #'x)

让我们看看如果我们尝试在阶段级别0为#'button语法对象创建绑定会发生了什么:

> (define button 0)
> (define see-button #'button)

现在在第阶段0中定义了button和see-button。#'button的词汇上下文会知道在阶段0有一个绑定。事实上,如果我们尝试对see-button进行eval,似乎一切都很顺利:

> (eval see-button)

0

现在,让我们在宏中使用see-button:

> (define-syntax (m stx)
    see-button)
> (m)

see-button: undefined;

 cannot reference an identifier before its definition

  in module: top-level

显然,see-button在阶段1没有定义,因此我们不能在宏主体内引用它。让我们尝试在另一个模块中使用see-button,方法是将将按钮(button)定义放在模块中,并在阶段级别1导入它。那么,我们将在阶段级别1获得see-button:

> (module a racket
    (define button 0)
    (define see-button #'button)
    (provide see-button))
> (module b racket
    (require (for-syntax 'a)) ; 在阶段级别1获得see-button
    (define-syntax (m stx)
      see-button)
    (m))

eval:1:0: button: unbound identifier;

 also, no #%top syntax transformer is bound

  in: button

Racket说button现在已解除绑定!当在阶段级别1导入a时,我们有以下绑定:

button     在阶段级别1
see-button 在阶段级别1

因此,宏m能够在阶段级别1看到see-button的绑定并将返回#'button语法对象,它指的是在阶段级别1的button绑定。但是m的使用是在阶段级别0,在b的阶段级别0没有button。这就是为什么see-button需要在阶段级别1,就像在原来的a中一样。那么,在原来的b中,我们有以下绑定:

button     在阶段级别0
see-button 在阶段级别1

在这种情况下,我们可以在宏中使用see-button,因为see-button是在阶段级别1绑定的。当宏展开时,它将引用在阶段级别0的button绑定。

用(define see-button #'button)定义see-button本身没有错;它取决于我们打算如何使用see-button。例如,我们可以安排m合理地使用see-button,因为它使用begin-for-syntax将其放在了阶段级别1的上下文中:

> (module a racket
    (define button 0)
    (define see-button #'button)
    (provide see-button))
> (module b racket
    (require (for-syntax 'a))
    (define-syntax (m stx)
      (with-syntax ([x see-button])
        #'(begin-for-syntax
            (displayln x))))
    (m))

0

在这种情况下,模块b在阶段级别1上同时绑定了button和see-button。宏的展开是

(begin-for-syntax
  (displayln button))

这是可行的,因为button是在阶段级别1绑定的。

现在,你可以通过在阶段等级0和阶段等级1中导入a来欺骗阶段系统。然后,你将拥有以下绑定

button     在阶段级别0
see-button 在阶段级别0
button     在阶段级别1
see-button 在阶段级别1

现在,你可能希望宏中的see-button可以工作,但它没有:

> (module a racket
    (define button 0)
    (define see-button #'button)
    (provide see-button))
> (module b racket
    (require 'a
             (for-syntax 'a))
    (define-syntax (m stx)
      see-button)
    (m))

eval:1:0: button: unbound identifier;

 also, no #%top syntax transformer is bound

  in: button

宏m中的see-button来自(for-syntax 'a)导入。要使宏m工作,需要在阶段0绑定button。这种绑定是存在的——它由(require 'a)表明。然而,(require 'a)和(require (for-syntax 'a))是同一模块的不同实例化。阶段1的see-button仅指第1阶段的button,而不是在阶段0从不同实例化绑定的button,即使来自同一个源模块。

实例化之间的这种阶段级别不匹配可以用syntax-shift-phase-level修复。回想一下,像#'button这样的语法对象在所有阶段级别捕获词汇信息。这里的问题是,see-button在阶段1调用,但需要返回一个可以在阶段0进行求值的语法对象。默认情况下,see-button在同一阶段级别绑定到#'button。但是,使用syntax-shift-phase-level,我们可以使see-button在不同的相对阶段级别上引用#'button。在这种情况下,我们使用-1的变换使阶段1的see-button指向阶段0的#'button。(由于阶段变换发生在每一个级别,它也使阶段0的see-button指向阶段-1的#'button)

请注意,syntax-shift-phase-level仅仅创建了一个跨阶段的引用。为了使该引用有效,我们仍然需要在两个阶段实例化模块,以便引用及其目标具有可用的绑定。因此,在模块'b中,我们仍然在阶段0和阶段1导入模块'a——使用(require 'a (for-syntax 'a))——所以我们有一个阶段1绑定用于see-button,一个阶段0绑定用于button。现在宏m就将起作用了。

> (module a racket
    (define button 0)
    (define see-button (syntax-shift-phase-level #'button -1))
    (provide see-button))
> (module b racket
    (require 'a (for-syntax 'a))
    (define-syntax (m stx)
      see-button)
    (m))
> (require 'b)

0

顺便问一下,在阶段0绑定的see-button会发生什么变化?它的#'button绑定也同样转移到阶段-1。由于button本身在阶段-1没有绑定,如果我们试图在阶段0对see-button求值,我们会得到一个错误。换句话说,我们还没有永久地解决错配问题——我们只是把它转移到一个不太麻烦的位置。

> (module a racket
    (define button 0)
    (define see-button (syntax-shift-phase-level #'button -1))
    (provide see-button))
> (module b racket
    (require 'a (for-syntax 'a))
    (define-syntax (m stx)
      see-button)
    (m))
> (module b2 racket
    (require 'a)
    (eval see-button))
> (require 'b2)

button: undefined;

 cannot reference an identifier before its definition

  in module: top-level

当宏试图匹配字面绑定时——使用syntax-case或syntax-parse,也会出现上述不匹配。

> (module x racket
    (require (for-syntax syntax/parse)
             (for-template racket/base))
  
    (provide (all-defined-out))
  
    (define button 0)
    (define (make) #'button)
    (define-syntax (process stx)
      (define-literal-set locals (button))
      (syntax-parse stx
        [(_ (n (~literal button))) #'#''ok])))
> (module y racket
    (require (for-meta 1 'x)
             (for-meta 2 'x racket/base))
  
    (begin-for-syntax
      (define-syntax (m stx)
        (with-syntax ([out (make)])
          #'(process (0 out)))))
  
    (define-syntax (p stx)
      (m))
  
    (p))

eval:2:0: process: expected the identifier `button'

  at: button

  in: (process (0 button))

在这个例子中,make在在阶段级别2的y中使用,它返回#'button语法对象——它是指在x中的阶段级别0绑定的button,以及在(for-meta 2 'x)中的y内阶段级别2绑定的。process宏是在阶段级别1从(for-meta 1 'x)导入的,它知道button应该绑定在阶段级别1。当syntax-parse在process中执行时,它正在寻找在阶段级别1绑定的button,但它只看到阶段级别2绑定,因此不匹配。

为了修正这个例子,我们可以在相对于x的阶段级别1中提供make,然后在阶段级别1在y中导入它:

> (module x racket
    (require (for-syntax syntax/parse)
             (for-template racket/base))
  
    (provide (all-defined-out))
  
    (define button 0)
  
    (provide (for-syntax make))
    (define-for-syntax (make) #'button)
    (define-syntax (process stx)
      (define-literal-set locals (button))
      (syntax-parse stx
        [(_ (n (~literal button))) #'#''ok])))
> (module y racket
    (require (for-meta 1 'x)
             (for-meta 2 racket/base))
  
    (begin-for-syntax
      (define-syntax (m stx)
        (with-syntax ([out (make)])
          #'(process (0 out)))))
  
    (define-syntax (p stx)
      (m))
  
    (p))
> (require 'y)

'ok

 

16.2.7 语法污染

宏的使用可以展开为未从绑定宏的模块导出的标识的使用。一般来说,这样的标识不能从展开表达式中提取出来并在不同的上下文中使用,因为在不同上下文中使用标识可能会破坏宏模块的不变量。

例如,下面的模块导出一个宏go,它展开为使用unchecked-go:

"m.rkt"

#lang racket
(provide go)
 
(define (unchecked-go n x)
  ; 为了避免灾难,n必须是数字
  (+ n 17))
 
(define-syntax (go stx)
  (syntax-case stx ()
   [(_ x)
    #'(unchecked-go 8 x)]))

如果从(go 'a)展开中解析了对unchecked-go的引用,那么它可能会被插入到一个新的表达式(unchecked-go #f 'a)中速,从而导致灾难。datum->syntax过程同样可以类似地用于构建对未报告标识的引用,即使没有宏展开包括对标识的引用。

为了防止滥用未报告的标识,go宏必须使用syntax-protect明确保护其展开:

(define-syntax (go stx)
  (syntax-case stx ()
   [(_ x)
    (syntax-protect #'(unchecked-go 8 x))]))

syntax-protect函数会使从go结果中提取的任何语法对象成为污染。宏展开器拒绝受污染的标识,因此试图从(go 'a)的展开中提取unchecked-go会产生一个标识,该标识不能用于构造新的表达式(或者至少宏展开器不会将接受)。syntax-rules、syntax-id-rule和define-syntax-rule表自动保护它们的展开结果。

更准确地说,syntax-protect 装备了一个带染料包(dye pack)的语法对象。当一个语法对象被装备起来时,syntax-e会在其结果中污染任何语法对象。同样,datum->syntax在其第一个参数被装备时会污染其结果。最后,如果引用的语法对象的任何部分被装备,那么相应的部分就会在结果语法常量中受到污染。

当然,宏展开器本身必须能够解除语法对象上的污染,以便进一步展开表达式或其子表达式。当语法对象装备有一个染料包时,染料包具有可用于解除染料包的关联检查器。(syntax-protect stx)函数调用实际上是(syntax-arm stx #f #t)的简写,它使用合适的检查器对stx进行检查。在试图展开或编译每个表达式之前,使用syntax-disarm和它的检查器。

与宏展开器将属性从语法转换器的输入复制到其输出(请参见《(part ("(lib scribblings/reference/reference.scrbl)" "stxprops"))》(语法对象属性))的方式大致相同,展开器会将染料包从转换器的输入拷贝到其输出。基于前面的示例,

"n.rkt"

#lang racket
(require "m.rkt")
 
(provide go-more)
 
(define y 'hello)
 
(define-syntax (go-more stx)
  (syntax-protect #'(go y)))

(go-more)的展开引入了对(go y)中未报告的y的引用,并且展开结果是装备起来的,因此无法从展开中提取y。即使go没有使用syntax-protect作为其结果(也许是因为它根本不需要保护unchecked-go),(go y)上的染色包也会传播到最终展开(unchecked-go 8 y)。宏展开器使用syntax-rearm将染料包从转换器的输入传播到其输出。

16.2.7.1 污染模式

在某些情况下,宏实现者打算允许对宏结果进行有限的破坏,而不会影响结果。例如,给定以下define-like-y宏,

"q.rkt"

#lang racket
 
(provide define-like-y)
 
(define y 'hello)
 
(define-syntax (define-like-y stx)
  (syntax-case stx ()
    [(_ id) (syntax-protect #'(define-values (id) y))]))

有人可能在内部定义中使用宏:

(let ()
  (define-like-y x)
  x)

"q.rkt"模块的实现者很可能打算允许使用define-like-y。然而,要将内部定义转换为letrec绑定,必须解构define-like-y生成的的define表,这通常会污染绑定的x和对y的引用。

相反,允许在内部使用define-like-y,因为syntax-protect专门处理以define-values开头的语法列表。在这种情况下,不是装备整个表达式,而是装备语法列表的每个单独元素,将染料包进一步推入列表的第二个元素,以便将它们附加到定义的标识。因此,展开结果(define-values (x) y)中的define-values、x和y是单独被装备的,并且可以对定义进行解构以转换为letrec

就像syntax-protect一样,展开器通过将染料包推送到列表元素中,重新排列以define-values开头的转换结果。因此,define-like-y可以实现为生成(define id y),它使用define而不是define-values。在这种情况下,整个define表首先装备一个染料包,但是当define表展开为define-values时,染料包会移动到各个部分。

宏展开器处理以define-values开头的语法列表结果的方式与处理以define-syntaxes开头结果的方式相同。以begin开头的语法列表结果被类似地处理,只是语法列表的第二个元素被视为所有其它元素(即,立即元素被装备,而不是其内容)。此外,宏展开器递归地应用这种特殊处理,以防宏生成包含嵌套define-values表的begin表。

染料包的默认应用程序可以通过将'taint-mode属性(见《(part ("(lib scribblings/reference/reference.scrbl)" "stxprops"))》(语法对象属性))附加到宏转换器的结果语法对象来覆盖。如果属性值是'opaque,那么语法对象被装备而且不是它的部件。如果属性值为'transparent,则语法对象的各个部分都是装备的。如果属性值是'transparent-binding,那么语法对象的部件和第二个部件的子部件(如define-valuesdefine-syntaxes)被装备。'transparent和'transparent-binding模式会在零件上触发递归属性检查,因此可以任意深入地将防护推送到变换器的结果中。

16.2.7.2 污染和代码检查

用于特权的工具(如调试变换器)必须解除展开器中的染料包。权限通过 代码检查器授予。每个染料包记录一个检查器,并且可以使用足够强大的检查器解除语法对象。

当声明一个模块时,声明会捕获current-code-inspector参数的当前值。当模块中定义的宏转换器应用syntax-protect时,将使用捕获的检查器。一个工具可以通过向syntax-disarm提供与模块检查器相同的检查器或超级检查器。不受信任的代码最终会在将current-code-inspector设置为功能较弱的检查器后运行(在加载了可信代码(如调试工具)之后)。

通过这种安排,宏生成宏需要小心一些,因为生成宏可能会在生成的宏中嵌入语法对象,这些对象需要具有生成模块的保护级别,而不是包含生成宏的模块的保护级别。为了避免这个问题,请使用模块的声明时间检查器,该检查器可以作为(variable-reference->module-declaration-inspector (#%variable-reference))访问,并使用它来定义syntax-protect的变体。

例如,假设go宏是通过一个宏实现的:

#lang racket
(provide def-go)
 
(define (unchecked-go n x)
  (+ n 17))
 
(define-syntax (def-go stx)
  (syntax-case stx ()
    [(_ go)
     (protect-syntax
      #'(define-syntax (go stx)
          (syntax-case stx ()
            [(_ x)
             (protect-syntax #'(unchecked-go 8 x))])))]))

当def-go在另一个模块中用于定义go时,以及当go定义模块与def-go定义模块处于不同的保护级别时,生成的宏使用protect-syntax是不正确的。使用unchecked-go应该在def-go定义模块的级别上进行保护,而不是在go定义模型的级别上。

解决方案是定义并使用go-syntax-protect,而不是:

#lang racket
(provide def-go)
 
(define (unchecked-go n x)
  (+ n 17))
 
(define-for-syntax go-syntax-protect
  (let ([insp (variable-reference->module-declaration-inspector
               (#%variable-reference))])
    (lambda (stx) (syntax-arm stx insp))))
 
(define-syntax (def-go stx)
  (syntax-case stx ()
    [(_ go)
     (protect-syntax
      #'(define-syntax (go stx)
          (syntax-case stx ()
           [(_ x)
            (go-syntax-protect #'(unchecked-go 8 x))])))]))

16.2.7.3 受保护的导出

有时,一个模块需要将绑定导出到一些模块——与导出模块处于相同信任级别的其他模块——但阻止来自不受信任模块的访问。此类导出应使用provide中的protect-out表。例如,在这个意义上,ffi/unsafe将其所有不安全绑定导出为受保护的(protected)

代码检查器同样提供了确定哪些模块是可信的,哪些模块是不可信的机制。当一个模块被声明时,current-code-inspector的值与模块声明相关联。当一个模块被实例化时(即,当声明的主体被实际执行时),会创建一个子检查器来保护模块的导出。访问模块的受保护导出需要在检查器层次结构中比模块的实例化检查器更高的代码检查器;请注意,模块的声明检查器始终高于其实例化检查器,因此使用相同的代码声明模块,检查器可以访问彼此的导出。

模块中的语法对象常量(如模板中的文字标识)保留其源模块的检查器。通过这种方式,来自受信任模块的宏可以在不受信任的模块中使用,并且宏展开中的受保护标识仍然有效,即使它们最终出现在不受信赖的模块中。当然,这些标识符应该是装备的,以便它们不能从宏展开中提取出来并被不可信代码滥用。

不幸的是,从".zo"文件编译的代码本质上是不可信的,因为它可以通过compile以外的方法进行合成。当编译代码被写入".zo"文件的被编译代码本质上是不可信的。当编译后的代码写入到一个".zo"文件时,编译代码中的语法对象常量会丢失其检查器。编译代码中的所有语法对象常量都会获取加载代码时封装模块的声明时间检查器。

 

17 创造语言

前一章中定义的宏功能允许程序员定义语言的语法扩展,但宏有两种限制:

  • 宏不能限制上下文中可用的语法或改变包围的表的意义;

  • 宏只能在语言的词汇约定参数内扩展语言的语法,例如用括号将宏名称与其子表分组,以及用标识、关键字和核心语法。

读取器和展开器层之间的区别在《列表和Racket语法》中介绍。

也就是说,宏只能扩展语言,并且只能在展开器层进行扩展。Racket提供了额外的功能,用于定义展开器层的起始点、读取器层、定义读取器层的起始点,以及将读取器和展开器起始点封装为方便命名的语言。

17.1 模块语言

当使用通常写法module表来编写模块时,在新模块名称之后指定的模块路径将为模块提供初始导入。由于初始导入模块甚至决定了模块主体中可用的最基本绑定,例如require,因此初始导入可以称为模块语言(module language)

最常见的模块语言是racket或racket/base,但你可以通过定义合适的模块来定义自己的模块语言。例如,使用provide子表,如all-from-outexcept-outrename-out,你可以添加、删除或重命名racket中的绑定,以生成模块语言,它是racket}的变体:

module》介绍了module表的通常写法。

> (module raquet racket
    (provide (except-out (all-from-out racket) lambda)
             (rename-out [lambda function])))
> (module score 'raquet
    (map (function (points) (case points
                             [(0) "love"] [(1) "fifteen"]
                             [(2) "thirty"] [(3) "forty"]))
         (list 0 2)))
> (require 'score)

'("love" "thirty")

17.1.1 隐式表绑定

如果在定义自己模块语言时试图从racket中删除太多内容 ,那么生成的模块将不再作为模块语言正常工作:

> (module just-lambda racket
    (provide lambda))
> (module identity 'just-lambda
    (lambda (x) x))

eval:2:0: module: no #%module-begin binding in the module's

language

  in: (module identity (quote just-lambda) (lambda (x) x))

#%module-begin表是一种封装模块主体的隐式表。它必须由要用作模块语言的模块提供:

> (module just-lambda racket
    (provide lambda #%module-begin))
> (module identity 'just-lambda
    (lambda (x) x))
> (require 'identity)

#<procedure>

racket/base提供的其它隐式表包括:用于函数调用的#%app、用于文本的#%datum和用于没有绑定的标识#%top

> (module just-lambda racket
    (provide lambda #%module-begin
             ; ten needs these, too:
             #%app #%datum))
> (module ten 'just-lambda
    ((lambda (x) x) 10))
> (require 'ten)

10

隐式表,如#%app可以在一个模块中显式地使用,但它们的存在主要是为了允许模块语言限制或更改隐式使用的含义。例如,lambda-calculus模块语言可能会将函数限制为单个参数,限制函数调用以提供单个参数,将模块主体限制为单个表达式,禁止使用文本,并将未绑定标识视为未解释的符号:

> (module lambda-calculus racket
    (provide (rename-out [1-arg-lambda lambda]
                         [1-arg-app #%app]
                         [1-form-module-begin #%module-begin]
                         [no-literals #%datum]
                         [unbound-as-quoted #%top]))
    (define-syntax-rule (1-arg-lambda (x) expr)
      (lambda (x) expr))
    (define-syntax-rule (1-arg-app e1 e2)
      (#%app e1 e2))
    (define-syntax-rule (1-form-module-begin e)
      (#%module-begin e))
    (define-syntax (no-literals stx)
      (raise-syntax-error #f "no" stx))
    (define-syntax-rule (unbound-as-quoted . id)
      'id))
> (module ok 'lambda-calculus
    ((lambda (x) (x z))
     (lambda (y) y)))
> (require 'ok)

'z

> (module not-ok 'lambda-calculus
    (lambda (x y) x))

eval:4:0: lambda: use does not match pattern: (lambda (x)

expr)

  in: (lambda (x y) x)

> (module not-ok 'lambda-calculus
    (lambda (x) x)
    (lambda (y) (y y)))

eval:5:0: #%module-begin: use does not match pattern:

(#%module-begin e)

  in: (#%module-begin (lambda (x) x) (lambda (y) (y y)))

> (module not-ok 'lambda-calculus
    (lambda (x) (x x x)))

eval:6:0: #%app: use does not match pattern: (#%app e1 e2)

  in: (#%app x x x)

> (module not-ok 'lambda-calculus
    10)

eval:7:0: #%datum: no

  in: (#%datum . 10)

模块语言很少重新定义#%app#%datum#%top,但重新定义#%module-begin往往更为有用。例如,当使用模块构建HTML页面的描述时,如果描述从模块导出为页(page),那么另一个#%module-begin可以帮助消除provide和准引用样板,就像在"html.rkt"所示:

"html.rkt"

#lang racket
(require racket/date)
 
(provide (except-out (all-from-out racket)
                     #%module-begin)
         (rename-out [module-begin #%module-begin])
         now)
 
(define-syntax-rule (module-begin expr ...)
  (#%module-begin
   (define page `(html expr ...))
   (provide page)))
 
(define (now)
  (parameterize ([date-display-format 'iso-8601])
    (date->string (seconds->date (current-seconds)))))

使用"html.rkt"模块语,可以描述一个简单的网页,而无需显式定义或导出页,并以quasiquote模式而不是表达式模式开始:

> (module lady-with-the-spinning-head "html.rkt"
    (title "Queen of Diamonds")
    (p "Updated: " ,(now)))
> (require 'lady-with-the-spinning-head)
> page

'(html (title "Queen of Diamonds") (p "Updated: " "2022-11-14"))

17.1.2 使用#lang s-exp

在#lang级别实现语言比声明单个模块更复杂,因为#lang允许程序员控制语言的多个不同方面。然而,s-exp语言作为一种元语言,使用带#lang简写的模块语言:

#lang s-exp module-name
form ...

等同于

(module name module-name
  form ...)

其中name源自包含#lang程序的源文件。名称s-exp是S-expression的缩写,它是读取器级词汇约定的传统名称:括号、标识、数字、带反斜杠转义的双引号字符串等等。

使用#lang s-exp,前面的lady-with-the-spinning-head例子可以写得更简洁:

#lang s-exp "html.rkt"
 
(title "Queen of Diamonds")
(p "Updated: " ,(now))

在这个指南的稍后边,《定义新的#lang语言》会讲解如何定义自己的#lang语言,但是首先我们讲解你如何写针对Racket读取器(reader)级的扩展。

 

17.2 读取器扩展

(part ("(lib scribblings/reference/reference.scrbl)" "parse-reader")) in The Racket Reference provides more on 读取器扩展.

Racket语言的读取器(reader)层可以通过#reader表进行扩展。读取器扩展实现为一个以#reader命名的模块。该模块导出将原始字符解析为扩展器层使用的表的函数。

#reader的语法是

#reader ‹module-path› ‹reader-specific

其中,‹module-path›命名了一个模块,该模块提供read和read-syntax函数。‹reader-specific›部分是由‹module-path›中的read和read-syntax函数确定的字符序列。

例如,假设文件"five.rkt"包含

"five.rkt"

#lang racket/base
 
(provide read read-syntax)
 
(define (read in) (list (read-string 5 in)))
(define (read-syntax src in) (list (read-string 5 in)))

那么,程序

#lang racket/base
 
'(1 #reader"five.rkt"234567 8)

等价于

#lang racket/base
 
'(1 ("23456") 7 8)

因为"five.rkt"的read和read-syntax函数都从输入流中读取五个字符,并把它们放入字符串,然后放入列表。"five.rkt"中的读取器函数不必遵循Racket词法约定,将连续序列234567视为单个数字。由于只有23456部分被read或read-syntax使用,所以7仍然需要以通常的Racket方式进行解析。类似地,"five.rkt"中的读取器函数不必忽略空白,并且

#lang racket/base
 
'(1 #reader"five.rkt" 234567 8)

等价于

#lang racket/base
 
'(1 (" 2345") 67 8)

因为紧跟"five.rkt"后面的第一个字符是空格。

REPL中也可以使用#reader表:

> '#reader"five.rkt"abcde

'("abcde")

17.2.1 源位置

read和read-syntax的区别在于,read用于数据,而 read-syntax用于解析程序。更准确地说,当通过Racket的read解析封闭流时,将使用read函数,当Racket的read-syntax函数解析封闭流时,使用read-syntax。没有什么需要read和read-syntax用同样的方式解析输入,但使它们不同会混淆程序员和工具。

read-syntax函数可以返回与read相同类型的值,但它通常应该返回一个语法对象(syntax object),该对象将解析的表达式与源位置连接起来。与"five.rkt"示例不同,read-syntax函数通常直接实现以生成语法对象,然后read可以使用read-syntax并去掉语法对象包装来产生原始结果。

下面的"arith.rkt"模块实现了一个读取器,用于将简单的中缀算术表达式解析为Racket表。例如,1*2+3解析为Racket表(+ (* 1 2) 3)。支持的运算符是+、-、*和/,而操作数可以是无符号整数或单字母变量。该实现使用port-next-location获取当前源位置,并使用 datum->syntax将原始值转换为语法对象。

"arith.rkt"

#lang racket
(require syntax/readerr)
 
(provide read read-syntax)
 
(define (read in)
  (syntax->datum (read-syntax #f in)))
 
(define (read-syntax src in)
  (skip-whitespace in)
  (read-arith src in))
 
(define (skip-whitespace in)
  (regexp-match #px"^\\s*" in))
 
(define (read-arith src in)
  (define-values (line col pos) (port-next-location in))
  (define expr-match
    (regexp-match
     ; Match an operand followed by any number of
     ; operator–operand sequences, and prohibit an
     ; additional operator from following immediately:
     #px"^([a-z]|[0-9]+)(?:[-+*/]([a-z]|[0-9]+))*(?![-+*/])"
     in))
 
  (define (to-syntax v delta span-str)
    (datum->syntax #f v (make-srcloc delta span-str)))
  (define (make-srcloc delta span-str)
    (and line
         (vector src line (+ col delta) (+ pos delta)
                 (string-length span-str))))
 
  (define (parse-expr s delta)
    (match (or (regexp-match #rx"^(.*?)([+-])(.*)$" s)
               (regexp-match #rx"^(.*?)([*/])(.*)$" s))
      [(list _ a-str op-str b-str)
       (define a-len (string-length a-str))
       (define a (parse-expr a-str delta))
       (define b (parse-expr b-str (+ delta 1 a-len)))
       (define op (to-syntax (string->symbol op-str)
                             (+ delta a-len) op-str))
       (to-syntax (list op a b) delta s)]
      [_ (to-syntax (or (string->number s)
                        (string->symbol s))
                    delta s)]))
 
  (unless expr-match
    (raise-read-error                              "错误的算术表达式"
                      src line col pos
                      (and pos (- (file-position in) pos))))
  (parse-expr (bytes->string/utf-8 (car expr-match)) 0))

如果在表达式位置使用"arith.rkt"读取器,则其解析结果将被视为Racket表达式。但是,如果它以引号形式使用,那么它只生成一个数字或一个列表:

> #reader"arith.rkt" 1*2+3

5

> '#reader"arith.rkt" 1*2+3

'(+ (* 1 2) 3)

"arith.rkt"读取器也可以用于毫无意义的位置。由于read-syntax实现跟踪源位置,语法错误至少可以根据其原始位置(在错误消息的开头)引用部分输入:

> (let #reader"arith.rkt" 1*2+3 8)

repl:1:27: let: bad syntax (not an identifier and expression

for a binding)

  at: +

  in: (let (+ (* 1 2) 3) 8)

17.2.2 可读表

读取器扩展以任意方式解析输入字符的能力可能是很强大,但许多词汇扩展需要一种不太通用但更易于组合的方法。与扩展器级别的Racket语法可以通过宏扩展的方式大致相同,读取器级别的Racket语法可以通过可读表(readtable)进行组合扩展。

例如,默认的Racket读取器是递归下降的解析器,可读表将字符映射到解析处理程序。例如,默认可读表将映射到一个处理程序,该处理程序递归解析子表,直到找到一个)。current-readtable的参数确定了readread-syntax使用的可读表。而不是直接解析原始字符,读取器扩展可以安装一个扩展的可读表,然后链接到readread-syntax

有关《parameters》的介绍,请参见《动态绑定:parameterize》。

make-readtable函数构造了一个新的可读表,作为现有可读表的扩展。它接受字符、字符映射类型和(特定类型的映射)解析程序方面的一系列规范。例如,要扩展可读表,以便$可以用于开始和结束中缀表达式,请实现一个read-dollar函数并使用:

(make-readtable (current-readtable)
                #\$ 'terminating-macro read-dollar)

read-dollar的协议要求函数接受不同数量的参数,这取决于它是否在read模式下使用还是在read-syntax模式下使用。在read模式下,解析器函数有两个参数:触发解析器的字符和正在读取的输入端口。在read-syntax模式下,函数必须接受四个额外的参数,以提供字符的源位置。

下面的"dollar.rkt"模块根据被"arith.rkt"提供的read和read-syntax函数定义了一个read-dollar函数,并将read-dollar与新的read和read-syntax函数放在一起,这些函数安装了可读表e并将其链接到Racket的readread-syntax

"dollar.rkt"

#lang racket
(require syntax/readerr
         (prefix-in arith: "arith.rkt"))
 
(provide (rename-out [$-read read]
                     [$-read-syntax read-syntax]))
 
(define ($-read in)
  (parameterize ([current-readtable (make-$-readtable)])
    (read in)))
 
(define ($-read-syntax src in)
  (parameterize ([current-readtable (make-$-readtable)])
    (read-syntax src in)))
 
(define (make-$-readtable)
  (make-readtable (current-readtable)
                  #\$ 'terminating-macro read-dollar))
 
(define read-dollar
  (case-lambda
   [(ch in)
    (check-$-after (arith:read in) in (object-name in))]
   [(ch in src line col pos)
    (check-$-after (arith:read-syntax src in) in src)]))
 
(define (check-$-after val in src)
  (regexp-match #px"^\\s*" in) ; skip whitespace
  (let ([ch (peek-char in)])
    (unless (equal? ch #\$) (bad-ending ch src in))
    (read-char in))
  val)
 
(define (bad-ending ch src in)
  (let-values ([(line col pos) (port-next-location in)])
    ((if (eof-object? ch)
         raise-read-error
         raise-read-eof-error)
     "expected a closing `$'"
     src line col pos
     (if (eof-object? ch) 0 1))))

使用此读取器扩展,可以在表达式的开头使用单个#reader,以启用切换到中缀运算的$的多种用法:

> #reader"dollar.rkt" (let ([a $1*2+3$] [b $5/6$]) $a+b$)

35/6

 

17.3 定义新的#lang语言

将模块作为启动的源程序加载时

#lang language

language决定了在读取器级别解析模块其余部分的方式。读取器级别解析必须生成一个module表作为语法对象。与以往一样,module后面的第二个子表指定了控制模块主体表含义的模块语言(module language)。因此,在language之后指定的#lang控制模块的读取器级别和扩展器级别解析。

 

17.3.1 指定#lang语言

language的语法有意与require模块语言(module language)中使用的模块路径的语法重叠,因此像racket、racket/base、slideshow或scribble/manual这样的名称既可以用作#lang语言,也可用作模块路径。

同时,language的语法比模块路径更受限制,因为只有a-z、A-Z、0-9、/(不在开头或结尾)、language名称中允许使用_、-和+。这些限制使#lang语法尽可能简单。反过来,保持#lang语法简单也很重要,因为语法本身就不灵活且不可扩展;#lang协议允许language以一种几乎不受约束的方式细化和定义语法,但#lang协议本身必须保持固定,以便各种不同的工具能够“引导”到扩展世界。

幸运的是,#lang协议提供了一种自然的方式来引用语言,而不是严格的language语法:通过定义一个实现自己的嵌套协议的language。我们已经看到了一个例子(在《使用#lang s-exp》里):s-exp的language允许程序员使用通用的模块路径语法指定模块语言。同时,s-exp负责#lang语言的读取器级别职责。

不同于racket,s-exp不能作用带有require的模块路径。尽管#lang的language语法与模块路径语法重叠,但language不能直接用作模块路径。相反,language通过尝试两个位置获得模块路径:首先,它为language查找主模块的读取器子模块。如果这不是有效的模块路径,那么language将用/lang/reader作为后缀。(如果两者都不是有效的模块路径,则会引发错误。)生成的模块使用与#reader类似的协议提供read和read-syntax函数。

读取器扩展》介绍了#reader.

将#lang的language转换为模块路径的一个结果是,该语言必须安装在集合(collection)中,类似于"racket"或"slideshow"是随Racket分发的集合。然而,还有一个例外:reader语言允许你使用通用模块路径指定语言的读取器级实现。

 

17.3.2 使用#lang reader

#lang的reader语言类似于s-exp,因为它是一种元语言。s-exp允许程序员在扩展器解析层指定模块语言,而reader允许程序员指定读取器层的语言。

#lang reader后面必须有一个模块路径,并且指定的模块必须提供两个函数:read和read-syntax。该协议与#reader实现的协议相同,但对于#lang,read和read-syntax函数必须生成一个基于模块输入文件其余部分的module表。

下面的"literal.rkt"模块实现了一种语言,该语言将其整个正文视为文本,并将文本导出为数据字符串:

"literal.rkt"

#lang racket
(require syntax/strip-context)
 
(provide (rename-out [literal-read read]
                     [literal-read-syntax read-syntax]))
 
(define (literal-read in)
  (syntax->datum
   (literal-read-syntax #f in)))
 
(define (literal-read-syntax src in)
  (with-syntax ([str (port->string in)])
    (strip-context
     #'(module anything racket
         (provide data)
         (define data 'str)))))

"literal.rkt"语言在生成的module表达式上使用strip-context,因为read-syntax函数应该返回一个没有词法上下文的语法对象。此外,"literal.rkt"语言创建了一个名为anything的模块,这是一个随意的选择;该语言旨在在文件中使用,当普通书写的模块名称出现在require文件中时,它将被忽略。

"literal.rkt"语言可以在模块中使用"tuvalu.rkt":

"tuvalu.rkt"

#lang reader "literal.rkt"
Technology!
System!
Perfect!

导入"tuvalu.rkt"将data绑定到模块内容的字符串版本:

> (require "tuvalu.rkt")
> data

"\nTechnology!\nSystem!\nPerfect!\n"

 

17.3.3 使用#lang s-exp syntax/module-reader

解析模块主体通常不像"literal.rkt"中那样简单。更典型的模块解析器必须迭代以解析模块体的多个表单。一种语言也更有可能扩展Racket语法——也许通过readtable——而不是完全替换Racket句法。

syntax/module-reader模块语言对语言实现的公共部分进行抽象,以简化新语言的创建。在最基本的形式中,用syntax/module-reader实现的语言只指定了该语言使用的模块语言,在这种情况下,该语言的读取器层与Racket相同。例如,使用

"raquet-mlang.rkt"

#lang racket
(provide (except-out (all-from-out racket) lambda)
         (rename-out [lambda function]))

以及

"raquet.rkt"

#lang s-exp syntax/module-reader
"raquet-mlang.rkt"

那么

#lang reader "raquet.rkt"
(define identity (function (x) x))
(provide identity)

由于"raquet-mlang.rkt"将lambda导出为function,因此实现并导出identity函数。

syntax/module-reader语言接受许多可选的规范来调整语言的其它特性。例如,替换的read和read-syntax解析语言可以使用#:read和#:read-syntax。以下内容"dollar-racket.rkt"语言使用"dollar.rkt"(见《可读表》)来构建类似racket,但带有$转义为简单中缀算术:

"dollar-racket.rkt"

#lang s-exp syntax/module-reader
racket
#:read $-read
#:read-syntax $-read-syntax
 
(require (prefix-in $- "dollar.rkt"))

require表出现在模块的末尾,因为syntax/module-reader的所有关键字标记的可选规范必须出现在任何助手导入或定义之前。

下面的模块使用"dollar-racket.rkt"来使用$转义实现cost函数:

"store.rkt"

#lang reader "dollar-racket.rkt"
 
(provide cost)
 
; Cost of ‘n' $1 rackets with 7% sales
; tax and shipping-and-handling fee ‘h':
(define (cost n h)
  $n*107/100+h$)

 

17.3.4 安装语言

到目前为止,我们已经使用reader元语言来访问"literal.rkt"和"dollar-racket.rkt"。如果你想直接使用#lang literal之类的,那你必须把"literal.rkt"移到名为"literal"的Racket集合中(另请参见《添加集合》)。具体而言,将"literal.rkt"移到任何目录名"literal/main.rkt"的reader子模块中,如下所示:

"literal/main.rkt"

#lang racket
 
(module reader racket
  (require syntax/strip-context)
 
  (provide (rename-out [literal-read read]
                       [literal-read-syntax read-syntax]))
 
  (define (literal-read in)
    (syntax->datum
     (literal-read-syntax #f in)))
 
  (define (literal-read-syntax src in)
    (with-syntax ([str (port->string in)])
      (strip-context
       #'(module anything racket
           (provide data)
           (define data 'str))))))

然后,将"literal"目录作为包安装:

  cd /path/to/literal ; raco pkg install

移动文件并安装包后,可以在#lang后面直接使用literal。

#lang literal
Technology!
System!
Perfect!

有关使用宏的详细信息参见《(part ("(lib scribblings/raco/raco.scrbl)" "top"))》。

你还可以使用Racket包管理器(参见《(part ("(lib pkg/scribblings/pkg.scrbl)" "top"))》)将您的语言提供给其他人安装。创造"literal"包并将其注册到Racket包目录(请参见《(part ("(lib pkg/scribblings/pkg.scrbl)" "concept:catalog"))》)后,其他人就可以使用raco pkg安装它:

  raco pkg install literal

安装后,其他人可以用同样的方式调用该语言:在源文件的顶部使用#lang literal。

如果你使用公共源代码库(例如GitHub),你可以将你的包链接到源代码。当你改进这个包,其他人可以使用raco pkg更新其的版本:

  raco pkg update literal

了解有关Racket包管理器的更多信息,请参见(part ("(lib pkg/scribblings/pkg.scrbl)" "top"))。

 

17.3.5 源处理配置

Racket发行版包括用于编写平常文档的Scribble语言,其中Scribble扩展了普通Racket以更好地支持文本。下面是一个示例Scribble文档:

 

  #lang scribble/base

  

  @(define (get-name) "Self-Describing Document")

  

  @title[(get-name)]

  

  The title of this document is ``@(get-name).''

 

如果你将该程序放在DrRacket的定义区域中并单击Run,那么是乎不会发生什么。scribble/base语言只绑定并导出doc作为文档的描述,类似于"literal.rkt"将字符串导出为data的方式。

然而,只需在DrRacket中打开一个带有scribble/base语言的模块,就会出现一个Scribble HTML按钮。此外,DrRacket知道如何通过将文档中与文字相对应的部分着色为绿色来为Scribble语法着色。语言名称scribble/base不是硬连接的DrRacket。相反,scribble/base语言的实现提供了按钮和语法着色信息,以响应DrRacket的查询。

如果你已经安装了安装语言中描述的literal语言,那你可以调整"literal/main.rkt",以便DrRacket将literal语言中的模块内容视为纯文本,而不是(错误地)视为Racket语法:

"literal/main.rkt"

#lang racket
 
(module reader racket
  (require syntax/strip-context)
 
  (provide (rename-out [literal-read read]
                       [literal-read-syntax read-syntax])
           get-info)
 
  (define (literal-read in)
    (syntax->datum
     (literal-read-syntax #f in)))
 
  (define (literal-read-syntax src in)
    (with-syntax ([str (port->string in)])
      (strip-context
       #'(module anything racket
           (provide data)
           (define data 'str)))))
 
  (define (get-info in mod line col pos)
    (lambda (key default)
      (case key
        [(color-lexer)
         (dynamic-require 'syntax-color/default-lexer
                          'default-lexer)]
        [else default]))))

这个修改后的literal实现提供了一个get-info函数。get-info函数由read-language(DrRacket调用)源输入流和位置信息,以防查询结果应取决于语言名称后面的模块内容(literal不是这样)。get-info的结果是两个参数的函数。第一个参数总是一个符号,表示工具从语言中请求的信息类型;如果语言无法识别查询或没有查询信息,则第二个参数是返回的默认结果。

DrRacket获得一种语言的get-info结果后,它使用'color-lexer查询调用该函数;结果应该是一个在输入流上实现语法着色解析的函数。对于literal,syntax-color/default-lexer模块提供了一个适用于纯文本的default-lexer语法着色解析器,因此literal加载并返回该解析器以响应'color-lexer查询。

编程工具用于查询的符号集完全在工具和选择与之合作的语言之间。例如,除了'color-lexer之外,DrRacket还使用'drracket:toolbar-buttons查询来确定工具栏中哪些按钮可以使用该语言在模块上操作。

syntax/module-reader语言允许你通过#:info可选规范指定get-info处理。#:info函数的协议与原始get-info协议略有不同;修订后的协议允许syntax/module-reader自动处理未来的语言信息查询。

 

17.3.6 模块处理配置

假设文件"death-list-5.rkt"包含

"death-list-5.rkt"

#lang racket
(list "O-Ren Ishii"
      "Vernita Green"
      "Budd"
      "Elle Driver"
      "Bill")

如果你直接require "death-list-5.rkt",那么它会以通常的Racket结果格式打印列表:

> (require "death-list-5.rkt")

'("O-Ren Ishii" "Vernita Green" "Budd" "Elle Driver" "Bill")

但是,如果"death-list-5.rkt"是由"kiddo.rkt"所要求的,该"kiddo.rkt"是用scheme而不是用racket实现的:

"kiddo.rkt"

#lang scheme
(require "death-list-5.rkt")

然后,如果你在DrRacket中运行"kiddo.rkt"文件,或者直接使用racket运行该文件,"kiddo.rkt"导致"death-list-5.rkt"以传统的Scheme格式打印其列表,而不是使用前导引号:

("O-Ren Ishii" "Vernita Green" "Budd" "Elle Driver" "Bill")

"kiddo.rkt"示例说明了打印结果值的格式如何依赖于程序的主模块而不是用于实现它的语言。

更广泛地说,只有当用一种语言编写的模块直接用racket运行(而不是导入导另一个模块中),才会调用该语言的某些特性。一个例子是结果打印样式(如上所示)。另一个例子是REPL行为。这些特性是语言的运行时配置(run-time configuration)的一部分。

与语言的语法着色属性(如《源处理配置》中所描述的),运行时配置本身是模块的属性,而不是表示模块的源文本属性。为此,即使模块被编译成字节码形式并且源代码不可用,模块的运行时配置也需要可用。因此,运行时配置不能由我们从语言的解析器模块导出的get-info函数处理。

相反,它将由一个新的configure-runtime(配置运行时)子模块处理,我们将在解析后的module表中添加该子模块。当一个模块直接用racket运行,racket会查找一个configure-runtime子模块。如果它存在,racket就运行它。但是,如果将模块导入到另一个模块中,则会忽略'configure-runtime子模块。(如果'configure-runtime子模块不存在,racket会照常对模块求值。)这意味着'configure-runtime子模块可用于直接运行模块时需要执行的任何特殊设置任务。

回到literal语言(请参见《源处理配置》),我们可以调整语言,以便直接运行literal模块可以使其打印出字符串,而在更大的程序中使用literal模块只需提供data而不打印。为了完成这项工作,我们需要一个额外的模块。(为了清楚起见,我们将把这个模块实现为一个单独的文件现有文件的子模块。)

....                                                         (主要安装或用户空间)
|- "literal"
   |- "main.rkt"                                                (带读卡器子模块)
   |- "show.rkt"                              (新增)
  • "literal/show.rkt"模块将提供一个show函数以被应用到一个literal(文本)模块的字符串内容,同时也提供一个show-enabled参数,它控制是否show的实际打印结果。

  • "literal/main.rkt"中新的configure-runtime子模块将show-enabled参数设置为#t。最终的效果是,show将打印给定的字符串,但只有当使用literal语言的模块直接运行时(因为只有这样才能调用configure-runtime子模块)。

这些更改在以下修订的"literal/main.rkt"中实现:

"literal/main.rkt"

#lang racket
 
(module reader racket
  (require syntax/strip-context)
 
  (provide (rename-out [literal-read read]
                       [literal-read-syntax read-syntax])
           get-info)
 
  (define (literal-read in)
    (syntax->datum
     (literal-read-syntax #f in)))
 
  (define (literal-read-syntax src in)
    (with-syntax ([str (port->string in)])
      (strip-context
       #'(module anything racket
           (module configure-runtime racket
             (require literal/show)
             (show-enabled #t))
           (require literal/show)
           (provide data)
           (define data 'str)
           (show data)))))
 
  (define (get-info in mod line col pos)
    (lambda (key default)
      (case key
        [(color-lexer)
         (dynamic-require 'syntax-color/default-lexer
                          'default-lexer)]
        [else default]))))

然后"literal/show.rkt"模块必须提供show-enabled参数和show函数:

"literal/show.rkt"

#lang racket
 
(provide show show-enabled)
 
(define show-enabled (make-parameter #f))
 
(define (show v)
  (when (show-enabled)
    (display v)))

有了literal的所有片段后,尝试直接运行"tuvalu.rkt"的以下变体,并通过另一个模块中的require

"tuvalu.rkt"

#lang literal
Technology!
System!
Perfect!

当直接运行时,我们会看到这样打印的结果,因为我们的configure-runtime子模块将show-enabled参数设置为#t:

Technology!
System!
Perfect!

但是,当导入到另一个模块中时,打印将被抑制,因为不会调用configure-runtime子模块,因此show-enabled参数将保持其默认值#f。

 

18 并发与同步

Racket以线程(threads)的形式提供了并发(concurrency), 并且它提供了一个通用的sync(同步)函数,可以用于同步线程和其它隐式并发表,如端口(ports)。

线程并发运行的意义是,一个线程可以在不进行协作的情况下抢占另一个线程,但线程不会在使用多个硬件处理器的情况下并行运行。有关Racket中并行的信息,请参见《并行》。

18.1 线程

要同时执行一个过程,请使用thread(线程)。以下示例从主线程创建两个新线程:

(displayln "This is the original thread")
(thread (lambda () (displayln "This is a new thread.")))
(thread (lambda () (displayln "This is another new thread.")))

下一个示例创建了一个原本会无限循环的新线程,但主线程使用sleep使其自身暂停2.5秒,然后使用kill-thread终止了worker线程:

(define worker (thread (lambda ()
                         (let loop ()
                           (displayln "Working...")
                           (sleep 0.2)
                           (loop)))))
(sleep 2.5)
(kill-thread worker)

在DrRacket中,主线程一直运行,直到单击Stop(停止)按钮,因此在DrRacket中,不需要使用thread-wait(线程等待)。

如果主线程结束或被终止,即使其它线程仍在运行,应用程序也会退出。线程可以使用thread-wait来等待另一个线程完成。这里,主线程使用thread-wait来确保worker线程在主线程退出之前完成:

(define worker (thread
                 (lambda ()
                   (for ([i 100])
                     (printf "Working hard... ~a~n" i)))))
(thread-wait worker)
(displayln "Worker finished")

18.2 线程邮箱

每个线程都有一个用于接收消息的邮箱。thread-send函数异步地将消息发送到另一个线程的邮箱, 而thread-receive从当前线程的邮箱中返回最旧的消息,如果需要的话则阻塞以等待消息。在下面的示例中,主线程将数据发送给要处理的工作线程(worker thread),然后在没有更多数据时发送'done消息,并等待工作线程完成。

(define worker-thread (thread
                       (lambda ()
                         (let loop ()
                           (match (thread-receive)
                             [(? number? num)
                              (printf "Processing ~a~n" num)
                              (loop)]
                             ['done
                              (printf "Done~n")])))))
(for ([i 20])
  (thread-send worker-thread i))
(thread-send worker-thread 'done)
(thread-wait worker-thread)

在下一个示例中,主线程将工作委托给多个算术线程,然后等待接收结果。算术线程处理工作项,然后将结果发送到主线程。

(define (make-arithmetic-thread operation)
  (thread (lambda ()
            (let loop ()
              (match (thread-receive)
                [(list oper1 oper2 result-thread)
                 (thread-send result-thread
                              (format "~a + ~a = ~a"
                                      oper1
                                      oper2
                                      (operation oper1 oper2)))
                 (loop)])))))
 
(define addition-thread (make-arithmetic-thread +))
(define subtraction-thread (make-arithmetic-thread -))
 
(define worklist '((+ 1 1) (+ 2 2) (- 3 2) (- 4 1)))
(for ([item worklist])
  (match item
    [(list '+ o1 o2)
     (thread-send addition-thread
                  (list o1 o2 (current-thread)))]
    [(list '- o1 o2)
     (thread-send subtraction-thread
                  (list o1 o2 (current-thread)))]))
 
(for ([i (length worklist)])
  (displayln (thread-receive)))

18.3 信号

信号(Semaphores)有助于同步访问任意共享资源。当多个线程必须对单个资源执行非原子操作时,应该使用信号。

在下面的示例中,多个线程同时打印到标准输出。如果不同步,一个线程打印的行可能出现在另一个线程打印行的中间。通过使用一个用1计数初始化的信号,一次只能打印一个线程能。semaphore-wait函数会阻塞,直到信号的内部计数器为非零,然后递减计数器并返回。semaphore-post函数递增计数器,以便另一个线程可以解除阻塞,然后打印。

(define output-semaphore (make-semaphore 1))
(define (make-thread name)
  (thread (lambda ()
            (for [(i 10)]
              (semaphore-wait output-semaphore)
              (printf "thread ~a: ~a~n" name i)
              (semaphore-post output-semaphore)))))
(define threads
  (map make-thread '(A B C)))
(for-each thread-wait threads)

等待信号、工作和发布到信号量的模式也可以使用call-with-semaphore来表示,如果控制退出(例如,由于异常),它具有发布到信号的优势:

(define output-semaphore (make-semaphore 1))
(define (make-thread name)
  (thread (lambda ()
            (for [(i 10)]
              (call-with-semaphore
               output-semaphore
               (lambda ()
                (printf "thread ~a: ~a~n" name i)))))))
(define threads
  (map make-thread '(A B C)))
(for-each thread-wait threads)

信号是一种底层技术。通常,更好的解决方案是将资源访问限制为单个线程。例如,通过有一个用于打印输出的专用线程,可以更好地实现对标准输出的同步访问。

18.4 通道

当一个值从一个线程传递到另一个线程时,通道(Channels)同步两个线程。与线程邮箱不同,多个线程可以从单个通道获取项目,因此当多个线程需要从单个工作队列中接受项目时,应该使用通道。

在下面的示例中,主线程使用channel-put将项目添加到一个通道中,而多个工作线程使用channel-get来消耗这些项目。对任一过程的每个调用都会阻塞,直到另一个线程使用相同的通道调用另一个过程。worker处理这些项目,然后通过result-channel将结果传递给结果线程。

(define result-channel (make-channel))
(define result-thread
        (thread (lambda ()
                  (let loop ()
                    (displayln (channel-get result-channel))
                    (loop)))))
 
(define work-channel (make-channel))
(define (make-worker thread-id)
  (thread
   (lambda ()
     (let loop ()
       (define item (channel-get work-channel))
       (case item
         [(DONE)
          (channel-put result-channel
                       (format "Thread ~a done" thread-id))]
         [else
          (channel-put result-channel
                       (format "Thread ~a processed ~a"
                               thread-id
                               item))
          (loop)])))))
(define work-threads (map make-worker '(1 2)))
(for ([item '(A B C D E F G H DONE DONE)])
  (channel-put work-channel item))
(for-each thread-wait work-threads)

18.5 缓冲异步通道

缓冲异步通道与上述通道类似,但是异步通道的“放置(put)”操作不会阻塞,除非给定通道是用缓冲区限制创建的,并且已达到限制。因此,异步放置(asynchronous-put)操作有点儿类似于thread-send, 但与线程邮箱不同,异步通道允许多个线程从单个通道中消耗项目。

在下面的示例中,主线程将项目添加到工作通道(work channel),该通道一次最多容纳三个项目。工作线程(worker thread)处理来自此通道的项目,然后将结果发送到打印线程。

(require racket/async-channel)
 
(define print-thread
  (thread (lambda ()
            (let loop ()
              (displayln (thread-receive))
              (loop)))))
(define (safer-printf . items)
  (thread-send print-thread
               (apply format items)))
 
(define work-channel (make-async-channel 3))
(define (make-worker-thread thread-id)
  (thread
   (lambda ()
     (let loop ()
       (define item (async-channel-get work-channel))
       (safer-printf "Thread ~a processing item: ~a" thread-id item)
       (loop)))))
 
(for-each make-worker-thread '(1 2 3))
(for ([item '(a b c d e f g h i j k l m)])
  (async-channel-put work-channel item))

请注意,上面的例子缺少任何同步来验证是否处理了所有项目。如果主线程在没有同步的情况下退出,则工作线程可能无法完成某些项目的处理,或者打印线程无法打印所有项目。

18.6 可同步事件和sync

还有其它方法可以同步线程。sync函数允许线程通过同步事件(synchronizable events)进行协调。许多值兼作事件,允许以统一的方式使用不同类型同步过程。事件示例包括通道、端口、线程和警报。

在下一个例子中,通道和警报用作可同步事件。工作人员(workers)在两个频道上都sync,这样他们就可以处理通道项目,直到警报启动。处理通道项目,然后将结果发送回主线程。

(define main-thread (current-thread))
(define alarm (alarm-evt (+ 3000 (current-inexact-milliseconds))))
(define channel (make-channel))
(define (make-worker-thread thread-id)
  (thread
   (lambda ()
     (define evt (sync channel alarm))
     (cond
       [(equal? evt alarm)
        (thread-send main-thread 'alarm)]
       [else
        (thread-send main-thread
                     (format "Thread ~a received ~a"
                             thread-id
                             evt))]))))
(make-worker-thread 1)
(make-worker-thread 2)
(make-worker-thread 3)
(channel-put channel 'A)
(channel-put channel 'B)
(let loop ()
  (match (thread-receive)
    ['alarm
     (displayln "Done")]
    [result
     (displayln result)
     (loop)]))

下一个示例展示了一个用简单TCP回显服务器的函数。该函数使用sync/timeout来同步来自给定端口的输入或线程邮箱中的消息。sync/timeout的第一个参数指定了在给定事件上应该等待的最大秒数。当给定输入端口中有一行输入可用时,read-line-evt函数返回一个准备就绪的事件。当调用thread-receive不会阻塞时,thread-receive-evt的结果为准备就绪。在实际应用程序中,线程邮箱中接到的消息可用于控制消息,等等。

(define (serve in-port out-port)
  (let loop []
    (define evt (sync/timeout 2
                              (read-line-evt in-port 'any)
                              (thread-receive-evt)))
    (cond
      [(not evt)
       (displayln "Timed out, exiting")
       (tcp-abandon-port in-port)
       (tcp-abandon-port out-port)]
      [(string? evt)
       (fprintf out-port "~a~n" evt)
       (flush-output out-port)
       (loop)]
      [else
       (printf "Received a message in mailbox: ~a~n"
               (thread-receive))
       (loop)])))

下面的例子中使用了serve函数,它启动通过TCP通信的服务器线程和客户端线程。客户端将三行打印到服务器端,服务器端将其回传。客户端的copy-port调用会阻塞,直到收到EOF。服务器端在两秒钟后超时,关闭端口,这允许copy-port完成并退出客户端。主线程使用thread-wait等待客户端线程退出(因为如有thread-wait,主线程可能会在其它线程完成之前退出)。

(define port-num 4321)
(define (start-server)
  (define listener (tcp-listen port-num))
  (thread
    (lambda ()
      (define-values [in-port out-port] (tcp-accept listener))
      (serve in-port out-port))))
 
(start-server)
 
(define client-thread
  (thread
   (lambda ()
     (define-values [in-port out-port] (tcp-connect "localhost" port-num))
     (display "first\nsecond\nthird\n" out-port)
     (flush-output out-port)
     ; copy-port将阻塞,直到从端口中读取EOF
     (copy-port in-port (current-output-port)))))
 
(thread-wait client-thread)

有时,你希望将结果行为直接附加到传递给sync的事件上。在下面的示例中,工作线程在三个通道上同步,但每个通道必须以不同的方式处理。使用handle-evt将回调与给定事件相关联。当sync选择给定的事件时,它调用回调来生成同步结果,而不是使用事件的正常同步结果。由于事件是在回调中处理,因此不需要对sync的返回值进行调度。

(define add-channel (make-channel))
(define multiply-channel (make-channel))
(define append-channel (make-channel))
 
(define (work)
  (let loop ()
    (sync (handle-evt add-channel
                      (lambda (list-of-numbers)
                        (printf "Sum of ~a is ~a~n"
                                list-of-numbers
                                (apply + list-of-numbers))))
          (handle-evt multiply-channel
                      (lambda (list-of-numbers)
                        (printf "Product of ~a is ~a~n"
                                list-of-numbers
                                (apply * list-of-numbers))))
          (handle-evt append-channel
                      (lambda (list-of-strings)
                        (printf "Concatenation of ~s is ~s~n"
                                list-of-strings
                                (apply string-append list-of-strings)))))
    (loop)))
 
(define worker (thread work))
(channel-put add-channel '(1 2))
(channel-put multiply-channel '(3 4))
(channel-put multiply-channel '(5 6))
(channel-put add-channel '(7 8))
(channel-put append-channel '("a" "b"))

handle-evt的结果调用了其相对于sync的尾部回调,因此可以安全地使用递归,如下面的例子所示。

(define control-channel (make-channel))
(define add-channel (make-channel))
(define subtract-channel (make-channel))
(define (work state)
  (printf "Current state: ~a~n" state)
  (sync (handle-evt add-channel
                    (lambda (number)
                      (printf "Adding: ~a~n" number)
                      (work (+ state number))))
        (handle-evt subtract-channel
                    (lambda (number)
                      (printf "Subtracting: ~a~n" number)
                      (work (- state number))))
        (handle-evt control-channel
                    (lambda (kill-message)
                      (printf "Done~n")))))
 
(define worker (thread (lambda () (work 0))))
(channel-put add-channel 2)
(channel-put subtract-channel 3)
(channel-put add-channel 4)
(channel-put add-channel 5)
(channel-put subtract-channel 1)
(channel-put control-channel 'done)
(thread-wait worker)

wrap-evt函数类似于handle-evt,只是它的处理程序不是在相对于sync的尾部被调用。同时,wrap-evt在其处理程序调用期间禁用中断(break)异常。

 

19 性能

艾伦·珀利斯(Alan Perlis)有一句著名的话:“Lisp程序员知道一切的价值,而不知道一切的代价(Lisp programmers know the value of everything and the cost of nothing)。”比如,一个Racket程序员知道,程序中任何地方的lambda都会生成一个在其词法环境中封闭的值——但是分配这个值要多少代价呢?尽管大多数程序员对机器级别的各种操作和数据结构的成本有合理的把握,但Racket语言模型和计算机底层之间的差距可能相当大。

本章,我们通过解释Racket编译器和运行时系统的细节以及它们如何影响Racket代码的运行时间和内存性能来缩小这一差距。

19.1 DrRacket中的性能

默认情况下,DrRacket对程序进行调试,而调试工具(由(part ("(lib errortrace/scribblings/errortrace.scrbl)" "top"))库所提供)会显著地降低某些程序的性能。即使通过Choose Language...对话框的Show Details(显示详细信息)面板被禁用调试,默认情况下也会单击Preserve stacktrace复选框,这也会影响性能。禁用调试和堆栈跟踪保留提供了与在纯racket中运行更一致的效果。

即便如此,DrRacket和在DrRacket中开发的程序使用同一个Racket虚拟机, 因此在DrRacket中垃圾收集时间(请参见《内存管理》)可能比程序本身运行时间更长,并且DrRacket线程可能会妨碍程序线程的执行。要获得程序最可靠的的计时结果,请在普通的racket中运行而不是在DrRacket开发环境中运行。应使用非交互式模式而不是REPL,以便受益于模块系统。详情请参见《模块和性能》。

19.2 字节码和实时(JIT)编译器

每个要被Racket求值的定义或表达式都被编译成内部字节码格式。在交互模式下,此编译会自动进行且在运行中进行。像raco make和raco setup这样的工具将编译的字节码编组到一个文件中,这样你就不必每次运行程序时都从源代码进行编译。(编译文件所需的大部分时间实际上花费在宏展开中;从完全展开的代码生成字节码是比较快的。)有关生成字节码文件的详细信息,请参见《同时编译和配置:raco》。

字节码编译器应用所有标准优化,比如常量传输、常量折叠、内联和死代码消除。例如,在+具有其通常绑定的环境中,表达式(let ([x 1] [y (lambda () 4)]) (+ 1 (y)))的编译与常量5相同。

在某些平台上,字节码通过just-in-timeJIT编译器进一步编译成本机代码。JIT编译器大大加快了执行紧凑循环、小整数算法以及不精确实数算法的程序。目前,x86、x86_64(也称为AMD64)、ARM和32位PowerPC处理器支持JIT编译。JIT编译器可以通过racket的eval-jit-enabled参数或racket的--no-jit/-j命令行标志禁用。

JIT编译器在应用函数时以增量方式工作,但JIT编译器在编译过程时仅有限地使用运行时信息,因为给定的模块主体或lambda抽象只编译一次。JIT的编译粒度是单个过程主体,不包含任何词汇嵌套过程的主体。JIT编译的开销通常很小,难以检测。

19.3 模块和性能

模块系统通过帮助确保标识具有通常的绑定来帮助优化。也就是说,编译器可以识别racket/base提供的+并进行内联,这对JIT编译的代码尤为重要。相反,在传统的交互式Scheme系统中,顶级的+绑定可能会被重新定义,因此编译器不能假定固定的+绑定(除非使用特殊标志或声明来弥补模块系统的不足)。

即使在顶级环境中,使用require导入也可以实现一些内联优化。尽管顶层的+定义可能会对导入的+进行覆盖,但覆盖定义仅适用于稍后求值的表达式。

在一个模块中,内联和常量传播优化还利用了这样一个事实,即当没有set!时,模块中的定义不能发生变化在编译时可见。此类优化在顶级环境中不可用。尽管模块内的这种优化对性能很重要,但它阻碍了某些形式的交互开发和探索。当交互式探索更重要时,compile-enforce-module-constants参数禁用JIT编译器关于模块定义的假设。有关更多信息,请参阅《赋值和重定义》。

编译器可以内联函数或跨模块边界传播常量。为了避免在函数内联的情况下生成过多的代码,编译器在选择跨模块内联的候选时是保守的;有关向编译器提供内联提示的信息,请参阅《函数调用优化》。

后边《letrec性能》部分提供一些关于模块绑定内联的附加说明。

19.4 函数调用优化

当编译器检测到对即时可见函数的函数调用时,它会生成比泛型调用更高效的代码,尤其是尾部调用。例如,给定程序

(letrec ([odd (lambda (x)
                (if (zero? x)
                    #f
                    (even (sub1 x))))]
         [even (lambda (x)
                 (if (zero? x)
                     #t
                     (odd (sub1 x))))])
  (odd 40000000))

编译器可以检测到odd——even循环并通过循环展开和相关优化生成运行速度更快的代码。

在模块表里,define变量在词汇上的作用域类似于letrec绑定,因此模块中的定义允许调用优化,因此

(define (odd x) ....)
(define (even x) ....)

在一个模块中,它将执行与letrec版本相同的操作。

对于直接调用带有关键字参数的函数,编译器通常可以静态检查关键字参数,并生成对函数的非关键字变量的直接调用,这减少了关键字检查的运行时开销。此优化仅适用于与define绑定的关键字接受过程。

对于对足够小的函数的即时调用,编译器可以通过用函数主体替换调用来内联函数调用。除了目标函数体的大小之外,编译器的试探法还考虑了在调用位置已经执行的内联数量,以及被调用函数本身是否调用了简单基元操作以外的函数。当编译模块时,在模块级定义的一些函数被确定为内联到其它模块的候选函数;通常情况下,只有足够小的函数才被认为是跨模块内联的候选函数,但程序员可以用begin-encourage-inline包装函数定义,以鼓励函数内联。

pair?carcdr这样的基础操作是在机器代码级被JIT编译器内联的。有关内联运算活动的信息,请参见后面的《Fixnum和Flonum优化》。

19.5 突变和性能

利用set!突变变量可能导致性能下降。例如,小规模基准测试

#lang racket/base
 
(define (subtract-one x)
  (set! x (sub1 x))
  x)
 
(time
  (let loop ([n 4000000])
    (if (zero? n)
        'done
        (loop (subtract-one n)))))

运行速度比同等速度慢得多

#lang racket/base
 
(define (subtract-one x)
  (sub1 x))
 
(time
  (let loop ([n 4000000])
    (if (zero? n)
        'done
        (loop (subtract-one n)))))

在第一个变量中,每次迭代都会为x分配一个新位置,导致性能不佳。在第一个例子中,一个更聪明的编译器可以破解set!的用法,由于不鼓励突变(参见《使用赋值的指导原则》),编译器的努力就花在了其它地方。

更重要的是,突变可能会模糊绑定,否则可能会应用内联和常量传播。例如,在

(let ([minus1 #f])
  (set! minus1 sub1)
  (let loop ([n 4000000])
    (if (zero? n)
        'done
        (loop (minus1 n)))))

set!掩盖了minus1只是内置的sub1的另一个名字的事实。

19.6 letrec性能

letrec仅用于绑定过程和文本时,编译器可以以最佳方式处理绑定,有效地编译绑定的使用。当其它类型的绑定与过程混合时,编译器可能无法能确定控制流。

例如,

(letrec ([loop (lambda (x)
                (if (zero? x)
                    'done
                    (loop (next x))))]
         [junk (display loop)]
         [next (lambda (x) (sub1 x))])
  (loop 40000000))

可能编译成比以下内容效率更低的代码

(letrec ([loop (lambda (x)
                (if (zero? x)
                    'done
                    (loop (next x))))]
         [next (lambda (x) (sub1 x))])
  (loop 40000000))

在第一种情况下,编译器可能不知道display没有调用loop。如果是,那么loop可能会在绑定之前引用next是可获得的。

关于letrec的这个警告也适用于作为内部定义或模块中的函数和常量的定义。模块主体中的定义序列类似于letrec绑定的序列,模块主体中非常量表达式可能会干扰对后面绑定的引用的优化。

19.7 Fixnum和Flonum优化

fixnum是一个小的精确整数。在这种情况下,“小”取决于平台。对于32位机器,可以用30位加上一个符号位表示的数字代表为fixnum。在64位机器上,62位加上一个符号位是有效的。

flonum用来表示任何不精确的实数。它们对应于所有平台上的64位IEEE浮点数。

内联fixnum和flonum算术运算是JIT编译器最重要的优点之一。例如,当+应用于两个参数时,生成的机器代码测试这两个参数是否为fixnum,如果是,则使用机器的指令将数字相加(并检查溢出)。如果这两个数字不是fixnum,则检查是否两者都是flonum;在这种情况下,计算机的浮点直接使用操作。对于采用任意数量的参数的函数,如+,当参数全部为fixnum或全部为flonum时,内联适用于两个或多个参数(除了-,其一个参数大小写也是内联的)。

flonum通常是盒装的(boxed),这意味着分配内存来保存flonum计算的每一个结果。幸运的是,分代垃圾回收器(稍后在《内存管理》中描述)使短期结果的分配开销相当小。相比之下,fixnum从不装盒,因此通常使用起来开销都很小。

有关flonum特定操作的示例用法,请参见《前程并行》。

racket/flonum库提供了flonum特定的操作,flonum操作的组合允许JIT编译器生成避免装盒和拆盒中间结果的代码。除了即时组合中的结果外,与let绑定并由后续特定于flonum的操作使用的特定于flonum的结果在临时存储中未装盒。最后,编译器可以检测一些flonum值循环累加器并避免累加器装盒。字节码反编译程序(见《(part ("(lib scribblings/raco/raco.scrbl)" "decompile"))》)注释JIT可以避免使用#%flonum、#%as-flonum和#%from-flonum盒子。

PowerPC的JIT不支持局部绑定和累加器的装盒。

racket/unsafe/ops库提供了未经检查的fixnum和flonum特定操作。未选中的flonum特定操作允许取消装盒,有时它们允许编译器重新排序表达式以提高性能。另请参见《未检查、不安全的操作》,尤其是关于不安全的警告。

19.8 未检查、不安全的操作

racket/unsafe/ops库提供的函数与racket/base中的其它函数类似,但它们假定(而不是检查)提供的参数类型正确。例如,unsafe-vector-ref从一个向量中访问一个元素,而不检查它的第一个参数实际上是一个向量,也不检查给定的索引是否在边界内。对于使用这些函数的紧凑循环,避免检查有时可以加快计算速度,尽管不同的未检查函数和不同上下文有不同的好处。

请注意,正如库和函数名称中的“不安全(unsafe)”所暗示的那样,错误使用racket/unsafe/ops的导出可能会导致崩溃或内存损坏。

19.9 外部指针

ffi/unsafe库提供了不安全读取和写入任意指针值的函数。JIT识别ptr-refptr-set!的用法,其中第二个参数是对以下内置C类型之一的直接引用:_int8_int16_int32_int64、 _double_float_pointer。然后,如果第一个参数是ptr-refptr-set!是C指针(不是字节字符串),则指针的读取或写入在生成的代码中内联执行。

字节码编译器会优化对整数缩写的引用,如_int到C类型(如_int32),其中表示大小在平台之间是恒定的,因此JIT可以专门访问这些C类型。C类型(如_long_intptr)在不同平台上是不恒定的,因此它们的使用目前没有被JIT专门化。

使用_float_double进行指针读取和写入目前不受拆盒优化的影响。

19.10 正则表达式性能

当一个字符串或字节字符串被提供给regexp-match这样的函数时,该字符串将在内部编译为regexp值。与其多次提供字符串或字节字符串作为匹配模式,不如使用regexpbyte-regexppregexp或 byte-pregexp将模式编译为regexp值。代替常量字符串或字节字符串,使用#rx或#px前缀编写常量regexp。

(define (slow-matcher str)
  (regexp-match? "[0-9]+" str))
 
(define (fast-matcher str)
  (regexp-match? #rx"[0-9]+" str))
 
(define (make-slow-matcher pattern-str)
  (lambda (str)
    (regexp-match? pattern-str str)))
 
(define (make-fast-matcher pattern-str)
  (define pattern-rx (regexp pattern-str))
  (lambda (str)
    (regexp-match? pattern-rx str)))

19.11 内存管理

Racket实现有两种变体:3m和CGC。3m变体使用了一个现代的分代垃圾收集器,使得对短期对象的分配开销相对较小。CGC变体使用了一个保守垃圾收集器,它以牺牲Racket内存管理的精度和速度为代价,促进与C代码的交互。3m变体是标准变型。

虽然内存分配开销相当小,但完全避免分配通常更快。有时可以避免分配的一个特定位置是closures(闭包),它包含自由变量函数的运行时表示。例如,

(let loop ([n 40000000] [prev-thunk (lambda () #f)])
  (if (zero? n)
      (prev-thunk)
      (loop (sub1 n)
            (lambda () n))))

在每次迭代时分配一个闭包,因为(lambda () n)有效地保护了n。

编译器可以自动清除许多闭包。例如,在

(let loop ([n 40000000] [prev-val #f])
  (let ([prev-thunk (lambda () n)])
    (if (zero? n)
        prev-val
        (loop (sub1 n) (prev-thunk)))))

从来没有为prev-thunk分配闭包,因为它的唯一应用程序是可见的,因此它是内联的。同样,在

(let n-loop ([n 400000])
  (if (zero? n)
      'done
      (let m-loop ([m 100])
        (if (zero? m)
            (n-loop (sub1 n))
            (m-loop (sub1 m))))))

然后,扩展let表以实现m-loop涉及到n上的闭包,但编译器会自动转换闭包,将其作为参数传递给n,但编译器会自动转换闭包,将其作为参数传递给n。

19.12 可访问性和垃圾回收

通常,当垃圾回收器可以证明对象无法从任何其它(可访问)值访问时,Racket会重新使用存储空间来获取某个值。可访问性(reachability)是一个低级的,打破抽象的概念(因此,必须理解运行时系统实现精确判断的许多细节,准确地说,当值可以相互访问时),但一般来说,当存在从第二个值恢复原始值时,一个值可以从另一个值访问。

为了帮助程序员理解对象何时不再可访问并且其存储可以重用,Racket提供了make-weak-boxweak-box-value,垃圾回收器专门处理的一个记录结构的创建者和访问者。在弱盒子内的对象不被视为可访问,因此weak-box-value可能会返回盒内的该对象,但也可能返回#f以表示该对象以其它方式访问并被垃圾回收。请注意,除非垃圾回收实际发生,否则该值将保留在弱盒中,即使无法访问。

例如,考虑这个程序:

#lang racket
(struct fish (weight color) #:transparent)
(define f (fish 7 'blue))
(define b (make-weak-box f))
(printf "b has ~s\n" (weak-box-value b))
(collect-garbage)
(printf "b has ~s\n" (weak-box-value b))

它将打印两次b has #(struct:fish 7 blue),因为f的定义仍然适用于fish。然而,如果程序是这样的:

#lang racket
(struct fish (weight color) #:transparent)
(define f (fish 7 'blue))
(define b (make-weak-box f))
(printf "b has ~s\n" (weak-box-value b))
(set! f #f)
(collect-garbage)
(printf "b has ~s\n" (weak-box-value b))

第二次打印输出将是b has #f,因为不再存在对fish的引用(除了盒子里的那个)。

作为第一个近似值,必须分配Racket中的所有值,并显示出与上述fish相似的行为。但也有一些例外情况:

 

{
  • 小整数(可使用fixnum?识别)在没有明确分配的情况下始终可用。从垃圾回收器和弱盒子的角度来看,它们的存储永远不会被回收。(然而,由于巧妙的表达技巧,它们的存储空间不计入Racket使用的空间。也就是说,它们实际上是免费的。)

  • 编译器可以看到其所有调用点的过程可能永远不会被分配(如上所述)。类似的优化也可以消除对其它类型值的分配。

  • 交错符号仅分配一次(每个位置)。Racket中的表跟踪此分配,因此符号可能不会因为该表而成为垃圾。

  • 可访问性仅与CGC回收器(即,当实际上无法再访问某个值时,该回收器可能会看到该值是可访问的。

 

19.13 弱盒及测试

弱盒的一个重要用途是在测试某些抽象是否正确地释放了不再需要的数据的存储,但有一个问题很容易导致此类测试用例不正确地通过。

假设你正在设计一个数据结构,该数据结构需要暂时保存某个值,但应清除字段或以某种方式断开链接,以避免引用该值,以便回收该值。弱箱子是测试数据结构是否能正确清除值的好方法。也就是说,你可以编写一个测试用例,构建一个值,从中提取一些其他值(你希望它变得不可访问),将提取的值放入一个弱盒中,然后检查该值是否从盒子消失。

这段代码试图遵循这种模式,但它有一个微妙的错误:

#lang racket
(let* ([fishes (list (fish 8 'red)
                     (fish 7 'blue))]
       [wb (make-weak-box (list-ref fishes 0))])
  (collect-garbage)
  (printf "still there? ~s\n" (weak-box-value wb)))

具体来说,它会显示弱盒是空的,但这不是因为fishes不再保留这个值,而是因为fishes本身不再可访问!

把程序改成这样:

#lang racket
(let* ([fishes (list (fish 8 'red)
                     (fish 7 'blue))]
       [wb (make-weak-box (list-ref fishes 0))])
  (collect-garbage)
  (printf "still there? ~s\n" (weak-box-value wb))
  (printf "fishes is ~s\n" fishes))

现在我们看到了预期的结果。不同的是,变量fishes最后一次出现。这构成了对列表的引用,确保列表本身不是被垃圾回收的,因此red fish也不是。

19.14 减少垃圾回收暂停

默认情况下,Racket的分代垃圾回收器为频繁的小回收(minor collections)创建短暂停,这只会检查最近分配的对象,为不频繁的大回收(major collections)创建长暂停,这会重新检查所有内存。

对于某些应用程序,如动画和游戏,由于一个大集合而导致的长时间暂停可能会对程序的操作造成无法接受的干扰。为了减少大回收暂停,Racket垃圾回收器支持增量式垃圾回收(incremental garbage-collection)模式。在增量模式中,小回收通过向下一个大回收执行额外工作来创建更长(但仍然相对较短)的暂停。如果一切顺利,一个大回收的大部分工作都是由小回收完成的,在需要大回收的时候,所以大回收暂停和小回收暂停一样短。增量模式总体上运行更慢,但它可以提供更一致的实时行为。

当在Racket启动时将PLT_INCREMENTAL_GC环境变量设置为以1、y或Y开头的值,则将永久启用增量模式。然而,由于增量模式仅对某些程序的某些部分有用,并且由于增量模式的需要是程序的属性而不是其环境的属性,因此启用增量模式的首选方式是使用(collect-garbage 'incremental)。

调用(collect-garbage 'incremental)不会立即执行垃圾回收,而是请求每个小回收执行增量工作,直到下一个大回收。该请求将在下一次大回收时过期。在应用程序中需要实时响应的任何重复任务中调用(collect-garbage 'incremental)。在初始(collect-garbage 'incremental)之前,使用(collect-garbage)强制进行完全回收,以从最佳状态启动增量模式。

要检查是否使用了增量模式以及它如何影响暂停时间,请为GC主题启用debug级日志记录输出。例如,

  racket -W "debuG@GC error" main.rkt

运行"main.rkt"并将垃圾回收日志记录到标准错误(stderr)(同时保留所有主题的error级日志记录)。小回收由min行报告,增量模式小回收由mIn行报告,大回收由MAJ(专业)行报告。

 

20 并行

Racket提供两种形式的并行(parallelism)前程(futures)和现场(places)。在提供多个处理器的平台上,并行可以提高程序的运行时性能。

关于Racket里顺序性能的信息另参见《性能》。Racket还为并发(concurrency)提供了线程,但线程没有提供并行性;有关的详细信息,请参见《并发与同步》。

20.1 前程并行

racket/future库通过与前程(futures)以及futuretouch函数的并行性,为性能改进提供支持。然而,这些构造中可用的并行性水平受到几个因素的限制,当前的实现最适合于数值任务。在《DrRacket中的性能》中的警告也适用于前程;值得注意的是,调试工具目前无法实现前程。

其它函数,如thread,支持创建可靠的并发任务。然而,线程永远不会真正并行运行,即使硬件和操作系统支持并行。

作为一个开始的例子,下面的any-double?函数获取一个数字列表,并确定列表中的任何数字是否具有列表中的double:

(define (any-double? l)
  (for/or ([i (in-list l)])
    (for/or ([i2 (in-list l)])
      (= i2 (* 2 i)))))

该函数以二次时间运行,因此在l1和l2这样的大列表上可能需要很长时间(大约一秒):

(define l1 (for/list ([i (in-range 5000)])
             (+ (* 2 i) 1)))
(define l2 (for/list ([i (in-range 5000)])
             (- (* 2 i) 1)))
(or (any-double? l1)
    (any-double? l2))

加快any-double?速度的最佳方法 any-double?是使用不同的算法。然而,在一台至少提供两个处理单元的机器上,上面的例子可以使用futuretouch运行大约一半的时间:

(let ([f (future (lambda () (any-double? l2)))])
  (or (any-double? l1)
      (touch f)))

前程的f与(any-double? l1)并行运行(any-double? l2),而(any-double? l2)的结果与(touch f)要求的结果大致相同。

只要前程能够安全运行,它们就可以并行运行,但“前程安全”的概念与实现有着内在的联系。“前程安全”和“前程不安全”操作之间的区别在Racket程序级别上可能还不明显。本节的其余部分将通过一个示例来说明这一区别,并展示如何使用前程的可视化工具来帮助阐明这一点。

考虑曼德尔布罗特集合(Mandelbrot-set)计算的以下核心:

(define (mandelbrot iterations x y n)
  (let ([ci (- (/ (* 2.0 y) n) 1.0)]
        [cr (- (/ (* 2.0 x) n) 1.5)])
    (let loop ([i 0] [zr 0.0] [zi 0.0])
      (if (> i iterations)
          i
          (let ([zrq (* zr zr)]
                [ziq (* zi zi)])
            (cond
              [(> (+ zrq ziq) 4) i]
              [else (loop (add1 i)
                          (+ (- zrq ziq) cr)
                          (+ (* 2 zr zi) ci))]))))))

表达式(mandelbrot 10000000 62 500 1000)和(mandelbrot 10000000 62 501 1000)每个都需要一段时间才能得出答案。当然,计算两者所需的时间是前者的两倍:

(list (mandelbrot 10000000 62 500 1000)
      (mandelbrot 10000000 62 501 1000))

不幸的是,尝试与future并行运行这两个计算并不能提高性能:

(let ([f (future (lambda () (mandelbrot 10000000 62 501 1000)))])
  (list (mandelbrot 10000000 62 500 1000)
        (touch f)))

要了解原因,请使用future-visualizer,如下所示:

(require future-visualizer)
(visualize-futures
 (let ([f (future (lambda () (mandelbrot 10000000 62 501 1000)))])
   (list (mandelbrot 10000000 62 500 1000)
         (touch f))))

这将打开一个窗口,显示计算跟踪的图形视图。窗口的左上部分包含执行时间线:

 

每个水平行代表一个操作系统级线程,彩色点代表程序执行过程中的重要事件(它们用颜色编码以区分事件类型)。时间线上左上角的蓝点代表前程的创建。前程在线程1上执行一小段时间(由第二行中的绿色条表示),然后暂停以允许运行时线程执行前程的不安全操作。

在Racket实现中,前程的不安全操作分为两类。阻塞(block)操作会中止对前程的求值,并且在被接触(touch)之前不会允许它继续。在touch内完成操作后,运行时线程将依次对前程工作的剩余部分求值。同步(synchronize)操作也会中止前程,但运行时线程可以随时执行该操作,一旦完成,前程可以继续并行运行。内存分配和JIT编译是同步操作的两个常见示例。

在时间线中,我们在线程1的绿色条右侧看到一个橙色点——这个点表示同步操作(内存分配)。线程0上的第一个橙色点表示运行时线程在前程暂停后不久执行了分配。不久后,前程将停止一次阻塞操作(第一个红点),并且必须等到touch后才能进行求值(略高于1049ms)。

当你把鼠标移动到某个事件上时,可视化工具将显示该事件的详细信息,并绘制连接相应前程所有事件的箭头。这张图片展示了我们前程的联系。

 

橙色虚线将前程的第一个事件连接到创建它的前程,紫色虚线连接前程的相邻事件。

我们之所以看不到并行性,是因为mandelbrot中循环下部的<*操作涉及浮点值和固定(整数)值的混合。这种混合通常会触发执行中的慢路径,并一般的慢路径通常会阻塞。

将常数更改为mandelbrot中的浮点数解决了第一个问题:

(define (mandelbrot iterations x y n)
  (let ([ci (- (/ (* 2.0 y) n) 1.0)]
        [cr (- (/ (* 2.0 x) n) 1.5)])
    (let loop ([i 0] [zr 0.0] [zi 0.0])
      (if (> i iterations)
          i
          (let ([zrq (* zr zr)]
                [ziq (* zi zi)])
            (cond
              [(> (+ zrq ziq) 4.0) i]
              [else (loop (add1 i)
                          (+ (- zrq ziq) cr)
                          (+ (* 2.0 zr zi) ci))]))))))

有了这样的改变,mandelbrot计算可以并行运行。尽管如此,我们仍然看到一种特殊类型的慢路径操作限制了我们的并行性(橙色点):

 

问题是,本例中的大多数算术运算都会产生一个不精确数,必须分配其存储空间。虽然某些分配可以在没有运行时线程的情况下安全地以独占方式执行,但特别频繁的分配需要同步操作,这会影响性能的提高。

通过使用flonum特定的操作(请参见Fixnum和Flonum优化),我们可以重写mandelbrot以使用更少的分配:

(define (mandelbrot iterations x y n)
  (let ([ci (fl- (fl/ (* 2.0 (->fl y)) (->fl n)) 1.0)]
        [cr (fl- (fl/ (* 2.0 (->fl x)) (->fl n)) 1.5)])
    (let loop ([i 0] [zr 0.0] [zi 0.0])
      (if (> i iterations)
          i
          (let ([zrq (fl* zr zr)]
                [ziq (fl* zi zi)])
            (cond
              [(fl> (fl+ zrq ziq) 4.0) i]
              [else (loop (add1 i)
                          (fl+ (fl- zrq ziq) cr)
                          (fl+ (fl* 2.0 (fl* zr zi)) ci))]))))))

即使在顺序模式下,这种转换也可以将mandelbrot的速度提高8倍,但避免分配也可以使mandelbrot在并行模式下运行得更快。执行此程序会在可视化工具中产生以下结果:​​​

 

请注意,这里只显示了一个绿色条,因为其中一个曼德尔布罗特计算没有被前程(在运行时线程上)求值。

作为一般准则,JIT编译器内联的任何操作都安全运行地并行运行,而其它未内联的操作(包括禁用JIT编译器的所有操作)则被认为是不安全的。raco反编译工具对编译器可以内联的操作进行注释(请参见《(part ("(lib scribblings/raco/raco.scrbl)" "decompile"))》),因此反编译器可以用来帮助预测并行性能。

20.2 现场并行

racket/place库通过与place表的并行性来支持性能改进。place表创建了一个现场(place),这实际上是一个新的Racket实例,可以与其它地方(包括初始现场)并行运行。Racket语言的全部功能在每一个现场都可以使用,但现场只能通过消息传递进行通信——在有限的一组值上使用place-channel-putplace-channel-get函数——这有助于确保并行计算的安全性和独立性。

作为一个开始的例子,下面的racket程序使用现场来确定列表中的任何数是否具有在列表中的双位数:

#lang racket
 
(provide main)
 
(define (any-double? l)
  (for/or ([i (in-list l)])
    (for/or ([i2 (in-list l)])
      (= i2 (* 2 i)))))
 
(define (main)
  (define p
    (place ch
      (define l (place-channel-get ch))
      (define l-double? (any-double? l))
      (place-channel-put ch l-double?)))
 
  (place-channel-put p (list 1 2 4 8))
 
  (place-channel-get p))

place后面的标识ch绑定到 现场通道(place channel)place表中剩余的主体表达式将在一个新的现场求值,主体表达式使用ch与生成新位置的现场进行通信。

在上面的place表的主体中,新位置通过ch接收一个数字列表,并将列表绑定到l。它接着调用表上的any-double?并将结果绑定到l-double?。最终的主体表达式发送l-double?结果返回ch上的原始现场。

在DrRacket中,保存并运行上述程序后,在交互窗口对(main)求值以创建新的现场。在DrRacket中使用现场时,必须将包含现场代码的模块保存到一个文件中,然后才能执行。或者,将程序保存为"double.rkt",并从一个命令行运行以下内容

  racket -tm double.rkt

其中,-t标志告诉racket加载double.rkt模块,-m标志调用导出的main函数,同时-tm将这两个标志组合起来。

place表有两个微妙的特点。首先,它将place主体提升到一个匿名的模块级函数。这种提升意味着,place主体引用的任何绑定必须在模块的顶层可用。其次,placedynamic-require在新创建的现场中包含封闭模块。作为dynamic-require的一部分,当前模块主体将在新现场被求值。第二个特性的结果是,place不应立即出现在模块中或在模块顶层调用的函数中;否则,调用模块将在新现场调用同一模块,以此类推,从而触发一系列位置创建,很快就会耗尽内存。

#lang racket
 
(provide main)
 
; Don't do this!
(define p (place ch (place-channel-get ch)))
 
(define (indirect-place-invocation)
  (define p2 (place ch (place-channel-get ch))))
 
; Don't do this, either!
(indirect-place-invocation)

20.3 分布式现场

racket/place/distributed库为分布式编程提供支持。

下面的示例演示了如何启动远程racket节点实例,在新的远程节点实例上启动远程现场,以及启动监视远程节点实例的事件循环。

示例代码也可以在"racket/distributed/examples/named/master.rkt"中找到。

#lang racket/base
(require racket/place/distributed
         racket/class
         racket/place
         racket/runtime-path
         "bank.rkt"
         "tuple.rkt")
(define-runtime-path bank-path "bank.rkt")
(define-runtime-path tuple-path "tuple.rkt")
 
(provide main)
 
(define (main)
  (define remote-node (spawn-remote-racket-node 
                        "localhost" 
                        #:listen-port 6344))
  (define tuple-place (supervise-place-at 
                        remote-node 
                        #:named 'tuple-server 
                        tuple-path 
                        'make-tuple-server))
  (define bank-place  (supervise-place-at 
                        remote-node bank-path 
                        'make-bank))
 
  (message-router
    remote-node
    (after-seconds 4
      (displayln (bank-new-account bank-place 'user0))
      (displayln (bank-add bank-place 'user0 10))
      (displayln (bank-removeM bank-place 'user0 5)))
 
    (after-seconds 2
      (define c (connect-to-named-place remote-node 
                                        'tuple-server))
      (define d (connect-to-named-place remote-node 
                                        'tuple-server))
      (tuple-server-hello c)
      (tuple-server-hello d)
      (displayln (tuple-server-set c "user0" 100))
      (displayln (tuple-server-set d "user2" 200))
      (displayln (tuple-server-get c "user0"))
      (displayln (tuple-server-get d "user2"))
      (displayln (tuple-server-get d "user0"))
      (displayln (tuple-server-get c "user2"))
      )
    (after-seconds 8
      (node-send-exit remote-node))
    (after-seconds 10
      (exit 0))))
 

Figure 1: examples/named/master.rkt

spawn-remote-racket-node原语连接到"localhost",并在那里启动一个racloud节点,该节点在端口6344上侦听进一步的指令。新的racloud节点的句柄被分配给remote-node变量。使用localhost,以便仅使用一台计算机即可运行该示例。然而,localhost可以被任何具有ssh公钥访问和racket的主机更换。supervise-named-dynamic-place-at在remote-node上创建一个新现场。这个新现场将在前程中以其名称符号'tuple-server标记。通过调用带有tuple-path模块路径和'make-tuple-server符号的dynamic-place,可以返回现场描述符。

元组服务器现场的代码存在于文件"tuple.rkt"中。"tuple.rkt"文件包含使用define-named-remote-server表,该表定义了一个适合supervise-named-dynamic-place-at调用的RPC服务器。

#lang racket/base
(require racket/match
         racket/place/define-remote-server)
 
(define-named-remote-server tuple-server
  (define-state h (make-hash))
  (define-rpc (set k v)
    (hash-set! h k v)
    v)
  (define-rpc (get k)
    (hash-ref h k #f))
  (define-cast (hello)
    (printf "Hello from define-cast\n")
    (flush-output)))
 

Figure 2: examples/named/tuple.rkt

define-named-remote-server表以标识和自定义表达式列表作为参数。通过make-前缀前面加上前缀,可以从标识创建一个place-thunk函数。在本例中,make-tuple-server。make-tuple-server标识是上面的supervise-named-dynamic-place-at表所使用的place-function-name。define-state自定义表转换为一个简单的define表,该表由define-rpc表关闭。

define-rpc表扩展为两部分。第一部分是调用rpc函数的客户机存根。客户机函数名是通过将define-named-remote-server标识tuple-server与RPC函数名set连接起来形成tuple-server-set的。RPC客户机函数获取一个目标参数,它是一个remote-connection%描述符,进而是RPC函数参数。这个RPC客户机函数通过调用内部函数named-place-channel-put将RPC函数名、set和RPC参数发送到目标。然后,RPC客户机调用named-place-channel-get等待RPC响应。

define-rpc的第二个扩展部分是RPC调用的服务器实现。该服务器由make-tuple-server函数中的匹配表达式实现。tuple-server-set的匹配子句匹配以'set符号开头的消息。服务器使用传递的参数执行RPC调用,并将结果发送回RPC客户机。

define-cast表类似于define-rpc表,只是没有从服务器到客户机的应答消息外。

(module tuple racket/base
  (require racket/place
           racket/match)
  (define/provide
   (tuple-server-set dest k v)
   (named-place-channel-put dest (list 'set k v))
   (named-place-channel-get dest))
  (define/provide
   (tuple-server-get dest k)
   (named-place-channel-put dest (list 'get k))
   (named-place-channel-get dest))
  (define/provide
   (tuple-server-hello dest)
   (named-place-channel-put dest (list 'hello)))
  (define/provide
   (make-tuple-server ch)
    (let ()
      (define h (make-hash))
      (let loop ()
        (define msg (place-channel-get ch))
        (define (log-to-parent-real
                  msg
                  #:severity (severity 'info))
          (place-channel-put
            ch
            (log-message severity msg)))
        (syntax-parameterize
         ((log-to-parent (make-rename-transformer
                           #'log-to-parent-real)))
         (match
          msg
          ((list (list 'set k v) src)
           (define result (let () (hash-set! h k v) v))
           (place-channel-put src result)
           (loop))
          ((list (list 'get k) src)
           (define result (let () (hash-ref h k #f)))
           (place-channel-put src result)
           (loop))
          ((list (list 'hello) src)
           (define result
             (let ()
               (printf "Hello from define-cast\n")
               (flush-output)))
           (loop))))
        loop))))

Figure 3: Expansion of define-named-remote-server

 

21 运行和创建可执行文件

在开发程序时,很多Racket程序员使用DrRacket编程环境。要在没有开发环境的情况下运行程序,请使用racket(用于基于控制台的程序)或gracket(对于GUI程序)。本章主要介绍如何运行racket和gracket。

21.1 运行racket和gracket

gracket可执行文件和racket一样,但表现上有小的调整,可作为GUI应用程序而不是控制台应用程序。例如,gracket在默认情况下以交互模式运行,使用GUI窗口而不是控制台提示符。然而,GUI应用程序可以以普通的racket运行。

根据命令行参数的不同,racket或gracket在交互模式模块模式加载模式下。

21.1.1 交互模式

当racket在没有命令行参数的情况下运行时(除了配置选项,如-j),那么启动REPL使用> 提示符:

 

  Welcome to Racket v8.4 [cs].

  >

 

为了增强你的REPL体验,请参见《xrepl》;有关GNU Readline支持的信息,请参见《readline》。

要初始化REPL的环境,racket首先需要racket/init模块,该模块提供所有racket,并安装pretty-print以显示结果。最后,在启动REPL之前,racket加载(find-system-path 'init-file)报告的文件(如果存在)。

如果提供了任何命令行参数(配置项除外),请添加-i或--repl以重启REPL。例如,

  racket -e '(display "hi\n")' -i

在启动时显示“hi”,但仍显示REPL

如果需要标志的模块出现在-i/--repl之前,它们将取消对racket/init的自动需求。这种行为可用来使用其它语言初始化REPL的环境。例如,

  racket -l racket/base -i

使用更小的初始语言(加载速度更快)启动REPL。请注意,大多数模块都不提供Racket的基本语法,包括函数调用语法和require。例如,

  racket -l racket/date -i

生成一个对每个表达式都失败的REPL,因为racket/date只提供了几个函数,而不是REPL里需要求值顶层函数调用所需的#%top-interaction#%app绑定。

如果一个需求标志的模块出现在-i/--repl之后,而不是出现在它之前,那么该模块需要在racket/init之后,以增强初始环境。例如,

  racket -i -l racket/date

除了racket的导出之外,可以使用racket/date启动一个有用的REPL

21.1.2 模块模式

如果一个文件参数在任何命令行开关(除了其它配置选项)之前提供给racket,那么这个文件作为一个模块导入,没有REPL启动。例如,

  racket hello.rkt

需求"hello.rkt"模块,然后退出。文件名、标志或其它内容之后的任何参数都作为命令行参数保存,以供所需的模块通过current-command-line-arguments使用。

如果使用命令行标志,则可以使用-u或--require-script标志来显示地将文件作为模块。-t或--require标志类似,只是额外的命令行标志由racket处理,而不是为需求的模块保留。例如,

  racket -t hello.rkt -t goodbye.rkt

需求"hello.rkt"模块,然后需求"goodbye.rkt"模块,再然后退出。

-l或--lib标志类似于-t/--require,但它需求一个使用lib模块路径而不是文件路径的模块。例如,

  racket -l raco

与运行不带参数的raco可执行文件是一样的,因为raco模块是可执行文件的主模块。

请注意,如果你想将命令行标志传递给上面的raco,你需要用--保护这些标志,这样racket就不会试图自己解析它们:

  racket -l raco -- --help

21.1.3 加载模式

-f或--load标志直接支持文件中的load顶级表达式,而不是模块文件中的表达式。这个求值就像启动一个REPL并直接键入表达式,只是结果不打印。例如,

  racket -f hi.rkts

load "hi.rkts"并退出。请注意,由于给有LISP/Scheme经验的读者的一个说明中解释的原因,加载模式通常是一个坏主意;使用模块模式通常更好。

-e或--eval标志接受表达式直接求值。与文件加载不同,表达式的结果是打印的,就像在REPL中一样。例如,

  racket -e '(current-seconds)'

打印自1970年1月1日以来的秒数。

对于文件加载和表达式求值,顶级环境的创建方式与交互模式相同:除非首先指定了另一个模块,否则需求racket/init。例如,

  racket -l racket/base -e '(current-seconds)'

很可能运行得更快,因为它使用较小的racket/base语言而不是racket/init初始化求值环境。

 

21.2 脚本

Racket文件可以在UNIX和Mac OS上转换为可执行脚本。在Windows中,像Cygwin这样的兼容层支持相同类型的脚本,或者可以将脚本实现为批处理文件。

21.2.1 Unix脚本

在UNIX环境(包括Linux和Mac OS)中,可以使用shell的#!约定将Racket文件转换为可执行脚本。文件的前两个字符必须是#!;下一个字符必须是空格或/,并且第一行的其余部分必须是执行脚本的命令。对于某些平台,第一行的总长度限制为32个字符,而且有时需要空间。

使用#lang racket/base代替#lang racket来生成启动时间更快的脚本。

最简单的脚本格式使用racket可执行文件的绝对路径,随后是模块声明。例如,如果racket安装在"/usr/local/bin"中,那么包含以下文本的文件将充当“Hello World”脚本:

 

  #! /usr/local/bin/racket

  #lang racket/base

  "Hello, world!"

 

特别是,如果将上述内容放入文件"hello"中,并使该文件可执行(例如,使用chmod a+x hello),然后在shell提示符处键入./hello将生成输出"Hello, world!"。

上述脚本之所以有效,是因为操作系统自动将脚本的路径作为#!启动的程序的参数行,并且因为racket将单个非标志参数视为包含要运行的模块的文件。

一种流行的代替方法是,不指定racket可执行文件的完整路径,而是需求racket位于用户的命令路径中,然后“trampoline“使用/usr/bin/env:

 

  #! /usr/bin/env racket

  #lang racket/base

  "Hello, world!"

 

在任何一种情况下,脚本的命令行参数都可以通过current-command-line-arguments获得:

 

  #! /usr/bin/env racket

  #lang racket/base

  (printf "Given arguments: ~s\n"

          (current-command-line-arguments))

 

如果需要脚本的名称,可以通过(find-system-path 'run-file)获取,而不是(current-command-line-arguments)。

通常,处理命令行参数的最佳方法是使用racket提供的command-line表解析它们。默认情况下,command-line表从(current-command-line-arguments)中提取命令行参数:

 

  #! /usr/bin/env racket

  #lang racket

  

  (define verbose? (make-parameter #f))

  

  (define greeting

    (command-line

     #:once-each

     [("-v") "Verbose mode" (verbose? #t)]

     #:args

     (str) str))

  

  (printf "~a~a\n"

          greeting

          (if (verbose?) " to you, too!" ""))

 

尝试使用--help标志运行上述脚本,以查看脚本允许哪些命令行参数。

更普通的trampoline使用/bin/sh加上一些行,这些行是一种语言的注释,另一种语言是表达式。这个trampoline更复杂,但它提供了对racket命令行参数的更多控制:

 

  #! /bin/sh

  #|

  exec racket -e '(printf "Running...\n")' -u "$0" ${1+"$@"}

  |#

  #lang racket/base

  (printf "The above line of output had been produced via\n")

  (printf "a use of the `-e' flag.\n")

  (printf "Given arguments: ~s\n"

          (current-command-line-arguments))

 

请注意,#!在Racket中开始一行注释,#|...|#形成块注释。同时,#还启动了一个shell脚本注释,而exec racket则中止了shell脚本以启动racket。这样,脚本文件就变成了/bin/sh和racket的有效输入。

21.2.2 Windows批处理文件

类似的技巧也可以用于在windows中编写Racket代码.bat批处理文件:

 

  ; @echo off

  ; Racket.exe "%~f0" %*

  ; exit /b

  #lang racket/base

  "Hello, world!"

 

21.3 创建独立可执行文件

有关创建和分发可执行文件的信息,参见在《(part ("(lib scribblings/raco/raco.scrbl)" "top"))》中的(part ("(lib scribblings/raco/raco.scrbl)" "exe"))》和《(part ("(lib scribblings/raco/raco.scrbl)" "exe-dist"))》。

(part ("(lib scribblings/raco/raco.scrbl)" "top")).

 

22 更多库

本指南仅涵盖记录在The Racket Reference中记录的Racket语言和库。Racket发行版包括许多额外的库。

22.1 图形和图形用户界面

Racket为图形和图形用户界面(GUI)提供许多库:

  • racket/draw提供了基本的绘图工具,包括绘制背景如位图(bitmap)和PostScript文件。 参见(part ("(lib scribblings/draw/draw.scrbl)" "top"))获取更多信息。

  • racket/gui库提供的GUI部件,如窗口(window)、按钮(button)、复选框(checkboxe)和文本字段(text field)。该库还包括一个复杂的、可扩展的文本编辑器。 参见(part ("(lib scribblings/gui/gui.scrbl)" "top"))获取更多信息。

  • pict库一个在racket/draw之上的更多功能的抽象层。这一层对用Slideshow创建幻灯片演示特别有用,但它对为Scribble文档或其它绘图任务创建图像也是有用的。随着图像库创建的图片可以呈现任何绘画语境。 参见(part ("(lib scribblings/slideshow/slideshow.scrbl)" "top"))获取更多信息。

  • 2htdp/image库类似于pict。对于教学使用来说它更精简,但对屏幕和位图绘图也更轻量一些。 参见2htdp/image获取更多信息。

  • sgl库为三维(3-D)图形提供OpenGL。渲染OpenGL的上下文可以是用racket/gui创建的的一个窗口或位图。 参见the SGL documentation获取更多信息。

22.2 Web服务器

《(part ("(lib web-server/scribblings/web-server.scrbl)" "top"))》介绍了Racket的Web服务器,它支持在Racket中实现的servlet。

22.3 使用外部库

《(part ("(lib scribblings/foreign/foreign.scrbl)" "top"))》介绍了使用Racket访问C程序通常使用的工具。

22.4 更多其它库

Racket文档列出了许多其它已安装库的文档。运行raco docs查找安装在你的系统上且特定于你的用户账户的库的文档。

Racket包库提供了更多由Racketer贡献的可下载包。

传统的PLaneT站点提供了额外的包,尽管维护的包通常已迁移到更新的包存储库。

 

23 Racket和Scheme的方言

我们使用“Racket”来指Lisp语言的一种特定方言,它基于Lisp家族的Scheme分支。尽管Racket与Scheme相似,但模块上的#lang前缀是Racket的一个特殊特性,以#lang开始的程序不太可能在Scheme的其它实现中运行。同时,不以#lang开始的程序不能在大多数Racket工具的默认模式下工作。

然而,“Racket”并不是Racket工具支持的唯一Lisp方言。相反,Racket工具旨在支持Lisp的多种方言,甚至是多种语言,这使得Racket工具包可以服务于多个社区。Racket还为程序员和研究人员提供了探索和创建新语言所需的工具。

    23.1 更多的Racket

    23.2 标准

      23.2.1 R5RS

      23.2.2 R6RS

    23.3 教学

 

23.1 更多的Racket

“Racket”更多的是关于编程语言的概念,而不是通常意义上的语言。宏可以扩展基本语言(如中所述),替代解析器可以从头构建一种全新的语言(如《创造语言》中所述)。

启动Racket模块的#lang行声明了模块的基本语言。所谓“Racket”,我们通常指的是#lang,后面是基础语言racket或racket/base(其中racket是一个扩展)。Racket发行版提供了其它语言,包括:

  • typed/racket——像racket一样,但是静态类型的;参见《(part ("(lib typed-racket/scribblings/ts-guide.scrbl)" "top"))》。

  • lazy——像racket/base一样,但避免在需要表达式值之前对其求值;参见惰性(Lazy)Racket文档

  • frtime——以更激进的方式改变求值,以支持反应式编程;参见FrTime文档

  • scribble/base——一种看起来更像Latex而不是Racket的语言,用于编写文档;参见《(part ("(lib scribblings/scribble/scribble.scrbl)" "top"))》。

这些语言中的每一种都是通过在#lang之后使用语言名称启动模块来使用的。例如,本文档的源码即以#lang scribble/base开头。

此外,Racket用户可以定义自己的语言,如创造语言中所述。通常,一个语言名通过添加/lang/reader通过模块路径映射到它的实现;例如,语言名scribble/base扩展为scribble/base/lang/reader,这是实现表面语法分析器的模块。一些语言名称充当语言加载程序;例如,#lang planet planet-path通过PLaneT下载、安装和使用一个语言。

 

23.2 标准

Scheme的标准方言包括由r5rs和r6rs定义的方言。

23.2.1 R5RS

“R5RS”代表The Revised5 Report on the Algorithmic Language Scheme,它是目前实施最广泛的Scheme标准。

Racket工具在其默认模式下不符合R5RS,这主要是因为Racket通常需要模块,而R5RS没有定义模块系统。典型的单文件R5RS程序可以通过在它们前面加上#lang r5rs来转换为Racket程序,但其它Scheme系统无法识别#lang r5rs。plt-r5rs可执行文件(参见《(part ("(lib r5rs/r5rs.scrbl)" "plt-r5rs"))》)更直接地符合R5RS标准。

除了模块系统之外,R5RS和Racket的句法表和函数也有所不同。只有简单的R5RS在前缀为#lang racket时才成为Racket程序,而当删除#lang行时,相对较少的Racket编程成为R5RS程序。此外,当将”R5RS模块”与Racket模块混合时,请注意R5RS序对对应于Racket可变序对(如用mcons构造的)。

有关使用Racket运行R5RS程序的详细信息,请参见《(part ("(lib r5rs/r5rs.scrbl)" "top"))》。

23.2.2 R6RS

“R6RS”代表The Revised6 Report on the Algorithmic Language Scheme,该方案使用类似于Racket模块系统的模块系统扩展了R5RS。

当R6RS库或顶级程序前缀为#!时r6rs(有效的R6RS语法),那么它也可以用作Racket程序。这是因为#!在Racket中被视为#lang的缩写,后跟空格,所以#! r6rs选择r6rs模块语言。然而,与R5RS一样,请注意R6RS的语法表和函数与Racket不同,并且R6RS序对是可变序对。

参见(part ("(lib r6rs/scribblings/r6rs.scrbl)" "top"))有关使用Racket运行R6RS程序的详细信息,请参见《(part ("(lib r6rs/scribblings/r6rs.scrbl)" "top"))》。

 

23.3 教学

How to Design Programs教科书依靠Racket的教学变体,为新程序员顺利引入编程概念。请参见the How to Design Programs语言 documentation

How to Design Programs语言通常不与#lang前缀一起使用,而是通过从Choose Language...对话框中选择语言在DrRacket中使用。

 

24 命令行工具和你的编辑器选择

虽然DrRacket是大多数人用Racket开始的最简单的方法,许多Racket使用者喜欢命令行工具和其它文本编辑器。Racket分配包括几个命令行工具,流行的编辑器包括或支持包以使它们能很好地配合Racket。

24.1 命令行工具

作为其标准发行版的一部分,Racket提供了许多命令行工具,这些工具可以使racket使用者更加愉快。

24.1.1 同时编译和配置:raco

raco(以下简称“Racket command”)程序为了编译Racket程序和维护一个Racket安装提供了一个命令行界面给许多额外的工具。

  • raco make将Racket源文件编译成字节码。

    例如,如果你有一个程序"take-over-world.rkt"并且你想把它编译成字节码,连同其所有的依赖,使其加载速度更快,然后运行

      raco make take-over-the-world.rkt

    字节码文件在一个"compiled"子文件夹中被写为"take-over-the-world_rkt.zo"“;".zo"是一个字节码文件的文件后缀。

  • raco setup管理一个Racket安装,包括手动安装包。

    例如,如果你创建了自己的名为"take-over"的库集合(collection),并且希望为集合构建所有字节码和文档,则运行

      raco setup take-over

  • raco pkg管理package,它可以通过Racket包管理器被安装。

    例如,要查看已安装包的列表,运行:

      raco pkg show

    安装一个名为<package-name>的新包,运行:

      raco pkg install <package-name>

    参见(part ("(lib pkg/scribblings/pkg.scrbl)" "top"))以获得关于包管理器的更多细节。

为了获得有关raco的更多细节,见(part ("(lib scribblings/raco/raco.scrbl)" "top"))。

24.1.2 交互式求值

Racket REPL提供了你从现代交互环境中所期望的一切。例如,它提供了一个,enter命令以使REPL在给定模块的上下文中运行,并提供了一个,edit命令来调用你输入的文件上的编辑器(由EDITOR环境变量指定)。,drracket命令可以很容易地使用你最喜欢的编辑器编来编写代码,同时仍有DrRacket可以尝试。

有关详细信息,请参见(part ("(lib xrepl/xrepl.scrbl)" "top"))。

24.1.3 Shell补全

bash和zsh的Shell自动完成功能分别在"share/pkgs/shell-completion/racket-completion.bash"和"share/pkgs/shell-completion/racket-completion.zsh"中提供。

要启用它,只需从.bashrc或.zshrc运行相应的文件。

"shell-completion"集合仅在Racket Full发行版中可用。完成脚本也可在联机

 

24.2 Emacs

Emacs一直是一个在Lisp使用者和Scheme使用者中特别受欢迎的,并且也是在Racket使用者中流行的。

24.2.1 主要模式

  • Racket模式通过语法高亮和DrRacket风格REPL及Emacs缓冲区执行对Emacs支持。

    Racket模式可以通过MELPA或安装melpa或手动从GitHub库安装。

  • Quack是一个为Racket提供更有力的支持的Emacs的scheme模式(scheme-mode)的扩展,包括高亮和Racket特定形式的缩进,以及文档一体化。

    Quack是包含在Debian和Ubuntu库里作为emacs-goodies-el包的一部分。一个Gentoo端口也可获取的(在名字app-emacs/quack下)。

  • Geiser提供了一个编程环境,编辑器和Racket的REPL紧密集成。习惯用Slime或Squeak环境的程序员使用Geiser应该有宾至如归的感觉。Geiser要求GNU Emacs 23.2或更高的版本。

    Quack和Geiser可以一起使用,并且相辅相成。更多信息见Geiser手册

    为Geiser提供的Debian和Ubuntu软件包在名称geiser下适可获取的。

  • Emacs用一个为Scheme的主要模式传递,Scheme模式,而不是与上面的选项一样的特性,合理地编辑Racket代码。然而,这种模式并不能为Racket特定形式提供支持。

  • 没有文件,Racket项目是不完整的。Scribble支持emacs可用Neil Van Dyke的Scribble模式获取。。

    此外,当编辑Scribble文件的时候,texinfo模式(包括用GNU Emacs)和纯文本模式工作会非常好。鉴于与Racket相比Scribble语法是如此不同,上边的Racket主要模式不是真正的适合这种任务。

24.2.2 小模式

  • Paredit是在LISP类似语言中伪结构编辑程序的一个小模式。除了提供高阶S表达式编辑命令外,它可以帮你防止意外的不平衡括号。

    对Paredit的Debian和Ubuntu软件包在名字paredit-el下可以获取。

  • Smartparen对编辑S表达式是一个小模式,保持括号平衡、类似于Paredit等等。

  • Alex Shinn的scheme-complete提供了智能的、上下文敏感的代码完成。它还用Emacs的eldoc模式集成以在小缓冲区中提供现场文档。

    而这种模式是专为R5RS设计,它仍能用于Racket的开发。该工具不知道Racket标准库的大部分,而且在Scheme和Racket有分歧的情况下,现场文档可能有一些出入。

  • RainbowDelimiters模式颜色括号和其它分隔符根据嵌套深度确定。通过嵌套深度着色使人们一目了然地知道哪些圆括号匹配。

  • ParenFace让你选择在哪面(字体,颜色,等等)的括号应显示。选择一个交替的面可以使“tone down(按下)”括号。

24.2.3 Evil模式的专有包

  • on-parens是对smartparens行为用evil模式的通常状态去更好工作的一个包装。

  • evil-surround提供命令去添加、删除和改变括号和其它分隔符。

  • evil-textobj-anyblock添加一个文本对象相匹配最接近的任何括号或其它分隔符序对。

 

24.3 Vim

带Scheme支持的Vim运送的许多分配,它们将更多地用于Racket工作。你可以像Scheme一样用以下方式激活Racket文件的文件类型检查:

 

  if has("autocmd")

    au BufReadPost *.rkt,*.rktl set filetype=scheme

  endif

 

或者,你可以使用vim-racket插件来实现自动检测、缩进和专门针对Racket文件的语法高亮显示。使用插件是最简单的方法,但是如果你想把你自己的设置或重写插件设置,添加类似于下面的内容到你的".vimrc"文件:

 

  if has("autocmd")

    au BufReadPost *.rkt,*.rktl set filetype=racket

    au filetype racket set lisp

    au filetype racket set autoindent

  endif

 

然而,如果您采取这一路径,你可能需要在安装插件时做更多的工作,因为很多与Lisp相关的插件和vim脚本都不知道Racket。你也可以在一个"scheme.vim"中或在vim文件夹的"ftplugin"子文件夹中的"racket.vim"文件中设置这些条件命令。

vim的大多数安装会自动具有有用的默认启用,但如果你的安装没有,你会希望至少在你的".vimrc"文件里去设置:

 

 

  " Syntax highlighting

  syntax on

  

  " These lines make vim load various plugins

  filetype on

  filetype indent on

  filetype plugin on

  

  " No tabs!

  set expandtab

 

缩格

你可以通过在Vim里设置lisp和autoindent(自动缩格)选项启用Racket的缩格。然而,缩格是有限的也不是和你在Emacs中能得到的一样完整。你也可以用Dorai Sitaram的scmindent达到Racket代码的更好缩格。有关如何使用缩格器的说明可在网站上查阅。

如果使用内置的缩格器,可以通过设置如何缩进某些关键字来定制它。上面提到的vim-racket插件为你设置了一些默认关键字。你可以在你的".vimrc"文件里添加你自己的关键字,像这样:

 

  " By default vim will indent arguments after the function name

  " but sometimes you want to only indent by 2 spaces similar to

  " how DrRacket indents define. Set the `lispwords' variable to

  " add function names that should have this type of indenting.

  

  set lispwords+=public-method,override-method,private-method,syntax-case,syntax-rules

  set lispwords+=..more..

 

突出

用于可视化的彩虹括号(Rainbow Parenthesis})脚本可以用于更可见的括号匹配。在许多平台上,有很多功能都是通过高亮显示来实现的。为你提供了良好的默认高亮显示设置。

结构化的编辑

Slimv插件有一paredit模式,就像Emacs里的paredit工作方式。然而,插件不知道Racket。你可以设置Vim去把Racket作为Scheme文件,也可以修改paredit脚本以加载".rkt"文件。

Scribble

Vim support for writing scribble documents is provided by the scribble.vim plugin. 对书写scribble文件,Vim通过scribble.vim插件被支持。

混杂的

如果你安装了很多Vim插件(不需要特别针对Racket),我们建议使用一个插件,让其它插件更容易加载。Pathogen是一个这样做的插件;使用它,你可以通过在你Vim安装的"bundle"文件夹里提取它们到子目录来安装新插件。

24.4 Sublime Text

Racket package支持语法高亮显示和构建 Sublime Text。

 

========== End

 

posted @ 2018-12-21 09:13  lsgxeva  阅读(3036)  评论(0编辑  收藏  举报