├── 00 Racket指南 ├── 01 Racket语言欢迎你 ├── 01.1 与Racket语言交互 ├── 01.2 定义和交互 ├── 01.3 创建可执行文件 ├── 01.4 给有LISP|Scheme经验的读者的一个说明 ├── 02 Racket概要 ├── 02.1 简单的值 ├── 02.2 简单的定义与表达式 ├── 02.2.1 定义 ├── 02.2.2 代码缩进 ├── 02.2.3 标识符 ├── 02.2.4 函数调用(应用程序) ├── 02.2.5 条件表达式if、and、or和cond ├── 02.2.6 函数重复调用 ├── 02.2.7 匿名函数与lambda ├── 02.2.8 用define、let和let*实现本地绑定 ├── 02.3 列表、迭代和递归 ├── 02.3.1 预定义列表循环 ├── 02.3.2 从头开始列表迭代 ├── 02.3.3 尾递归 ├── 02.3.4 递归和迭代 ├── 02.4 pair、list和Racket的语法 ├── 02.4.1 用quote引用pair和symbol ├── 02.4.2 使用’缩写quote ├── 02.4.3 列表和Racket的语法 ├── 03 内置的数据类型 ├── 03.01 布尔值(Boolean) ├── 03.02 数值(Number) ├── 03.03 字符(Character) ├── 03.04 字符串(Unicode Strings) ├── 03.05 字节(Byte)和字节字符串(Byte String) ├── 03.06 符号(Symbol) ├── 03.07 关键字(Keyword) ├── 03.08 配对(Pair)和列表(List) ├── 03.09 向量(Vector) ├── 03.10 哈希表(Hash Table) ├── 03.11 格子(Box) ├── 03.12 空值(Void)和未定义值(Undefined) ├── 04 表达式和定义 ├── 04.01 标记法 ├── 04.02 标识符和绑定 ├── 04.03 函数调用(过程程序) ├── 04.03.1 求值顺序和元数 ├── 04.03.2 关键字参数 ├── 04.03.3 apply函数 ├── 04.04 lambda函数(程序) ├── 04.04.1 申明剩余(rest)参数 ├── 04.04.2 声明可选(optional)参数 ├── 04.04.3 声明关键字(keyword)参数 ├── 04.04.4 多解函数:case-lambda ├── 04.05 定义:define ├── 04.05.1 函数简写 ├── 04.05.2 咖喱函数简写 ├── 04.05.3 多值和define-values ├── 04.05.4 内部定义 ├── 04.06 局部绑定 ├── 04.06.1 平行绑定:let ├── 04.06.2 相继绑定:let* ├── 04.06.3 递归绑定:letrec ├── 04.06.4 命名let ├── 04.06.5 多值绑定:let-values,let*-values,letrec-values ├── 04.07 条件分支 ├── 04.07.1 简单分支:if ├── 04.07.2 组合测试:and和or ├── 04.07.3 约束测试:cond ├── 04.08 排序 ├── 04.08.1 前置影响:begin ├── 04.08.2 后置影响:begin0 ├── 04.08.3 if影响:when和unless ├── 04.09 赋值:set! ├── 04.09.1 使用赋值的指导原则 ├── 04.09.2 多值赋值:set!-values ├── 04.10 引用:quote和' ├── 04.11 准引用:quasiquote和` ├── 04.12 简单分派:case ├── 04.13 动态绑定:parameterize ├── 05 自定义的数据类型 ├── 05.1 简单的结构类型:struct ├── 05.2 复制和更新 ├── 05.3 结构子类 ├── 05.4 不透明结构类型与透明结构类型对比 ├── 05.5 结构的比较 ├── 05.6 结构类型的生成性 ├── 05.7 预制结构类型 ├── 05.8 更多的结构选项 ├── 06 模块 ├── 06.1 模块基础知识 ├── 06.1.1 组织模块 ├── 06.1.2 库集合 ├── 06.1.3 包和集合 ├── 06.1.4 添加集合 ├── 06.2 模块的语法 ├── 06.2.1 module表 ├── 06.2.2 #lang简写 ├── 06.2.3 子模块 ├── 06.2.4 main和test子模块 ├── 06.3 模块的路径 ├── 06.4 导入:require ├── 06.5 输出:provide ├── 06.6 赋值和重定义 ├── 07 合约 ├── 07.1 合约和边界 ├── 07.1.1合约的违反 ├── 07.1.2 合约与模块的测试 ├── 07.1.3 嵌套合约的测试 ├── 07.2 函数的简单合约 ├── 07.2.1 ->类型 ├── 07.2.2 define|contract和->的使用 ├── 07.2.3 and-any|c ├── 07.2.4 扩展自己的合约 ├── 07.2.5 合约的高阶函数 ├── 07.2.6 带”???“的合约信息 ├── 07.2.7 解析合约的错误消息 ├── 07.3 通常的函数合约 ├── 07.3.1 可选参数 ├── 07.3.2 剩余参数 ├── 07.3.3 关键字参数 ├── 07.3.4 可选关键字参数 ├── 07.3.5 case-lambda合约 ├── 07.3.6 参数和结果的依赖 ├── 07.3.7 状态变化的检查 ├── 07.3.8 多个结果值 ├── 07.3.9 固定但静态未知数量的参数 ├── 07.4合约:一个全面的例子 ├── 07.5 结构的合约 ├── 07.5.1 对特定值的确保 ├── 07.5.2 对所有值的确保 ├── 07.5.3 检查数据结构的特性 ├── 07.6 用#:exists和#:∃抽象合约 ├── 07.7 额外的例子 ├── 07.7.1 客户管理器的组成 ├── 07.7.2 参数(简单)栈 ├── 07.7.3 字典 ├── 07.7.4 队列 ├── 07.8 建立新合约 ├── 07.8.1 合约结构属性 ├── 07.8.2 所有的警告和报警 ├── 07.9 陷阱 ├── 07.9.1 合约和eq? ├── 07.9.2 合约的范围和define|contract ├── 07.9.3 合约的生存期和判定 ├── 07.9.4 定义递归合合约 ├── 07.9.5 混合set!和合约 ├── 08 输入和输出 ├── 08.1 端口的种类 ├── 08.2 默认端口 ├── 08.3 读和写Racket数据 ├── 08.4 字符类型和序列化 ├── 08.5 字节、字符和编码 ├── 08.6 IO模式 ├── 09 正则表达式 ├── 09.01 写regexp模式 ├── 09.02 匹配正则表达式模式 ├── 09.10 一个扩展示例 ├── 09.3 基本申明 ├── 09.4 字符和字符类 ├── 09.4.1 常用的字符类 ├── 09.4.2 POSIX字符类 ├── 09.5 量词 ├── 09.6 聚类 ├── 09.6.1 后向引用 ├── 09.6.2 非捕捉簇 ├── 09.6.3 回廊 ├── 09.7 替代 ├── 09.8 回溯 ├── 09.9 前后查找 ├── 09.9.1 向前查找 ├── 09.9.2 向后查找 ├── 10 异常与控制 ├── 10.1 异常 ├── 10.2 提示和中止 ├── 10.3 延续 ├── 11 迭代和推导 ├── 11.01 序列构造 ├── 11.02 for和for* ├── 11.03 for|list和for*|list ├── 11.04 for|vector和for*|vector ├── 11.05 for|and和for|or ├── 11.06 for|first和for|last ├── 11.07 for|fold和for*fold ├── 11.08 多值序列 ├── 11.09 打断迭代 ├── 11.10 迭代性能 ├── 12 模式匹配 ├── 13 类和对象 ├── 13.1 方法 ├── 13.2 初始化参数 ├── 13.3 内部和外部名称 ├── 13.4 接口(Interface) ├── 13.5 Final、Augment和Inner ├── 13.6 控制外部名称的范围 ├── 13.7 混合(mixin) ├── 13.7.1 混合和接口 ├── 13.7.2 mixin表 ├── 13.7.3 参数化的混合 ├── 13.8 特征(trait) ├── 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 命名空间(Namespace) ├── 15.1.3 命名空间和模块 ├── 15.2 操纵的命名空间 ├── 15.2.1 创建和安装命名空间 ├── 15.2.2 共享数据和代码的命名空间 ├── 15.3 脚本求值和使用load ├── 16 宏(Macro) ├── 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 语法污染 ├── 16.2.7.1 污染模式 ├── 16.2.7.2 污染和代码检查 ├── README.md └── WORDBOOK.md /01 Racket语言欢迎你: -------------------------------------------------------------------------------- 1 | 1 Racket语言欢迎你 2 | 3 | 依赖于你如何看待它,Racket语言会是: 4 | 1、一种编程语言——一种Lisp语言的方言,继承于Scheme; 5 | 2、一系列编程语言——如Racket或者其它等等; 6 | 3、一系列工具集——用于一系列编程语言的。 7 | 当不会出现混乱的地方,我们就用简单的Racket。 8 | 9 | Racket的主要工具是包括: 10 | 1、racket——核心编译器、解释器和运行时系统; 11 | 2、DrRacket——编程环境; 12 | 3、raco——一个用于执行Racket命令以安装软件包、建立库等等的命令行工具。 13 | 14 | 最有可能的是,你想使用DrRacket探索Racket语言,尤其是在开始阶段。如果您愿意,您还可以使用命令行racket解释器和您喜欢的文本编辑器,也可以参见《命令行工具和选择编辑器》(Command-Line Tools and Your Editor of Choice)部分内容。本指南的其余部分介绍了与语言无关的编辑器的选择。 15 | 16 | 如果你使用DrRacket,就需要选择适当的语言,因为DrRacket可以容纳许多不同的变种如Racket,以及其他语言。如果你以前从未使用DrRacket,启动它,在DrRacket顶上的文本区域输入这一行: 17 | 18 | #lang racket 19 | 20 | 然后单击“运行”(run)按钮的上方的文本区。DrRacket就明白你的意思在Racket执行正常变体的工作(相对于较小的racket/base或许多其他的可能性)。 21 | 22 | 如果你使用DrRacket之前已经使用了以#lang开始的其它语言,那么DrRacket会记得你上次使用的语言,而不是从#lang推断的语言。在这种情况下,使用"语言|选择语言……"(Language|Choose Language…)菜单项去改变。在出现的对话框中,选择第一项,它告诉DrRacket使用通过#lang申明在源程序中的语言。仍然要把把#lang放在文本区域的顶部的。 23 | -------------------------------------------------------------------------------- /01.1 与Racket语言交互: -------------------------------------------------------------------------------- 1 | 1.1 与Racket语言交互 2 | 3 | DrRacket底部的文本区和racket的命令行程序(启动时没有选择)作为一种计算器。你打出一个racket的表达式,按下回车键,答案就打印出来了。在Racket的术语里,这种计算器叫做“读取求值打印”(read-eval-print)循环或REPL。 4 | 5 | 一个数字本身就是一个表达式,而答案就是数字: 6 | > 5 7 | 5 8 | 9 | 字符串也是一个求值的表达式。字符串在字符串的开始和结尾使用双引号: 10 | > "Hello, world!" 11 | "Hello, world!" 12 | 13 | Racket使用圆括号包装较大的表达式——几乎任何一种表达式,而不是简单的常数。例如,函数调用被写入:大括号,函数名,参数表达式,闭括号。下面的表达式用参数调用"the boy out of the country",4,and7调用内置函数substring: 14 | > (substring "the boy out of the country" 4 7) 15 | "boy" 16 | -------------------------------------------------------------------------------- /01.2 定义和交互: -------------------------------------------------------------------------------- 1 | 1.2 定义和交互 2 | 3 | 你可以通过使用define表像substring那样定义自己的函数,像这样: 4 | 5 | (define (extract str) 6 | (substring str 4 7)) 7 | 8 | > (extract "the boy out of the country") 9 | "boy" 10 | > (extract "the country out of the boy") 11 | "cou" 12 | 13 | 虽然你可以在REPL求值这个define表,但定义通常是你要保持并今后使用一个程序的一部分。所以,在DrRacket中,你通常会把定义放在顶部的文本区——被称作定义区——随着#lang前缀一起: 14 | 15 | #lang racket 16 | 17 | (define (extract str) 18 | (substring str 4 7)) 19 | 20 | 如果调用(extract "the boy")是程序的主要行为的一部分,那么它也将进入定义区域。但如果这只是一个例子,你用来测试extract,那么你会更容易如上面那样离开定义区域,点击“运行”(Run),然后将在REPL中求值(extract "the boy")。 21 | 22 | 当使用命令行的racket代替DrRacket,你会在一个文件中用你喜欢的编辑器保存上面的文本。如果你将它保存为“extract.rkt”,然后在同一目录开始racket,你会对以下序列求值: 23 | 24 | (enter! "extract.rkt") 25 | > (extract "the gal out of the city") 26 | "gal" 27 | 28 | enter!表加载代码和开关的求值语境到模块里面,就像DrRacket的运行按钮一样。 29 | -------------------------------------------------------------------------------- /01.3 创建可执行文件: -------------------------------------------------------------------------------- 1 | 1.3 创建可执行文件 2 | 3 | 如果你的文件(或在DrRacket的定义区域)包含: 4 | 5 | #lang racket 6 | 7 | (define (extract str) 8 | (substring str 4 7)) 9 | 10 | (extract "the cat out of the bag") 11 | 12 | 那么它是一个在运行时打印“cat” 的完整程序。你可以在DrRacket中运行程序或在racket中使用enter!,但如果程序被保存在‹src-filename›中,你也可以从命令行运行 13 | 14 | racket ‹src-filename› 15 | 16 | 将程序打包为可执行文件,您有几个选项: 17 | *在DrRacket,你可以选择Racket|Create Executable...菜单项。 18 | *从命令提示符,运行raco exe ‹src-filename›,这里‹src-filename›包含程序。(参见《raco exe: Creating Stand-Alone Executables 》部分获取更多信息。) 19 | *在UNIX或Mac OS中,可以通过在文件的开头插入以下行将程序文件转换为可执行脚本: 20 | 21 | #! /usr/bin/env racket 22 | 23 | 同时,在命令行中用chmod +x ‹filename› 改变文件权限去执行。 24 | 25 | 只要racket在用户的可执行搜索路径中脚本就会工作。另外,在#!后使用完整路径提交给racket(在#!和路径之间有空格),在这种情况下用户的可执行搜索路径无关紧要。 26 | -------------------------------------------------------------------------------- /01.4 给有LISP|Scheme经验的读者的一个说明: -------------------------------------------------------------------------------- 1 | 1.4 给有LISP/Scheme经验的读者的一个说明 2 | 3 | 如果你已经知道一些关于Scheme或Lisp的东西,你可能会试图这样将 4 | 5 | (define (extract str) 6 | (substring str 4 7)) 7 | 8 | 放入"extract.rktl"并且如下运行racket 9 | 10 | > (load "extract.rktl") 11 | > (extract "the dog out") 12 | "dog" 13 | 14 | 这将起作用,因为racket会模仿传统的Lisp环境,但我们强烈建议不要在模块之外使用load或编写程序。 15 | 16 | 在模块之外编写定义会导致糟糕的错误消息、差的性能和笨拙的脚本来组合和运行程序。这些问题并不是特别针对racket,它们是传统顶层环境的根本限制,Scheme和Lisp实现在历史上与临时命令行标志、编译器指令和构建工具进行了斗争。模块系统的设计是为了避免这些问题,所以以#lang开始,你会在长期工作中与Racket更愉快。 17 | -------------------------------------------------------------------------------- /02 Racket概要: -------------------------------------------------------------------------------- 1 | 本章提供了一个快速入门的以Racket语言骨架作为背景的指南。有Racket经验的读者可以直接跳到内置数据类型部分。 2 | 3 | 2.1 简单值 4 | 2.2 简单的定义与表达式 5 | 2.2.1 定义 6 | 2.2.2 代码缩进 7 | 2.2.3 标识符 8 | 2.2.4 函数调用(应用程序) 9 | 2.2.5 条件表达式if、and、or和cond 10 | 2.2.6 函数反复调用 11 | 2.2.7 匿名函数与lambda 12 | 2.2.8 用define、let和let*实现局部绑定 13 | 2.3 列表、迭代和递归 14 | 2.3.1 预定义列表循环 15 | 2.3.2 从头开始列表迭代 16 | 2.3.3 尾递归 17 | 2.3.4 递归和迭代 18 | 2.4 pair、list和Racket的语法 19 | 2.4.1 用quote引用pair和symbol 20 | 2.4.2 使用引用的缩写(') 21 | 2.4.3 列表和Racket的语法 22 | -------------------------------------------------------------------------------- /02.1 简单的值: -------------------------------------------------------------------------------- 1 | Racket值包括数字、布尔值、字符串和字节字符串。DrRacket和文档示例中(当你在着色状态下阅读文档时),值表达式显示为绿色。 2 | 3 | 一、数值(Numbers) 4 | 数值书写为惯常的方式,包括分数和虚数: 5 | 1 3.14 6 | 1/2 6.02e+23 7 | 1+2i 9999999999999999999999 8 | 9 | 二、布尔值(Booleans) 10 | 布尔值用#t表示真,#f表示假。但是,在条件表达式里,所有的非#f值都被当做真。 11 | 12 | 三、字符串(Strings) 13 | 字符串写在双引号("")之间。在一个字符串中,反斜杠(/)是一个转义字符;例如,一个反斜杠之后的双引号为包括文字双引号的字符串。除了一个保留的双引号或反斜杠,任何Unicode字符都可以在字符串常量中出现。 14 | "Hello, world!" 15 | "Benjamin \"Bugsy\" Siegel" 16 | "λx:(μα.α→α).xx" 17 | 当一个常量在REPL中被评估,通常它的打印结果与输入的语法相同。在某些情况下,打印格式是输入语法的标准化版本。在文档和DrRacket REPL中,结果打印为蓝色而不是绿色以强调打印结果与输入表达式之间的区别。 18 | 比如: 19 | 20 | > 1.0000 21 | 1.0 22 | 23 | > "Bugs \u0022Figaro\u0022 Bunny" 24 | "Bugs \"Figaro\" Bunny" 25 | -------------------------------------------------------------------------------- /02.2 简单的定义与表达式: -------------------------------------------------------------------------------- 1 | 2.2 简单的定义与表达式 2 | 3 | 一个程序模块一般被写作: 4 | #lang ‹langname› ‹topform›* 5 | 既是一个也是一个。REPL也评估。 6 | 7 | 在语法规范里,文本使用灰色背景,比如#lang,代表文本。文本与非结束符(像)之间必须有空格,除了(、)及[、]之前或之后不需要空格。注释以;开始,直至这一行结束,空白也做相同处理。 8 | 9 | Racket参考中提供了更多不同的注释形式。 10 | 11 | 后边遵从如下惯例:*在程序中表示零个或多个前面元素的重复,+表示前一个或多个前面元素的重复,{}组合一个序列作为一个元素的重复。 12 | 13 | -------------------------------------------------------------------------------- /02.2.1 定义: -------------------------------------------------------------------------------- 1 | 2.2.1 定义 2 | 3 | 表的定义: 4 | ( define ‹id› ‹expr› ) 5 | 绑定‹id›到‹expr›的结果,而 6 | ( define ( ‹id› ‹id›* ) ‹expr›+ ) 7 | 绑定第一个‹ID›到一个函数(也叫程序),以参数作为命名‹ID›,对函数的实例,该‹expr›是函数的函数体。当函数被调用时,它返回最后一个‹expr›的结果。 8 | 9 | 例子: 10 | (define pie 3) ; 定义 pie 为 3 11 | 12 | (define (piece str) ; 定义 piece 为一个有一个参数的函数 13 | (substring str 0 pie)) 14 | 15 | > pie 16 | 3 17 | > (piece "key lime") 18 | "key" 19 | 20 | 在封装下,函数定义实际上与非函数定义相同,函数名不需要在函数调用中使用。函数只是另一种类型的值,尽管打印形式必须比数字或字符串的打印形式更不完整。 21 | 22 | 例子: 23 | > piece 24 | # 25 | > substring 26 | # 27 | 28 | 函数定义可以包含函数体的多个表达式。在这种情况下,在调用函数时只返回最后一个表达式的值。其他表达式只对一些副作用进行求值,比如打印这些。 29 | 30 | 例子: 31 | (define (bake flavor) 32 | (printf "pre-heating oven...\n") 33 | (string-append flavor " pie")) 34 | 35 | > (bake "apple") 36 | pre-heating oven... 37 | "apple pie" 38 | 39 | Racket程序员更喜欢避免副作用,所以一个定义通常只有一个表达式。这是重要的,但是,了解多个表达式在定义体内是被允许的,因为它解释了为什么以下nobake函数未在其结果中包含它的参数: 40 | 41 | (define (nobake flavor) 42 | string-append flavor "jello") 43 | 44 | > (nobake "green") 45 | "jello" 46 | 47 | 在nobake,没有括号包括string-append给"jello",那么他们是三个单独的表达而不是函数调用表达式。string-append表达式和flavor被求值,但结果没有被使用。相反,该函数的结果是最终的表达式"jello"。 48 | -------------------------------------------------------------------------------- /02.2.2 代码缩进: -------------------------------------------------------------------------------- 1 | 2.2.2 代码缩进 2 | 3 | 换行和缩进对于解析Racket程序来说并不重要,但大多数Racket程序员使用一套标准的约定来使代码更易读。例如,定义的主体通常在定义的第一行下缩进。标识符是在一个没有额外空格的括号内立即写出来的,而闭括号则从不自己独立一行。 4 | 5 | DrRacket会根据标准风格自动缩进,当你输入一个程序或REPL表达式。例如,如果你点击进入后输入(define (greet name),那么DrRacket自动为下一行插入两个空格。如果你改变了代码区域,你可以在DrRacket打Tab选择它,并且DrRacket将重新缩进代码(没有插入任何换行)。象Emacs这样的编辑器提供Racket或Scheme类似的缩进模式。 6 | 7 | 重新缩进不仅使代码更易于阅读,它还会以你希望的方式给你更多的反馈,象你的括号是否匹配等等。例如,如果在函数的最后一个参数之后省略一个结束括号,则自动缩进在第一个参数下开始下一行,而不是在define关键字下: 8 | 9 | (define (halfbake flavor 10 | (string-append flavor " creme brulee"))) 11 | 12 | 在这种情况下,缩进有助于突出错误。在其他情况下,在缩进可能是正常的,一个开括号没有匹配的闭括号,racket和DrRacket都在源程序的缩进中提示括号丢失。 13 | -------------------------------------------------------------------------------- /02.2.3 标识符: -------------------------------------------------------------------------------- 1 | 2.2.3 标识符 2 | 3 | Racket的标识符语法特别自由。不含特殊字符。 4 | 5 | ( ) [ ] { } " , ' ` ; # | \ 6 | 7 | 除了文字,使常数数字序列,几乎任何非空白字符序列形成一个‹ID›。例如,substring是一个标识符。另外,string-append和a+b是标识符,而不是算术表达式。这里还有几个例子: 8 | 9 | + 10 | Hfuhruhurr 11 | integer? 12 | pass/fail 13 | john-jacob-jingleheimer-schmidt 14 | a-b-c+1-2-3 15 | -------------------------------------------------------------------------------- /02.2.4 函数调用(应用程序): -------------------------------------------------------------------------------- 1 | 2.2.4 函数调用(应用程序) 2 | 3 | 我们已经看到过许多函数调用,更传统的术语称之为过程应用程序。函数调用的语法是: 4 | 5 | ( ‹id› ‹expr›* ) 6 | 7 | ‹expr›决定了‹ID›命名函数提供的参数个数。 8 | 9 | racket语言预定义了许多函数标识符,比如substring和string-append。下面有更多的例子。 10 | 11 | Racket代码例子贯穿整个文档,预定义的名称的使用链接到参考手册。因此,你可以单击标识符来获得关于其使用的详细信息。 12 | 13 | > (string-append "rope" "twine" "yarn") ; append strings 14 | "ropetwineyarn" 15 | > (substring "corduroys" 0 4) ; extract a substring 16 | "cord" 17 | > (string-length "shoelace") ; get a string's length 18 | 8 19 | > (string? "Ceci n'est pas une string.") ; recognize strings 20 | #t 21 | > (string? 1) 22 | #f 23 | > (sqrt 16) ; find a square root 24 | 4 25 | > (sqrt -16) 26 | 0+4i 27 | > (+ 1 2) ; add numbers 28 | 3 29 | > (- 2 1) ; subtract numbers 30 | 1 31 | > (< 2 1) ; compare numbers 32 | #f 33 | > (>= 2 1) 34 | #t 35 | > (number? "c'est une number") ; recognize numbers 36 | #f 37 | > (number? 1) 38 | #t 39 | > (equal? 6 "half dozen") ; compare anything 40 | #f 41 | > (equal? 6 6) 42 | #t 43 | > (equal? "half dozen" "half dozen") 44 | #t 45 | -------------------------------------------------------------------------------- /02.2.6 函数重复调用: -------------------------------------------------------------------------------- 1 | 2.2.6 函数重复调用 2 | 3 | 在我们早期的函数语法调用,我们是过分简单化的。一个函数调用的语法允许任意的函数表达式,而不是一个‹ID›: 4 | 5 | ( ‹expr› ‹expr›* ) 6 | 7 | 第一‹expr›常常是一个‹ID›,比如string-append或+,但它可以是对一个函数的求值的任意情况。例如,它可以是一个条件表达式: 8 | 9 | (define (double v) 10 | ((if (string? v) string-append +) v v)) 11 | 12 | > (double "mnah") 13 | "mnahmnah" 14 | > (double 5) 15 | 10 16 | 17 | 在语法上,在一个函数调用的第一个表达甚至可以是一个数——但那会导致一个错误,因为一个数不是一个函数。 18 | 19 | > (1 2 3 4) 20 | 21 | application: not a procedure; 22 | expected a procedure that can be applied to arguments 23 | given: 1 24 | arguments...: 25 | 2 26 | 3 27 | 4 28 | 29 | 当您意外地忽略函数名或在表达式中使用额外的括号时,你通常会得到像这样“expected a procedure”的错误。 30 | -------------------------------------------------------------------------------- /02.2.7 匿名函数与lambda: -------------------------------------------------------------------------------- 1 | 2.2.7 匿名函数与lambda 2 | 3 | 如果你必须命名你所有的数值,那Racket的编程就太乏味了。替换(+ 1 2)的写法,你得这样写: 4 | 5 | > (define a 1) 6 | > (define b 2) 7 | > (+ a b) 8 | 3 9 | 10 | 事实证明,要命名所有函数也可能很乏味。例如,您可能有个函数twice,带了一个函数和一个参数。如果你已经有了函数的名字那么使用twice是比较方便的,如: 11 | 12 | (define (twice f v) 13 | (f (f v))) 14 | 15 | > (twice sqrt 16) 16 | 2 17 | 18 | 如果您想调用尚未定义的函数,您可以定义它,然后将其传递给twice: 19 | 20 | (define (louder s) 21 | (string-append s "!")) 22 | 23 | 24 | > (twice louder "hello") 25 | "hello!!" 26 | 27 | 但是如果对twice的调用是唯一使用louder的地方,却还要写一个完整的定义是很可惜的。在Racket中,可以使用lambda表达式直接生成函数。lambda表后面是函数参数的标识符,然后是函数的体表达式: 28 | 29 | ( lambda ( ‹id›* ) ‹expr›+ ) 30 | 31 | 求值lambda表本身即产生一个函数: 32 | 33 | > (lambda (s) (string-append s "!")) 34 | # 35 | 36 | 使用lambda,上述调用两次可以重写为: 37 | 38 | > (twice (lambda (s) (string-append s "!")) 39 | "hello") 40 | "hello!!" 41 | 42 | > (twice (lambda (s) (string-append s "?!")) 43 | "hello") 44 | "hello?!?!" 45 | 46 | lambda的另一个用途是作为生成函数的函数的结果: 47 | 48 | (define (make-add-suffix s2) 49 | (lambda (s) (string-append s s2))) 50 | 51 | 52 | > (twice (make-add-suffix "!") "hello") 53 | "hello!!" 54 | 55 | > (twice (make-add-suffix "?!") "hello") 56 | "hello?!?!" 57 | 58 | > (twice (make-add-suffix "...") "hello") 59 | "hello......" 60 | 61 | Racket是一个词法作用域(lexically scoped)的语言,这意味着函数中的S2通过make-add-suffix总是指创建该函数调用的参数返回。换句话说,lambda生成的函数“记住”了右边的S2: 62 | 63 | > (define louder (make-add-suffix "!")) 64 | > (define less-sure (make-add-suffix "?")) 65 | > (twice less-sure "really") 66 | "really??" 67 | > (twice louder "really") 68 | "really!!" 69 | 70 | 到目前为止我们已经提到了表(define ‹ID›‹expr›)的定义。作为“非函数的定义“。“这种表征是误导性的,因为‹expr›可以lambda形式,在这种情况下,定义是等效于使用“功能”的定义形式。例如,下面两个更响亮的定义是等价的: 71 | 72 | (define (louder s) 73 | (string-append s "!")) 74 | 75 | (define louder 76 | (lambda (s) 77 | (string-append s "!"))) 78 | 79 | > louder 80 | 81 | # 82 | 注意,第二种情况下更响亮的表达式是用lambda写成的“匿名”(anonymous)函数,但如果可能的话,编译器推断出一个名称,无论如何,使打印和错误报告尽可能地有信息。 83 | 84 | -------------------------------------------------------------------------------- /02.2.8 用define、let和let*实现本地绑定: -------------------------------------------------------------------------------- 1 | 2.2.8 用define、let和let*实现局部绑定 2 | 3 | 现在是收起我们的Racket语法的另一个简化的时候了。在函数体中,定义可以出现在函数体表达式之前: 4 | 5 | ( define ( ‹id› ‹id›* ) ‹definition›* ‹expr›+ ) 6 | ( lambda ( ‹id›* ) ‹definition›* ‹expr›+ ) 7 | 8 | 函数体开始时的定义在函数体中是局部的。 9 | 10 | 例如: 11 | (define (converse s) 12 | (define (starts? s2) ; local to converse 13 | (define len2 (string-length s2)) ; local to starts? 14 | (and (>= (string-length s) len2) 15 | (equal? s2 (substring s 0 len2)))) 16 | (cond 17 | [(starts? "hello") "hi!"] 18 | [(starts? "goodbye") "bye!"] 19 | [else "huh?"])) 20 | 21 | > (converse "hello!") 22 | "hi!" 23 | > (converse "urp") 24 | "huh?" 25 | > starts? ; outside of converse, so... 26 | starts?: undefined; 27 | cannot reference undefined identifier 28 | 29 | 创建本地绑定的另一种方法是let表。let的优点是它可以在任何表达式位置使用。另外,let同时可以绑定多个标识符,而不是每个标识符都需要单独用define定义。 30 | 31 | ( let ( {[ ‹id› ‹expr› ]}* ) ‹expr›+ ) 32 | 33 | 每个约束条款是一个‹ID›和‹expr›方括号包围,和之后的从句表达的let函数体。在每一个条款,该‹ID›势必对应与函数体的‹expr›结果。 34 | 35 | > (let ([x (random 4)] 36 | [o (random 4)]) 37 | (cond 38 | [(> x o) "X wins"] 39 | [(> o x) "O wins"] 40 | [else "cat's game"])) 41 | 42 | "O wins" 43 | 44 | let表的绑定仅在let的函数体中可用,因此绑定子句不能互相引用。相反,let*表允许后面的子句使用前面的绑定: 45 | 46 | > (let* ([x (random 4)] 47 | [o (random 4)] 48 | [diff (number->string (abs (- x o)))]) 49 | (cond 50 | [(> x o) (string-append "X wins by " diff)] 51 | [(> o x) (string-append "O wins by " diff)] 52 | [else "cat's game"])) 53 | 54 | "cat's game" 55 | -------------------------------------------------------------------------------- /02.3 列表、迭代和递归: -------------------------------------------------------------------------------- 1 | 2.3 列表、迭代和递归 2 | 3 | Racket语言是Lisp的一种方言,名字来自于“LISt Processor”。内置的列表数据类型保持了这种语言的一个显著特征。 4 | 5 | list函数接受任意数量的值并返回包含值的列表: 6 | 7 | > (list "red" "green" "blue") 8 | '("red" "green" "blue") 9 | 10 | > (list 1 2 3 4 5) 11 | '(1 2 3 4 5) 12 | 13 | 你可以看到,一个列表的结果是作为引用(')打印在REPL中,并且采用一对圆括号包围的列表元素的打印表。这里有一个容易混淆的地方,因为两个表达式都使用圆括号,比如(list "red" "green" "blue"),那么打印结果为'("red" "green" "blue")。除了引用,括号中的结果在文档中和DrRacket中打印为蓝色的,而表达式的括号是棕色的。 14 | 15 | 在列表方面有许多预定义的函数操作。下面是几个例子: 16 | 17 | > (length (list "hop" "skip" "jump")) ; 计算元子个数 18 | 3 19 | 20 | > (list-ref (list "hop" "skip" "jump") 0) ; 通过位置提取 21 | "hop" 22 | 23 | > (list-ref (list "hop" "skip" "jump") 1) 24 | "skip" 25 | 26 | > (append (list "hop" "skip") (list "jump")) ; 结合列表 27 | '("hop" "skip" "jump") 28 | 29 | > (reverse (list "hop" "skip" "jump")) ; 反序 30 | '("jump" "skip" "hop") 31 | 32 | > (member "fall" (list "hop" "skip" "jump")) ;核查一个元子是否为成员 33 | #f 34 | -------------------------------------------------------------------------------- /02.3.1 预定义列表循环: -------------------------------------------------------------------------------- 1 | 2.3.1 预定义列表循环 2 | 3 | 除了像append这样的简单的操作,Racket还包括遍历列表元素的函数。这些迭代函数作用类似于java、Racket及其它语言里的for。Racket迭代的主体被打包成一个应用于每个元素的函数,所以lambda表在与迭代函数的组合中变得特别方便。 4 | 5 | 不同的列表迭代函数以不同的方式组合迭代结果。map函数使用每个元素结果创建一个新的列表: 6 | 7 | > (map sqrt (list 1 4 9 16)) 8 | '(1 2 3 4) 9 | 10 | > (map (lambda (i) 11 | (string-append i "!")) 12 | (list "peanuts" "popcorn" "crackerjack")) 13 | '("peanuts!" "popcorn!" "crackerjack!") 14 | 15 | andmap和ormap函数相结合,结果通过and或or决定: 16 | > (andmap string? (list "a" "b" "c")) 17 | #t 18 | 19 | > (andmap string? (list "a" "b" 6)) 20 | #f 21 | 22 | > (ormap number? (list "a" "b" 6)) 23 | #t 24 | 25 | map、andmap和ormap函数都可以处理多个列表,而不只是一个单一的列表。列表必须具有相同的长度,并且给定的函数必须接受每个列表元素作为参数: 26 | 27 | > (map (lambda (s n) (substring s 0 n)) 28 | (list "peanuts" "popcorn" "crackerjack") 29 | (list 6 3 7)) 30 | 31 | '("peanut" "pop" "cracker") 32 | 33 | filter函数保持函数体结果是真的元素,并忽略是#f的元素: 34 | 35 | > (filter string? (list "a" "b" 6)) 36 | '("a" "b") 37 | 38 | > (filter positive? (list 1 -2 6 7 0)) 39 | '(1 6 7) 40 | 41 | foldl函数包含某些迭代函数。它使用每个元素函数处理一个元素并将其与“当前”值相结合,因此每个元素函数接受额外的第一个参数。另外,在列表之前必须提供一个开始的“当前”值: 42 | 43 | > (foldl (lambda (elem v) 44 | (+ v (* elem elem))) 45 | 0 46 | '(1 2 3)) 47 | 14 48 | 49 | 尽管有其共性,foldl不是像其它函数一样受欢迎。一个原因是map, ormap, andmap,和filter覆盖最常见的列表迭代。 50 | 51 | Racket为列表提供了一个通用的列表解析表for/list,它通过迭代序列建立一个列表。列表解析和相关迭代表将在迭代和解析(Iterations and Comprehensions)部分解释。 52 | -------------------------------------------------------------------------------- /02.3.2 从头开始列表迭代: -------------------------------------------------------------------------------- 1 | 2.3.2 从头开始列表迭代 2 | 3 | 尽管map和其他迭代函数是预定义的,但它们在任何有趣的意义上都不是原始的。使用少量列表原语即能编写等效迭代。 4 | 5 | 由于Racket列表是一个链表,在非空列表中的两个核心操作是: 6 | first:取得列表上的第一件事物; 7 | rest:获取列表的其余部分。 8 | 9 | 例子: 10 | > (first (list 1 2 3)) 11 | 1 12 | 13 | > (rest (list 1 2 3)) 14 | '(2 3) 15 | 16 | 为链表添加一个新的节点——确切地说,添加到列表的前面——使用cons函数,那是“construct”(构造)的缩写。要得到一个空列表用于开始,用empty来构造: 17 | 18 | > empty 19 | '() 20 | 21 | > (cons "head" empty) 22 | '("head") 23 | 24 | > (cons "dead" (cons "head" empty)) 25 | '("dead" "head") 26 | 27 | 要处理列表,你需要能够区分空列表和非空列表,因为first和rest只在非空列表上工作。empty?函数检测空列表,cons? 检测非空列表: 28 | 29 | > (empty? empty) 30 | #t 31 | 32 | > (empty? (cons "head" empty)) 33 | #f 34 | 35 | > (cons? empty) 36 | #f 37 | 38 | > (cons? (cons "head" empty)) 39 | #t 40 | 41 | 通过这些片段,您可以编写自己的length函数、map函数以及更多的函数的版本。 42 | 43 | 例子: 44 | (define (my-length lst) 45 | (cond 46 | [(empty? lst) 0] 47 | [else (+ 1 (my-length (rest lst)))])) 48 | 49 | > (my-length empty) 50 | 0 51 | > (my-length (list "a" "b" "c")) 52 | 3 53 | 54 | (define (my-map f lst) 55 | (cond 56 | [(empty? lst) empty] 57 | [else (cons (f (first lst)) 58 | (my-map f (rest lst)))])) 59 | 60 | > (my-map string-upcase (list "ready" "set" "go")) 61 | '("READY" "SET" "GO") 62 | 63 | 如果上述定义的派生对你来说难以理解,建议去读《如何设计程序》(How to Design Programs)。如果您只对使用递归调用而不是循环结构表示疑惑,那就继续往后读。 64 | -------------------------------------------------------------------------------- /02.3.3 尾递归: -------------------------------------------------------------------------------- 1 | 2.3.3 尾递归 2 | 3 | 前面my-length和my-map自定义函数都在O(n)的时间内运行一个n长度的列表。很显然能够想象(my-length (list "a" "b" "c"))必须如下求值: 4 | 5 | (my-length (list "a" "b" "c")) 6 | = (+ 1 (my-length (list "b" "c"))) 7 | = (+ 1 (+ 1 (my-length (list "c")))) 8 | = (+ 1 (+ 1 (+ 1 (my-length (list))))) 9 | = (+ 1 (+ 1 (+ 1 0))) 10 | = (+ 1 (+ 1 1)) 11 | = (+ 1 2) 12 | = 3 13 | 14 | 对于带有n个元素的列表,求值将叠加n(+ 1…),并且直到列表用完时最后才添加它们。 15 | 16 | 您可以通过一路求和避免堆积添加。要以这种方式累积长度,我们需要一个函数,它既可以操作列表,也可以操作当前列表的长度;下面的代码使用一个局部函数iter,在一个参数len中累积长度: 17 | 18 | (define (my-length lst) 19 | ; local function iter: 20 | (define (iter lst len) 21 | (cond 22 | [(empty? lst) len] 23 | [else (iter (rest lst) (+ len 1))])) 24 | ; body of my-length calls iter: 25 | (iter lst 0)) 26 | 27 | 现在求值过程看起来像这样: 28 | (my-length (list "a" "b" "c")) 29 | = (iter (list "a" "b" "c") 0) 30 | = (iter (list "b" "c") 1) 31 | = (iter (list "c") 2) 32 | = (iter (list) 3) 33 | 3 34 | 35 | 修正后的my-length函数在恒定的空间中运行,正如上面的求值步骤所表明的那样。也就是说,当函数调用的结果,比如(iter (list "b" "c") 1),确切地说是其他函数调用的结果,例如(iter (list "c") 2),那么第一个函数不需要等待第二个函数回绕,因为那样会为了不恰当的原因占用空间。 36 | 37 | 这种求值行为有时称为”尾部调用优化“,但它在Racket里不仅仅是一种“优化”,它是代码运行方式的保证。更确切地说,相对于另一表达式的尾部位置表达式在另一表达式上不占用额外的计算空间。 38 | 39 | 在my-map例子中,O(n)空间复杂度是合理的,因为它必须生成O(n)的结果。不过,您可以通过累积结果列表来减少常数因子。唯一的问题是累积的列表将是向后的,所以你必须在结尾处反转它: 40 | 41 | (define (my-map f lst) 42 | (define (iter lst backward-result) 43 | (cond 44 | [(empty? lst) (reverse backward-result)] 45 | [else (iter (rest lst) 46 | (cons (f (first lst)) 47 | backward-result))])) 48 | (iter lst empty)) 49 | 50 | 事实证明,如果你这样写: 51 | 52 | (define (my-map f lst) 53 | (for/list ([i lst]) 54 | (f i))) 55 | 56 | 然后函数中的for/list表扩展到和iter函数局部定义和使用在本质上相同的代码。区别仅仅是句法上的便利。 57 | -------------------------------------------------------------------------------- /02.3.4 递归和迭代: -------------------------------------------------------------------------------- 1 | 2.3.4 递归和迭代 2 | 3 | my-length和my-map示例表明迭代只是递归的一个特例。在许多语言中,尽可能地将尽可能多的计算合并成迭代形式是很重要的。否则,性能会变差,适度大的输入都会导致堆栈溢出。同样地,在Racket中,有时很重要的一点是要确保在易于计算的常数空间中使用尾递归避免O(n)空间消耗。 4 | 5 | 同时,在Racket里递归不会导致特别差的性能,而且没有堆栈溢出那样的事情;如果计算涉及到太多的上下文,你可能耗尽内存,但耗尽内存通常需要比可能触发其他语言中的堆栈溢出更多数量级以上的更深层次的递归。基于这些考虑因素,加上尾递归程序自动与循环运行的事实相结合,导致Racket程序员接受递归形式而不是避免它们。 6 | 7 | 例如,假设您希望从列表中删除连续的重复项。虽然这样的函数可以写成一个循环,每次迭代都记得以前的元素,但Racket程序员更可能只写以下内容: 8 | 9 | (define (remove-dups l) 10 | (cond 11 | [(empty? l) empty] 12 | [(empty? (rest l)) l] 13 | [else 14 | (let ([i (first l)]) 15 | (if (equal? i (first (rest l))) 16 | (remove-dups (rest l)) 17 | (cons i (remove-dups (rest l)))))])) 18 | 19 | 20 | > (remove-dups (list "a" "b" "b" "b" "c" "c")) 21 | '("a" "b" "c") 22 | 23 | 一般来说,这个函数为长度n的输入列表消耗O(n)空间,但这很好,因为它产生一个O(n)结果。如果输入列表恰巧是连续重复的,然后得到的列表可以比O(n)小得多——而且remove-dups也将使用比O(n)更少的空间!原因是当函数放弃重复,它返回一个remove-dups的直接调用结果,所以尾部调用“优化”加入: 24 | 25 | (remove-dups (list "a" "b" "b" "b" "b" "b")) 26 | = (cons "a" (remove-dups (list "b" "b" "b" "b" "b"))) 27 | = (cons "a" (remove-dups (list "b" "b" "b" "b"))) 28 | = (cons "a" (remove-dups (list "b" "b" "b"))) 29 | = (cons "a" (remove-dups (list "b" "b"))) 30 | = (cons "a" (remove-dups (list "b"))) 31 | = (cons "a" (list "b")) 32 | = (list "a" "b") 33 | -------------------------------------------------------------------------------- /02.4 pair、list和Racket的语法: -------------------------------------------------------------------------------- 1 | 2.4 pair、list和Racket的语法 2 | 3 | cons函数实际上接受任何两个值,而不只是第二个参数的列表。当第二个参数不是empty,并且不是自己通过cons产生,结果以一种特殊的方式打印出来。两值加入cons被打印在括号之间,但在两者之间有一个点(即,一个被空格环绕的句点): 4 | 5 | > (cons 1 2) 6 | '(1 . 2) 7 | 8 | > (cons "banana" "split") 9 | '("banana" . "split") 10 | 11 | 因此,由cons产生的值并不总是列表。一般来说,cons的结果是一个对(pair)。更传统的cons?函数的名字是pair?,我们从现在开始使用传统的名字。 12 | 13 | 名字rest对非列表对没有意义;first和rest更传统的名字分别是car和cdr。(当然,传统的名字也是没有意义。)。请记住,“a”出现在“d”之前,cdr被声明为“可以”。 14 | 15 | 例子: 16 | > (car (cons 1 2)) 17 | 1 18 | 19 | > (cdr (cons 1 2)) 20 | 2 21 | 22 | > (pair? empty) 23 | #f 24 | 25 | > (pair? (cons 1 2)) 26 | #t 27 | 28 | > (pair? (list 1 2 3)) 29 | #t 30 | 31 | Racket对数据类型和表的关系本质上是一种历史的好奇心,连同打印的点符号和滑稽的名字car和cdr。但是,对在Racket的文化、规范和实施有着深刻的联系,所以他们能在语言中生存下来。 32 | 33 | 在犯错误时你很可能会遇到非列表对,例如不小心把参数颠倒过来cons: 34 | 35 | > (cons (list 2 3) 1) 36 | '((2 3) . 1) 37 | 38 | > (cons 1 (list 2 3)) 39 | '(1 2 3) 40 | 41 | 非列表对有时是有意使用的。例如, make-hash函数取得一个对的列表,其中每个对的car是一个键,cdr是任意值。 42 | 43 | 对新的Racket程序员唯一更困惑的情况莫过于非列表对是对对的打印习惯,第二个元素是一个对而不是一个列表: 44 | 45 | > (cons 0 (cons 1 2)) 46 | '(0 1 . 2) 47 | 48 | 一般来说,打印一个对的规则如下:除非该点紧接着是一个开括号,否则使用点表示法。在这种情况下,去掉点、开括号和匹配的括号。由此,“(0 . (1 . 2)“变成”(0 1 . 2),(1 . (2 . (3 . ())))变成为'(1 2 3)。 49 | -------------------------------------------------------------------------------- /02.4.1 用quote引用pair和symbol: -------------------------------------------------------------------------------- 1 | 2.4.1 用quote引用pair和symbol 2 | 3 | 一个列表在前面打印一个引号,但是如果一个列表的元素本身是一个列表,那么就不会为内部列表打印引号: 4 | 5 | > (list (list 1) (list 2 3) (list 4)) 6 | '((1) (2 3) (4)) 7 | 8 | 对于嵌套列表,尤其是quote表,你可以将列表作为表达式来写,基本上与列表打印的方式相同: 9 | 10 | > (quote ("red" "green" "blue")) 11 | '("red" "green" "blue") 12 | 13 | > (quote ((1) (2 3) (4))) 14 | '((1) (2 3) (4)) 15 | 16 | > (quote ()) 17 | '() 18 | 19 | 无论引用表是否由点括号消除规则规范,quote表都要包含点符号: 20 | 21 | > (quote (1 . 2)) 22 | '(1 . 2) 23 | 24 | > (quote (0 . (1 . 2))) 25 | '(0 1 . 2) 26 | 27 | 当然,任何种类的列表都可以嵌套: 28 | 29 | > (list (list 1 2 3) 5 (list "a" "b" "c")) 30 | '((1 2 3) 5 ("a" "b" "c")) 31 | 32 | > (quote ((1 2 3) 5 ("a" "b" "c"))) 33 | '((1 2 3) 5 ("a" "b" "c")) 34 | 35 | 如果用quote包装标识符,则得到看起来像标识符的输出,但带有前缀: 36 | 37 | > (quote jane-doe) 38 | 'jane-doe 39 | 40 | 像引用标识符那样打印的值是符号(symbol)。同样,括号输出不应该和表达式混淆,一个打印符号不应与一个标识符混淆。特别是,符号(quote map)与map标识符或绑定到map的预定义函数无关,除了符号和标识符碰巧由相同的字母组成。 41 | 42 | 的确,符号的内在价值不过是它的字符内容。从这个意义上说,符号和字符串几乎是一样的东西,主要区别在于它们是如何打印的。symbol->string和 string->symbol在它们之间转换。 43 | 44 | 例子: 45 | > map 46 | # 47 | 48 | > (quote map) 49 | 'map 50 | 51 | > (symbol? (quote map)) 52 | #t 53 | 54 | > (symbol? map) 55 | #f 56 | 57 | > (procedure? map) 58 | #t 59 | 60 | > (string->symbol "map") 61 | 'map 62 | 63 | > (symbol->string (quote map)) 64 | "map" 65 | 66 | 在同样,对一个列表的quote会自动作用于它自己的嵌套列表,quote在一个括号序列的标识符会自动应用到它自身的标识符以创建一个符号表: 67 | 68 | > (car (quote (road map))) 69 | 'road 70 | 71 | > (symbol? (car (quote (road map)))) 72 | #t 73 | 74 | 当一个符号在一个打印有‘的列表中时,在符号上的这个’被省略了,因为‘已经在做这项工作了: 75 | 76 | > (quote (road map)) 77 | '(road map) 78 | 79 | quote表对文字表达式(如数字或字符串)没有影响: 80 | 81 | > (quote 42) 82 | 42 83 | 84 | > (quote "on the record") 85 | "on the record" 86 | -------------------------------------------------------------------------------- /02.4.2 使用’缩写quote: -------------------------------------------------------------------------------- 1 | 2.4.2 使用’缩写quote 2 | 3 | 你可能已经猜到了,你可以通过在一个表前面仅放置一个'来缩写一个quote的应用: 4 | 5 | > '(1 2 3) 6 | '(1 2 3) 7 | 8 | > 'road 9 | 'road 10 | 11 | > '((1 2 3) road ("a" "b" "c")) 12 | '((1 2 3) road ("a" "b" "c")) 13 | 14 | 在文档中,‘在表达式中和后面的表单一起被打印成绿色,因为这个组合是一个常量表达式。在DrRacket,只有’是绿色的。DrRacket更正确,因为quote的意义可以根据表达式的上下文而变化。然而,在文档中,我们经常假定标准绑定在作用域中,因此我们为了更清晰将引用的表用绿色绘制。 15 | 16 | 一个‘以字面方式扩展成一个quote表。你能够明白如果你在一个表前面放置一个’那么就有了一个‘: 17 | 18 | > (car ''road) 19 | 'quote 20 | 21 | > (car '(quote road)) 22 | 'quote 23 | 24 | ’缩写在输出和输入中起作用。在打印输出时,REPL打印机识别符号'quote的两元素列表的第一个元素,在这种情况下,它使用‘的打印输出: 25 | 26 | > (quote (quote road)) 27 | ''road 28 | 29 | > '(quote road) 30 | ''road 31 | 32 | > ''road 33 | ''road 34 | -------------------------------------------------------------------------------- /02.4.3 列表和Racket的语法: -------------------------------------------------------------------------------- 1 | 2.4.3 列表和Racket的语法 2 | 3 | 现在你已经知道了关于配对和列表的真相,而且现在你已经明白了quote,你已经准备好理解我们一直在简化Racket真实语法的主要方法。 4 | 5 | Racket的语法并不是直接用字符流来定义的。相反,语法是由两层确定的: 6 | 7 | 1、读取器层,将字符序列转换成列表、符号和其他常量; 8 | 2、一个扩展层,它处理列表、符号和其他常量,将它们解析为表达式。 9 | 10 | 打印和阅读的规则是一致的。例如,一个列表用圆括号打印,读一对圆括号生成一个列表。类似地,一个非列表对用点表示法打印,输入的一个点有效地运行点标记规则从反向得到一个配对。 11 | 12 | 表达式读取层的一个结果是,可以在不引用的表达式中使用点标记: 13 | 14 | > (+ 1 . (2)) 15 | 3 16 | 17 | 这是因为(+ 1 . (2))只是(+ 1 2)的另一种法。用这种点表示法编写应用程序表达式实际上并不是一个好主意,它只是定义了Racket语法的一个结果。 18 | 19 | 通常,在括号序列里只有一个.是被读者允许的,并且只有在序列的最后一个元素。然而,一对.也可以出现在一个括号序列的单个元素周围,只要不是第一个或最后一个元素。这样的一对触发阅读器转换,将元素从.移动到列表的前面。转换可以使一种普遍的中缀表示法成为可能: 20 | 21 | > (1 . < . 2) 22 | #t 23 | 24 | > '(1 . < . 2) 25 | '(< 1 2) 26 | 27 | 这两个点约定是非传统的,它与非列表对的点记法基本上没有关系。Racket程序员使用中缀标记——多为非对称二元操作符如<和is-a?。 28 | -------------------------------------------------------------------------------- /03 内置的数据类型: -------------------------------------------------------------------------------- 1 | 3 内置的数据类型 2 | 3 | 上一章介绍了Racket的内置数据类型:数字、布尔值、字符串、列表、和程序。本节提供了简单的数据表的内置数据表更完整的覆盖。 4 | 5 | 3.1 布尔值(Boolean) 6 | 3.2 数值(Number) 7 | 3.3 字符(Character) 8 | 3.4 字符串(Unicode Strings) 9 | 3.5 字节(Byte)和字节字符串(Byte String) 10 | 3.6 符号(Symbol) 11 | 3.7 关键字(Keyword) 12 | 3.8 配对(Pair)和列表(List) 13 | 3.9 向量(Vector) 14 | 3.10 哈希表(Hash Table) 15 | 3.11 格子(Box) 16 | 3.12 无效值(Void)和未定义值(Undefined) 17 | -------------------------------------------------------------------------------- /03.01 布尔值(Boolean): -------------------------------------------------------------------------------- 1 | 3.1 布尔值(Boolean) 2 | 3 | Racket有表示布尔值的两个常数:#f表示真,#f表示假。大写的#T和#F在语法上描述为同样的值,但小写形式是首选。 4 | 5 | boolean? 过程识别两个布尔常量。然而,在对if、cond、and、or等等的测试表达式的结果里,除了#f外,任何值都是记为真。 6 | 7 | 比如: 8 | > (= 2 (+ 1 1)) 9 | #t 10 | 11 | > (boolean? #t) 12 | #t 13 | 14 | > (boolean? #f) 15 | #t 16 | 17 | > (boolean? "no") 18 | #f 19 | 20 | > (if "no" 1 0) 21 | 1 22 | -------------------------------------------------------------------------------- /03.03 字符(Character): -------------------------------------------------------------------------------- 1 | 3.3 字符(Character) 2 | 3 | Racket字符对应于Unicode标量值。粗略地说,一个标量值是一个无符号整数,它的表示适合21位,并且映射到某种自然语言字符或字符块的某些概念。从技术上讲,标量值是比Unicode标准中的“字符”概念更简单的概念,但它是一种用于许多目的的近似值。例如,任何重音罗马字母都可以表示为一个标量值,就像任何普通的汉字一样。 4 | 5 | 虽然每个Racket字符对应一个整数,但字符数据类型和数值是有区别的。char->integer和integer->char程序在标量值和相应字符之间转换。 6 | 7 | 一个可打印字符通常在以#\作为代表字符后打印。一个不可打印字符通常在以#\u开始十六进制数的标量值打印。几个字符特殊打印;例如,空格和换行字符分别打印为#\space和#\newline。 8 | 9 | 例如: 10 | 11 | > (integer->char 65) 12 | #\A 13 | 14 | > (char->integer #\A) 15 | 65 16 | 17 | > #\λ 18 | #\λ 19 | 20 | > #\u03BB 21 | #\λ 22 | 23 | > (integer->char 17) 24 | #\u0011 25 | 26 | > (char->integer #\space) 27 | 32 28 | 29 | display程序直接将字符写入当前输出端口(见《输入和输出》部分)( Input and Output),与用于打印字符结果的字符常量语法形成对照。 30 | 31 | 例如: 32 | > #\A 33 | #\A 34 | 35 | > (display #\A) 36 | A 37 | 38 | Racket提供了几种分类和转换字符的程序。注意,然而,某些Unicode字符要如人类希望的那样转换只有在一个字符串中才行(例如,”ß”的大写转换或者”Σ”的小写转换)。 39 | 40 | 例如: 41 | > (char-alphabetic? #\A) 42 | #t 43 | > (char-numeric? #\0) 44 | #t 45 | > (char-whitespace? #\newline) 46 | #t 47 | > (char-downcase #\A) 48 | #\a 49 | > (char-upcase #\ß) 50 | #\ß 51 | 52 | char=?程序比较两个或多个字符,char-ci=?比较忽略字符。eqv?和equal?程序的行为与char=?在字符方面的表现一样;当更具体地声明正在比较的值是字符时使用char=?。 53 | 54 | 例如: 55 | > (char=? #\a #\A) 56 | #f 57 | > (char-ci=? #\a #\A) 58 | #t 59 | > (eqv? #\a #\A) 60 | #f 61 | -------------------------------------------------------------------------------- /03.04 字符串(Unicode Strings): -------------------------------------------------------------------------------- 1 | 3.4 字符串(Unicode Strings) 2 | 3 | 字符串是固定长度的字符(characters)数组。它打印使用双引号,双引号和反斜杠字符在字符串中是用反斜杠转义。其他常见的字符串转义是支持的,包括\n换行,\r回车,使用\后边跟随三个八进制数字实现八进制转义,使用\u(达四位数)实现十六进制转义。在打印字符串时通常用\u显示字符串中的不可打印字符。 4 | 5 | display程序直接将字符串的字符写入当前输出端口(见《输入和输出》)(Input and Output),与打印字符串结果的字符串常量语法形成对照。 6 | 7 | 例如: 8 | > "Apple" 9 | "Apple" 10 | > "\u03BB" 11 | "λ" 12 | > (display "Apple") 13 | Apple 14 | > (display "a \"quoted\" thing") 15 | a "quoted" thing 16 | > (display "two\nlines") 17 | two 18 | lines 19 | > (display "\u03BB") 20 | λ 21 | 22 | 字符串可以是可变的,也可以是不可变的;作为表达式直接写入的字符串是不可变的,但大多数其他字符串是可变的。 make-string程序创建一个给定长度和可选填充字符的可变字符串。string-ref程序从字符串(用0字符串集索引)存取一个字符。string-set!程序更改可变字符串中的一个字符。 23 | 24 | 例如: 25 | > (string-ref "Apple" 0) 26 | #\A 27 | > (define s (make-string 5 #\.)) 28 | > s 29 | "....." 30 | > (string-set! s 2 #\λ) 31 | > s 32 | "..λ.." 33 | 34 | 字符串排序和状态操作通常是区域无关(locale-independent)的,也就是说,它们对所有用户都是相同的。提供了一些与区域相关(locale-dependent)的操作,允许字符串折叠和排序的方式取决于最终用户的区域设置。如果你在排序字符串,例如,如果排序结果应该在机器和用户之间保持一致,使用string (string (string-ci (string-upcase "Straße") 42 | "STRASSE" 43 | > (parameterize ([current-locale "C"]) 44 | (string-locale-upcase "Straße")) 45 | "STRAßE" 46 | 47 | 对于使用纯ASCII、处理原始字节、或将Unicode字符串编码/解码为字节,使用字节字符串(byte strings)。 48 | -------------------------------------------------------------------------------- /03.05 字节(Byte)和字节字符串(Byte String): -------------------------------------------------------------------------------- 1 | 3.5 字节(Byte)和字节字符串(Byte String) 2 | 3 | 字节是包含0到255之间的精确整数。byte?判断表示字节的数字。 4 | 5 | 例如: 6 | > (byte? 0) 7 | #t 8 | > (byte? 256) 9 | #f 10 | 11 | 字节字符串类似于字符串——参见《字符串(Unicode)》部分,但它的内容是字节序列而不是字符。字节字符串可用于处理纯ASCII而不是Unicode文本的应用程序中。一个字节的字符串打印形式特别支持这样的用途,因为一个字节的字符串打印的ASCII的字节字符串解码,但有一个#前缀。在字节字符串不可打印的ASCII字符或非ASCII字节用八进制表示法。 12 | 13 | 例如: 14 | > #"Apple" 15 | #"Apple" 16 | 17 | > (bytes-ref #"Apple" 0) 18 | 65 19 | 20 | > (make-bytes 3 65) 21 | #"AAA" 22 | 23 | > (define b (make-bytes 2 0)) 24 | > b 25 | #"\0\0" 26 | 27 | > (bytes-set! b 0 1) 28 | > (bytes-set! b 1 255) 29 | > b 30 | #"\1\377" 31 | 32 | 一个字节字符串的display表写入其原始字节的电流输出端口(看《输入和输出》(Input and Output)部分)。从技术上讲,一个正常的display(即,字符编码的字符串)字符串打印到当前输出端口的UTF-8,因为产出的最终依据字节的定义;然而一个字节的字符串,显示,没有编码写入原始字节。同样,当这个文件显示输出,技术上显示输出的utf-8编码格式。 33 | 34 | 例如: 35 | > (display #"Apple") 36 | Apple 37 | > (display "\316\273") ; 等同于"λ" 38 | λ 39 | > (display #"\316\273") ; λ的UTF-8编码 40 | λ 41 | 42 | 字符串和字节字符串之间的显式转换,Racket直接支持三种编码:UTF-8,Latin-1,和当前的本地编码。字节到字节的通用转换器(特别是从UTF-8)弥合了支持任意字符串编码的差异分歧。 43 | 44 | 例如: 45 | > (bytes->string/utf-8 #"\316\273") 46 | "λ" 47 | 48 | > (bytes->string/latin-1 #"\316\273") 49 | "λ" 50 | 51 | > (parameterize ([current-locale "C"]) ; C locale supports ASCII, 52 | (bytes->string/locale #"\316\273")) ; only, so... 53 | bytes->string/locale: byte string is not a valid encoding 54 | for the current locale 55 | byte string: #"\316\273" 56 | 57 | > (let ([cvt (bytes-open-converter "cp1253" ; Greek code page 58 | "UTF-8")] 59 | [dest (make-bytes 2)]) 60 | (bytes-convert cvt #"\353" 0 1 dest) 61 | (bytes-close-converter cvt) 62 | (bytes->string/utf-8 dest)) 63 | 64 | "λ" 65 | -------------------------------------------------------------------------------- /03.06 符号(Symbol): -------------------------------------------------------------------------------- 1 | 3.6 符号(Symbol) 2 | 3 | 符号是一个原子值,它像前面的标识符那样以'前缀打印。一个表达式以'开始并以标识符继续表达式产生一个符号值。 4 | 5 | 例如: 6 | > 'a 7 | 'a 8 | > (symbol? 'a) 9 | #t 10 | 11 | 对于任何字符序列,一个相应的符号被保留;调用string->symbol程序,或读入一个语法标识,产生一个保留符号。由于互联网的符号可以方便地用eq?(或这样:eqv?或equal?)进行比较,所以他们作为一个易于使用的标签和枚举值提供。 12 | 13 | 符号是区分大小写的。通过使用一个#ci前缀或其他方式,在读者保留默认情况下,读者可以将大小写字符序列生成一个符号。 14 | 15 | 例如: 16 | > (eq? 'a 'a) 17 | #t 18 | > (eq? 'a (string->symbol "a")) 19 | #t 20 | > (eq? 'a 'b) 21 | #f 22 | > (eq? 'a 'A) 23 | #f 24 | > #ci'A 25 | 'a 26 | 27 | 任何字符串(即,任何字符序列)都可以提供给string->symbol以获得相应的符号。读者输入任何字符都可以直接出现在一个标识符里,除了空白和以下特殊字符: 28 | 29 | ( ) [ ] { } ” , “ ' ` ; # | \ 30 | 31 | 实际上,#只有在一个符号开始是不允许的,或者仅仅如果随后是%;然而,#也被允许。同样。.它本身不是一个符号。 32 | 33 | 空格或特殊字符可以通过用|或\引用包含标识符。这些引用机制用于包含特殊字符或可能看起来像数字的标识符的打印形式中。 34 | 35 | 例如: 36 | > (string->symbol "one, two") 37 | '|one, two| 38 | 39 | > (string->symbol "6") 40 | '|6| 41 | 42 | write函数打印一个没有‘前缀的符号。一个符号的display表与相应的字符串相同。 43 | 44 | 例如: 45 | > (write 'Apple) 46 | Apple 47 | > (display 'Apple) 48 | Apple 49 | > (write '|6|) 50 | |6| 51 | > (display '|6|) 52 | 6 53 | 54 | gensym和string->uninterned-symbol程序产生新的非保留(uninterned)符号,那不等同(比照eq?)于任何先前的保留或非保留符号。非保留符号是可用的新标签,不能与任何其它值混淆。 55 | 56 | 例如: 57 | > (define s (gensym)) 58 | > s 59 | 'g42 60 | > (eq? s 'g42) 61 | #f 62 | > (eq? 'a (string->uninterned-symbol "a")) 63 | #f 64 | -------------------------------------------------------------------------------- /03.07 关键字(Keyword): -------------------------------------------------------------------------------- 1 | 3.7 关键字(Keyword) 2 | 3 | 一个关键字值是类似于一个符号(见《符号(Symbols)》),但它的印刷形式是用前缀#:。 4 | 5 | 例如: 6 | > (string->keyword "apple") 7 | '#:apple 8 | 9 | > '#:apple 10 | '#:apple 11 | 12 | > (eq? '#:apple (string->keyword "apple")) 13 | #t 14 | 15 | 更确切地说,关键字类似于标识符;以同样的方式,可以引用标识符来生成符号,可以引用关键字来生成值。在这两种情况下都使用同一术语“关键字”,但有时我们使用关键字值更具体地引用引号关键字表达式或使用string->keyword程序的结果。一个不带引号的关键字不是表达式,只是作为一个不带引号的标识符不产生符号: 16 | 17 | 例如: 18 | > not-a-symbol-expression 19 | not-a-symbol-expression: undefined; 20 | cannot reference undefined identifier 21 | 22 | > #:not-a-keyword-expression 23 | eval:2:0: #%datum: keyword used as an expression 24 | in: #:not-a-keyword-expression 25 | 26 | 尽管它们有相似之处,但关键字的使用方式不同于标识符或符号。关键字是为了使用(不带引号)作为参数列表和在特定的句法形式的特殊标记。运行时的标记和枚举,而不是关键字用符号。下面的示例说明了关键字和符号的不同角色。 27 | 28 | 例如: 29 | > (define dir (find-system-path 'temp-dir)) ; 不是符号 '#:temp-dir 30 | > (with-output-to-file (build-path dir "stuff.txt") 31 | (lambda () (printf "example\n")) 32 | ; 可选项#:mode参数,可以是 'text or 'binary 33 | #:mode 'text 34 | ; 可选项#:exists参数,可以是 'replace, 'truncate, ... 35 | #:exists 'replace) 36 | -------------------------------------------------------------------------------- /03.09 向量(Vector): -------------------------------------------------------------------------------- 1 | 3.9 向量(Vector) 2 | 3 | 向量是任意值的固定长度数组。与列表不同,向量支持常量时间访问和元素更新。 4 | 5 | 向量打印类似列表——作为其元素的括号序列——但向量要在'之后加前缀#,或如果某个元素不能用引号则使用vector表示。 6 | 7 | 向量作为表达式,可以提供可选长度。同时,一个向量作为一个隐式引用的形式表达的内容,这意味着在一个矢量常数标识符和括号表表示的符号和列表。 8 | 9 | 例如: 10 | > #("a" "b" "c") 11 | '#("a" "b" "c") 12 | 13 | > #(name (that tune)) 14 | '#(name (that tune)) 15 | 16 | > #4(baldwin bruce) 17 | '#(baldwin bruce bruce bruce) 18 | 19 | > (vector-ref #("a" "b" "c") 1) 20 | "b" 21 | 22 | > (vector-ref #(name (that tune)) 1) 23 | '(that tune) 24 | 25 | 像字符串一样,向量要么是可变的,要么是不可变的,直接作为表达式编写的向量是不可变的。 26 | 27 | 向量可以通过vector->list和list->vector转换成列表,反之亦然。这种转换与列表中预定义的程序相结合特别有用。当分配额外的列表似乎太昂贵时,考虑使用像for/fold的循环形式,它像列表一样识别向量。 28 | 29 | 例如: 30 | > (list->vector (map string-titlecase 31 | (vector->list #("three" "blind" "mice")))) 32 | 33 | '#("Three" "Blind" "Mice") 34 | -------------------------------------------------------------------------------- /03.10 哈希表(Hash Table): -------------------------------------------------------------------------------- 1 | 3.10 哈希表(Hash Table) 2 | 3 | 哈希表实现了从键到值的映射,其中键和值可以是任意的Racket值,而对表的访问和更新通常是常量时间操作。键的比较用equal?、eqv?或eq?,取决于哈希表的键创建方式为make-hash、make-hasheqv或是make-hasheq。 4 | 5 | 例如: 6 | > (define ht (make-hash)) 7 | > (hash-set! ht "apple" '(red round)) 8 | > (hash-set! ht "banana" '(yellow long)) 9 | > (hash-ref ht "apple") 10 | '(red round) 11 | 12 | > (hash-ref ht "coconut") 13 | hash-ref: no value found for key 14 | key: "coconut" 15 | 16 | > (hash-ref ht "coconut" "not there") 17 | "not there" 18 | 19 | hash、hasheqv和hasheq函数创建不可变的哈希表的键和值的初始设置,其中每个值在键后提供一个参数。不可变的哈希表可通过hash-set扩展,在恒定的时间里产生一个新的不可变的哈希表。 20 | 21 | 例如: 22 | > (define ht (hash "apple" 'red "banana" 'yellow)) 23 | > (hash-ref ht "apple") 24 | 'red 25 | 26 | > (define ht2 (hash-set ht "coconut" 'brown)) 27 | > (hash-ref ht "coconut") 28 | hash-ref: no value found for key 29 | key: "coconut" 30 | > (hash-ref ht2 "coconut") 31 | 'brown 32 | 33 | 一个原意的不可变哈希表可以写为一个表达式,使用#hash(以equal?为基础的表)、#hasheqv(以eqv?为基础的表)或#hasheq(以eq?为基础的表)。一个括号序列必须紧跟#hash、#hasheq或#hasheqv,其中每个元素是一个点的键–值对。这个#hash等其它表都暗含quote它们的键和值的子表。 34 | 35 | 例如: 36 | > (define ht #hash(("apple" . red) 37 | ("banana" . yellow))) 38 | > (hash-ref ht "apple") 39 | 'red 40 | 41 | 可变和不可变的哈希表都像不可变的哈希表一样打印,如果所有的键和值可以通过引用或使用别的#hash、#hasheq或#hasheqv,那么使用一个被引用的#hash、#hasheqv或#hasheq表: 42 | 43 | 例如: 44 | > #hash(("apple" . red) 45 | ("banana" . yellow)) 46 | '#hash(("apple" . red) ("banana" . yellow)) 47 | 48 | > (hash 1 (srcloc "file.rkt" 1 0 1 (+ 4 4))) 49 | (hash 1 (srcloc "file.rkt" 1 0 1 8)) 50 | 51 | 可变哈希表可以选择性地弱方式(weakly)保留其键,因此只要保留在其它地方的键,每个映射都被保留。 52 | 53 | 例如: 54 | > (define ht (make-weak-hasheq)) 55 | > (hash-set! ht (gensym) "can you see me?") 56 | > (collect-garbage) 57 | > (hash-count ht) 58 | 0 59 | 60 | 请注意,即使是弱哈希表,只要对应的键是可访问的,它的值也很强健。当一个值指回到它的键,就造成了一个两难的依赖,以致这个映射永久保持。要打破这个循环,映射一个键到一个暂存值(ephemeron),配对它的键和值(除这个隐配对的哈希表之外)。 61 | 62 | 例如: 63 | > (define ht (make-weak-hasheq)) 64 | > (let ([g (gensym)]) 65 | (hash-set! ht g (list g))) 66 | > (collect-garbage) 67 | > (hash-count ht) 68 | 1 69 | 70 | > (define ht (make-weak-hasheq)) 71 | > (let ([g (gensym)]) 72 | (hash-set! ht g (make-ephemeron g (list g)))) 73 | > (collect-garbage) 74 | > (hash-count ht) 75 | 0 76 | -------------------------------------------------------------------------------- /03.11 格子(Box): -------------------------------------------------------------------------------- 1 | 3.11 格子(Box) 2 | 3 | 一个格子是一个单元素矢量。它可以打印成一个引用#&后边跟随这个格子值的打印表。一个#&表也可以用来作为一种表达,但由于作为结果的格子是常量,它实际上没有使用。 4 | 5 | 例如: 6 | > (define b (box "apple")) 7 | > b 8 | '#&"apple" 9 | 10 | > (unbox b) 11 | "apple" 12 | 13 | > (set-box! b '(banana boat)) 14 | > b 15 | '#&(banana boat) 16 | -------------------------------------------------------------------------------- /03.12 空值(Void)和未定义值(Undefined): -------------------------------------------------------------------------------- 1 | 3.12 空值(Void)和未定义值(Undefined) 2 | 3 | 某些过程或表达式形式不需要结果值。例如,display程序仅调用输出的副作用。在这样的情况下,得到的值通常是一个特殊的常量,打印为#。当一个表达式的结果是简单的#,REPL不打印任何东西。 4 | 5 | void程序接受任意数量的参数并返回#。(即,void标识符绑定到一个返回#的程序,而不是直接绑定到#。) 6 | 7 | 例如: 8 | > (void) 9 | > (void 1 2 3) 10 | > (list (void)) 11 | '(#) 12 | 13 | undefined常量,它打印为#,有时是作为一个参考的结果,其值是不可用的。在Racket以前的版本(6.1以前的版本),过早参照一个局部绑定会产生#;而不是像太早的参照现在会引发一个异常。 14 | 15 | (define (fails) 16 | (define x x) 17 | x) 18 | 19 | > (fails) 20 | x: undefined; 21 | cannot use before initialization 22 | -------------------------------------------------------------------------------- /04 表达式和定义: -------------------------------------------------------------------------------- 1 | 4 表达式和定义 2 | 3 | 《Racket语言概要》这一章介绍了一些基本的Racket的句法形式:定义、程序、条件表达式等。本节提供这些形式的更详细的信息,以及一些附加的基本形式。 4 | 5 | 4.1 符号 6 | 4.2 标识符和绑定 7 | 4.3 函数调用(过程程序) 8 | 4.3.1 赋值顺序和数量 9 | 4.3.2 关键字参数 10 | 4.3.3 apply函数 11 | 4.4 lambda函数(程序) 12 | 4.4.1 申明剩余(rest)参数 13 | 4.4.2 声明可选(optional)参数 14 | 4.4.3 声明关键字(keyword)参数 15 | 4.4.4 多解函数:case-lambda 16 | 4.5 定义:define 17 | 4.5.1 函数速记法 18 | 4.5.2 特殊功能速记法 19 | 4.5.3 多值和define-values 20 | 4.5.4 内部定义 21 | 4.6 本地绑定 22 | 4.6.1 并行绑定:let 23 | 4.6.2 顺序绑定:let* 24 | 4.6.3 递归绑定:letrec 25 | 4.6.4 命名let 26 | 4.6.5 多值绑定:let-values,let*-values,letrec-values 27 | 4.7 条件分支 28 | 4.7.1 简单分支:if 29 | 4.7.2 组合测试:and和or 30 | 4.7.3 约束测试:cond 31 | 4.8 排序 32 | 4.8.1 前置效应:begin 33 | 4.8.2 后置效应:begin0 34 | 4.8.3 条件效应:when和unless 35 | 4.9 赋值:set! 36 | 4.9.1 使用的指导原则 37 | 4.9.2 多值赋值:set!-values 38 | 4.10 引用:quote和' 39 | 4.11 类引用:quasiquote和` 40 | 4.12 简单调度:case 41 | 4.13 动态绑定:parameterize 42 | -------------------------------------------------------------------------------- /04.01 标记法: -------------------------------------------------------------------------------- 1 | 4.1 标记法 2 | 3 | 这一章(和其余的文档)使用了一个稍微不同的标记法,而不是基于字符的《Racket语言概要》章节语法。使用语法表something的语法如下所示: 4 | 5 | (something [id ...+] an-expr ...) 6 | 7 | 斜体的元变量在本规范中,如id和an-expr,使用Racket标识符的语法,所以an-expr是一元变量。命名约定隐式定义了许多元变量的含义: 8 | 1、以id结尾的元变量表示标识符,如X或my-favorite-martian。 9 | 2、一元标识符以keyword结束代表一个关键字,如#:tag。 10 | 3、一元标识符以expr结束表达代表任何子表,它将被解析为一个表达式。 11 | 4、一元标识符以body结束代表任何子表;它将被解析为局部定义或表达式。只有在没有任何表达式之前,一个body才能解析为一个定义,而最后一个body必须是一个表达式;参见《内部定义》(Internal Definitions)部分。 12 | 13 | 在语法的方括号表示形式的括号序列,其中方括号通常用于(约定)。也就是说,方括号并不意味着是句法表的可选部分。 14 | 15 | …表示前一个表的零个或多个重复,…+表示前面数据的一个或多个重复。否则,非斜体标识代表自己。 16 | 17 | 根据上面的语法,这里有一些something的合乎逻辑的用法: 18 | 19 | (something [x]) 20 | (something [x] (+ 1 2)) 21 | (something [x my-favorite-martian x] (+ 1 2) #f) 22 | 23 | 一些语法表规范指的是不隐式定义而不是预先定义的元变量。这样的元变量在主表定义后面使用BNF-like格式提供选择: 24 | 25 | (something-else [thing ...+] an-expr ...) 26 | 27 | thing = thing-id 28 | | thing-keyword 29 | 30 | 上面的例子表明,在其它的表中,一个thing要么是标识符要么是关键字。 31 | -------------------------------------------------------------------------------- /04.02 标识符和绑定: -------------------------------------------------------------------------------- 1 | 4.2 标识符和绑定 2 | 3 | 表达式的上下文决定表达式中出现的标识符的含义。特别是,用语言racket开始一个模块时,如: 4 | 5 | #lang racket 6 | 7 | 意味着,在模块中,本指南中描述的标识符从这里描述的含义开始:cons指创建一个配对的函数,car指的是提取一个配对的第一个元素的函数,等等。 8 | 9 | 诸如像 define、lambda和let的表,并让一个意义与一个或多个标识符相关联,也就是说,它们绑定标识符。绑定应用的程序的一部分是绑定的范围。对给定表达式有效的绑定集是表达式的环境。 10 | 11 | 例如,有以下内容: 12 | 13 | #lang racket 14 | 15 | (define f 16 | (lambda (x) 17 | (let ([y 5]) 18 | (+ x y)))) 19 | 20 | (f 10) 21 | 22 | define是f的绑定,lambda有一个对x的绑定,let有一个对y的绑定,对f的绑定范围是整个模块;x绑定的范围是(let ([y 5]) (+ x y));y绑定的范围仅仅是 (+ x y)。(+ x y)的环境包括对y、x和f的绑定,以及所有在racket中的绑定。 23 | 24 | 模块级的define只能绑定尚未定义或被require进入模块的标识符。但是,局部define或其他绑定表可以为已有绑定的标识符提供新的局部绑定;这样的绑定会对现有绑定屏蔽(shadow)。 25 | 26 | 例如: 27 | (define f 28 | (lambda (append) 29 | (define cons (append "ugly" "confusing")) 30 | (let ([append 'this-was]) 31 | (list append cons)))) 32 | 33 | > (f list) 34 | 35 | '(this-was ("ugly" "confusing")) 36 | 37 | 类似地,模块级define可以从模块的语言中shadow一个绑定。例如,一个racket模块里的(define cons 1)屏蔽被racket所提供的cons。有意屏蔽一个语言绑定是一个绝佳的主意——尤其是像cons这种被广泛使用的绑定——但是屏蔽消除了程序员应该避免使用的语言提供的所有模糊绑定。 38 | 39 | 即使像define和lambda这些从绑定中得到它们的含义,尽管它们有转换(transformer)绑定(这意味着它们表示语法表)而不是值绑定。由于define具有一个转换绑定,因此标识符本身不能用于获取值。但是,对define的常规绑定可以被屏蔽。 40 | 41 | 例如: 42 | > define 43 | 44 | eval:1:0: define: bad syntax 45 | 46 | in: define 47 | > (let ([define 5]) define) 48 | 49 | 5 50 | 51 | 同样,用这种方式来隐藏标准绑定是一个绝佳主意,但这种可能性是Racket灵活性的与生俱来的部分。 52 | -------------------------------------------------------------------------------- /04.03 函数调用(过程程序): -------------------------------------------------------------------------------- 1 | 4.3 函数调用(过程程序) 2 | 3 | 一个表表达式: 4 | 5 | (proc-expr arg-expr ...) 6 | 7 | 是一个函数调用——也被称为一个应用程序——proc-expr不是标识符,而是作为一个语法翻译器(如if或define)。 8 | -------------------------------------------------------------------------------- /04.03.1 求值顺序和元数: -------------------------------------------------------------------------------- 1 | 4.3.1 求值顺序和元数 2 | 3 | 一个函数调用求值是首先求值proc-expr和为所有arg-expr(由左至右)。然后,如果proc-expr产生一个函数接受arg-expr提供的所有参数,这个函数被调用。否则,将引发异常。 4 | 5 | 例如: 6 | > (cons 1 null) 7 | '(1) 8 | > (+ 1 2 3) 9 | 6 10 | > (cons 1 2 3) 11 | cons: arity mismatch; 12 | the expected number of arguments does not match the given 13 | number 14 | expected: 2 15 | given: 3 16 | arguments...: 17 | 1 18 | 2 19 | 3 20 | > (1 2 3) 21 | application: not a procedure; 22 | expected a procedure that can be applied to arguments 23 | given: 1 24 | arguments...: 25 | 2 26 | 3 27 | 28 | 某些函数,如cons,接受固定数量的参数。某些函数,如+或list,接受任意数量的参数。一些函数接受一系列参数计数;例如substring接受两个或三个参数。一个函数的元数(arity)是它接受参数的数量。 29 | -------------------------------------------------------------------------------- /04.03.2 关键字参数: -------------------------------------------------------------------------------- 1 | 4.3.2 关键字参数 2 | 3 | 除了通过位置参数外,有些函数接受关键字参数。因此,arg可以是一个arg-keyword arg-expr序列而不只是一个arg-expr: 4 | 5 | (proc-expr arg ...) 6 | arg = arg-expr 7 | | arg-keyword arg-expr 8 | 9 | 例如: 10 | (go "super.rkt" #:mode 'fast) 11 | 12 | 调用函数绑定到"super.rkt" 作为位置参数,并用'fast通过#:mode关键字作为相关参数。关键字隐式地与后面的表达式配对。 13 | 14 | 既然关键字本身不是一个表达式,那么 15 | 16 | (go "super.rkt" #:mode #:fast) 17 | 18 | 就是语法错误。#:mode关键字必须跟着一个表达式以产生一个参数值,并#:fast不是一个表达式。 19 | 20 | 关键字arg的顺序决定arg-expr的求值顺序,而一个函数接受关键字参数与在参数列表中的位置无关。上面对go的调用可以等价地写为: 21 | 22 | (go #:mode 'fast "super.rkt") 23 | -------------------------------------------------------------------------------- /04.03.3 apply函数: -------------------------------------------------------------------------------- 1 | 4.3.3 apply函数 2 | 3 | 函数调用的语法支持任意数量的参数,但是一个特定的调用总是指定一个固定数量的参数。因此,一个带参数列表的函数不能直接将一个类似于+的函数应用到列表中的所有项中: 4 | 5 | (define (avg lst) ; doesn’t work... 6 | (/ (+ lst) (length lst))) 7 | 8 | > (avg '(1 2 3)) 9 | +: contract violation 10 | expected: number? 11 | given: '(1 2 3) 12 | 13 | (define (avg lst) ; doesn’t always work... 14 | (/ (+ (list-ref lst 0) (list-ref lst 1) (list-ref lst 2)) 15 | (length lst))) 16 | 17 | > (avg '(1 2 3)) 18 | 2 19 | > (avg '(1 2)) 20 | list-ref: index too large for list 21 | index: 2 22 | in: '(1 2) 23 | 24 | apply函数提供了一种绕过这种限制的方法。它使用一个函数和一个list参数,并将函数应用到列表中的值: 25 | 26 | (define (avg lst) 27 | (/ (apply + lst) (length lst))) 28 | 29 | > (avg '(1 2 3)) 30 | 2 31 | > (avg '(1 2)) 32 | 3/2 33 | > (avg '(1 2 3 4)) 34 | 5/2 35 | 36 | 为方便起见,apply函数接受函数和列表之间的附加参数。额外的参数被有效地加入参数列表: 37 | 38 | (define (anti-sum lst) 39 | (apply - 0 lst)) 40 | 41 | > (anti-sum '(1 2 3)) 42 | -6 43 | 44 | apply函数也接受关键字参数,并将其传递给调用函数: 45 | 46 | (apply go #:mode 'fast '("super.rkt")) 47 | (apply go '("super.rkt") #:mode 'fast) 48 | 49 | 包含在apply列表参数中的关键字不算作调用函数的关键字参数;相反,这个列表中的所有参数都被位置参数处理。要将一个关键字参数列表传递给函数,使用keyword-apply函数,它接受一个要应用的函数和三个列表。前两个列表是平行的,其中第一个列表包含关键字(按keyword ((lambda (x) x) 11 | 1) 12 | 1 13 | 14 | > ((lambda (x y) (+ x y)) 15 | 1 2) 16 | 3 17 | 18 | > ((lambda (x y) (+ x y)) 19 | 1) 20 | #: arity mismatch; 21 | the expected number of arguments does not match the given 22 | number 23 | expected: 2 24 | given: 1 25 | arguments...: 26 | 1 27 | -------------------------------------------------------------------------------- /04.04.1 申明剩余(rest)参数: -------------------------------------------------------------------------------- 1 | 4.4.1 申明剩余(rest)参数 2 | 3 | lambda表达式也可以有这种形式: 4 | 5 | (lambda rest-id 6 | body ...+) 7 | 8 | 也就是说,lambda表达式可以有一个没有被圆括号包围的单个rest-id。所得到的函数接受任意数目的参数,并且这个参数放入一个绑定到rest-id的列表: 9 | 10 | 例如: 11 | > ((lambda x x) 12 | 1 2 3) 13 | '(1 2 3) 14 | > ((lambda x x)) 15 | '() 16 | > ((lambda x (car x)) 17 | 1 2 3) 18 | 1 19 | 20 | 带有一个rest-id的函数经常使用apply函数调用另一个函数,它接受任意数量的参数。 21 | 22 | 例如: 23 | (define max-mag 24 | (lambda nums 25 | (apply max (map magnitude nums)))) 26 | 27 | > (max 1 -2 0) 28 | 1 29 | > (max-mag 1 -2 0) 30 | 2 31 | 32 | lambda表还支持必需参数与rest-id相结合: 33 | 34 | (lambda (arg-id ...+ . rest-id) 35 | body ...+) 36 | 37 | 这个表的结果是一个函数,它至少需要与arg-id一样多的参数,并且还接受任意数量的附加参数。 38 | 39 | 例如: 40 | 41 | (define max-mag 42 | (lambda (num . nums) 43 | (apply max (map magnitude (cons num nums))))) 44 | 45 | > (max-mag 1 -2 0) 46 | 2 47 | > (max-mag) 48 | max-mag: arity mismatch; 49 | the expected number of arguments does not match the given 50 | number 51 | expected: at least 1 52 | given: 0 53 | 54 | rest-id变量有时称为rest参数,因为它接受函数参数的“rest”。 55 | -------------------------------------------------------------------------------- /04.04.2 声明可选(optional)参数: -------------------------------------------------------------------------------- 1 | 4.4.2 声明可选(optional)参数 2 | 3 | 不只是标识符,一个lambda表的参数(不仅是剩余参数)可以用标识符和缺省值指定: 4 | 5 | (lambda gen-formals 6 | body ...+) 7 | 8 | gen-formals = (arg ...) 9 | | rest-id 10 | | (arg ...+ . rest-id) 11 | 12 | arg = arg-id 13 | | [arg-id default-expr] 14 | 15 | 表的参数[arg-id default-expr]是可选的。当参数不在应用程序中提供,default-expr产生默认值。default-expr可以引用任何前面的arg-id,并且下面的每个arg-id也必须应该有一个默认值。 16 | 17 | 例如: 18 | (define greet 19 | (lambda (given [surname "Smith"]) 20 | (string-append "Hello, " given " " surname))) 21 | 22 | > (greet "John") 23 | "Hello, John Smith" 24 | > (greet "John" "Doe") 25 | "Hello, John Doe" 26 | 27 | (define greet 28 | (lambda (given [surname (if (equal? given "John") 29 | "Doe" 30 | "Smith")]) 31 | (string-append "Hello, " given " " surname))) 32 | 33 | > (greet "John") 34 | "Hello, John Doe" 35 | > (greet "Adam") 36 | "Hello, Adam Smith" 37 | -------------------------------------------------------------------------------- /04.04.3 声明关键字(keyword)参数: -------------------------------------------------------------------------------- 1 | 4.4.3 声明关键字(keyword)参数 2 | 3 | lambda表可以声明要通过关键字传递的参数,而不是位置。关键字参数可以与位置参数混合,也可以为两种参数提供默认值表达式: 4 | 5 | (lambda gen-formals 6 | body ...+) 7 | 8 | gen-formals = (arg ...) 9 | | rest-id 10 | | (arg ...+ . rest-id) 11 | 12 | arg = arg-id 13 | | [arg-id default-expr] 14 | | arg-keyword arg-id 15 | | arg-keyword [arg-id default-expr] 16 | 17 | 由一个应用程序使用同一个arg-keyword关键字提供一个参数,该参数指定为arg-keyword arg-id。在参数列表中关键字标识符对的位置与应用程序中的参数匹配并不重要,因为它将通过关键字而不是位置与参数值匹配。 18 | 19 | (define greet 20 | (lambda (given #:last surname) 21 | (string-append "Hello, " given " " surname))) 22 | 23 | > (greet "John" #:last "Smith") 24 | "Hello, John Smith" 25 | > (greet #:last "Doe" "John") 26 | "Hello, John Doe" 27 | 28 | arg-keyword [arg-id default-expr]参数指定一个带默认值的关键字参数。 29 | 30 | 例如: 31 | (define greet 32 | (lambda (#:hi [hi "Hello"] given #:last [surname "Smith"]) 33 | (string-append hi ", " given " " surname))) 34 | 35 | > (greet "John") 36 | "Hello, John Smith" 37 | > (greet "Karl" #:last "Marx") 38 | "Hello, Karl Marx" 39 | > (greet "John" #:hi "Howdy") 40 | "Howdy, John Smith" 41 | > (greet "Karl" #:last "Marx" #:hi "Guten Tag") 42 | "Guten Tag, Karl Marx" 43 | 44 | lambda表不支持创建一个接受“rest”关键字的函数。要构造一个接受所有关键字参数的函数,请使用make-keyword-procedure函数。这个函数支持make-keyword-procedure通过前两个(位置)参数中的并行列表接受关键字参数,然后由应用程序的所有位置参数作为剩余位置参数。 45 | 46 | 例如: 47 | (define (trace-wrap f) 48 | (make-keyword-procedure 49 | (lambda (kws kw-args . rest) 50 | (printf "Called with ~s ~s ~s\n" kws kw-args rest) 51 | (keyword-apply f kws kw-args rest)))) 52 | 53 | > ((trace-wrap greet) "John" #:hi "Howdy") 54 | Called with (#:hi) ("Howdy") ("John") 55 | "Howdy, John Smith" 56 | -------------------------------------------------------------------------------- /04.04.4 多解函数:case-lambda: -------------------------------------------------------------------------------- 1 | 4.4.4 多解函数:case-lambda 2 | 3 | case-lambda表创建一个函数,该函数可以根据所提供的参数的数量具有完全不同的行为。case-lambda表达式有以下形式: 4 | 5 | (case-lambda 6 | [formals body ...+] 7 | ...) 8 | 9 | formals = (arg-id ...) 10 | | rest-id 11 | | (arg-id ...+ . rest-id) 12 | 13 | 每个[formals body ...+]类似于(lambda formals body ...+)。通过case-lambda应用函数生成类似于应用一个lambda匹配给定参数数量的第一种情况。 14 | 15 | 例如: 16 | (define greet 17 | (case-lambda 18 | [(name) (string-append "Hello, " name)] 19 | [(given surname) (string-append "Hello, " given " " surname)])) 20 | 21 | > (greet "John") 22 | "Hello, John" 23 | > (greet "John" "Smith") 24 | "Hello, John Smith" 25 | > (greet) 26 | greet: arity mismatch; 27 | the expected number of arguments does not match the given 28 | number 29 | given: 0 30 | 31 | lambda函数不能直接支持可选参数或关键字参数。 32 | -------------------------------------------------------------------------------- /04.05 定义:define: -------------------------------------------------------------------------------- 1 | 4.5 定义:define 2 | 3 | 基本定义具为如下形式: 4 | 5 | (define id expr) 6 | 7 | 在这种情况下,id绑定到了expr的结果。 8 | 9 | 例如: 10 | (define salutation (list-ref '("Hi" "Hello") (random 2))) 11 | 12 | > salutation 13 | "Hi" 14 | -------------------------------------------------------------------------------- /04.05.1 函数简写: -------------------------------------------------------------------------------- 1 | 4.5.1 函数简写 2 | 3 | 定义表还支持函数定义的简写: 4 | 5 | (define (id arg ...) body ...+) 6 | 7 | 这是以下内容的简写: 8 | 9 | (define id (lambda (arg ...) body ...+)) 10 | 11 | 例如: 12 | (define (greet name) 13 | (string-append salutation ", " name)) 14 | 15 | > (greet "John") 16 | "Hi, John" 17 | 18 | (define (greet first [surname "Smith"] #:hi [hi salutation]) 19 | (string-append hi ", " first " " surname)) 20 | 21 | > (greet "John") 22 | "Hi, John Smith" 23 | > (greet "John" #:hi "Hey") 24 | "Hey, John Smith" 25 | > (greet "John" "Doe") 26 | "Hi, John Doe" 27 | 28 | 通过define这个函数简写也支持一个剩余参数(rest argument)(即,一个额外参数用于在列表中收集最后参数): 29 | 30 | (define (id arg ... . rest-id) body ...+) 31 | 32 | 这是以下内容的简写: 33 | 34 | (define id (lambda (arg ... . rest-id) body ...+)) 35 | 36 | 例如: 37 | (define (avg . l) 38 | (/ (apply + l) (length l))) 39 | 40 | > (avg 1 2 3) 41 | 2 42 | -------------------------------------------------------------------------------- /04.05.2 咖喱函数简写: -------------------------------------------------------------------------------- 1 | 4.5.2 咖喱函数简写 2 | 3 | 注意下面的make-add-suffix函数接收一个字符串并返回另一个带字符串的函数: 4 | 5 | (define make-add-suffix 6 | (lambda (s2) 7 | (lambda (s) (string-append s s2)))) 8 | 9 | 虽然不常见,但make-add-suffix的结果可以直接调用,就像这样: 10 | 11 | > ((make-add-suffix "!") "hello") 12 | "hello!" 13 | 14 | 从某种意义上说,make-add-suffix是一个函数,需要两个参数,但每次只需要一个参数。一个函数带有一些参数并返回一个函数会消费更多,有时被称为一个咖喱函数(curried function)。 15 | 16 | 使用define的函数简写形式,make-add-suffix可以等效地写成: 17 | 18 | (define (make-add-suffix s2) 19 | (lambda (s) (string-append s s2))) 20 | 21 | 这个简写反映了(make-add-suffix "!")函数调用的形状。define表更进一步支持定义反映嵌套函数调用的咖喱函数简写: 22 | 23 | (define ((make-add-suffix s2) s) 24 | (string-append s s2)) 25 | 26 | > ((make-add-suffix "!") "hello") 27 | "hello!" 28 | 29 | (define louder (make-add-suffix "!")) 30 | (define less-sure (make-add-suffix "?")) 31 | 32 | > (less-sure "really") 33 | "really?" 34 | > (louder "really") 35 | "really!" 36 | 37 | define函数简写的完整语法如下所示: 38 | 39 | (define (head args) body ...+) 40 | 41 | head = id 42 | | (head args) 43 | 44 | args = arg ... 45 | | arg ... . rest-id 46 | 47 | 这个简写的扩展有一个给定义中的每个head的嵌套lambda表,最里面的head与最外面的lambda通信。 48 | -------------------------------------------------------------------------------- /04.05.3 多值和define-values: -------------------------------------------------------------------------------- 1 | 4.5.3 多值和define-values 2 | 3 | Racket表达式通常产生一个单独的结果,但有些表达式可以产生多个结果。例如,quotient(商)和remainder(余数)各自产生一个值,但quotient/remainder同时产生相同的两个值: 4 | 5 | > (quotient 13 3) 6 | 4 7 | > (remainder 13 3) 8 | 1 9 | > (quotient/remainder 13 3) 10 | 4 11 | 1 12 | 13 | 如上所示,REPL在自己的行打印每一结果值。 14 | 15 | 多值函数可以用values函数来实现,它接受任意数量的值,并将它们作为结果返回: 16 | 17 | > (values 1 2 3) 18 | 1 19 | 2 20 | 3 21 | 22 | (define (split-name name) 23 | (let ([parts (regexp-split " " name)]) 24 | (if (= (length parts) 2) 25 | (values (list-ref parts 0) (list-ref parts 1)) 26 | (error "not a name")))) 27 | 28 | 29 | > (split-name "Adam Smith") 30 | "Adam" 31 | "Smith" 32 | 33 | define-values表同时将多个标识符绑定到多个结果产生单个表达式: 34 | 35 | (define-values (id ...) expr) 36 | 37 | 由expr产生的结果数必须与id的数相匹配。 38 | 39 | 例如: 40 | 41 | (define-values (given surname) (split-name "Adam Smith")) 42 | 43 | > given 44 | "Adam" 45 | > surname 46 | "Smith" 47 | 48 | 一个define表(不是一个函数简写)等价于一个带有单个id的define-values表。 49 | -------------------------------------------------------------------------------- /04.05.4 内部定义: -------------------------------------------------------------------------------- 1 | 4.5.4 内部定义 2 | 3 | 当句法表的语法指定body,那相应的表可以是定义或表达式。作为一个body的定义是一个内部定义(internal definition)。 4 | 5 | 一个body序列中的表达式和内部定义可以混合,只要最后一个body是表达式。 6 | 7 | 例如,lambda的语法是: 8 | 9 | (lambda gen-formals 10 | body ...+) 11 | 12 | 下面是语法的有效实例: 13 | 14 | (lambda (f) ; 没有定义 15 | (printf "running\n") 16 | (f 0)) 17 | 18 | (lambda (f) ; 一个定义 19 | (define (log-it what) 20 | (printf "~a\n" what)) 21 | (log-it "running") 22 | (f 0) 23 | (log-it "done")) 24 | 25 | (lambda (f n) ; 两个定义 26 | (define (call n) 27 | (if (zero? n) 28 | (log-it "done") 29 | (begin 30 | (log-it "running") 31 | (f n) 32 | (call (- n 1))))) 33 | (define (log-it what) 34 | (printf "~a\n" what)) 35 | (call n)) 36 | 37 | 特定的body序列中的内部定义是相互递归的,也就是说,只要引用在定义发生之前没有实际求值,那么任何定义都可以引用任何其他定义。如果过早引用定义,则会出现错误。 38 | 39 | 例如: 40 | (define (weird) 41 | (define x x) 42 | x) 43 | 44 | > (weird) 45 | x: undefined; 46 | cannot use before initialization 47 | 48 | 一系列的内部定义只使用define很容易转换为等效的letrec表(如同在下一节介绍的内容)。然而,其他的定义表可以表现为一个body,包括define-values、 struct(见《程序员定义的数据类型》(Programmer-Defined Datatypes))或define-syntax(见《宏》(Macros))。 49 | -------------------------------------------------------------------------------- /04.06 局部绑定: -------------------------------------------------------------------------------- 1 | 4.6 局部绑定 2 | 3 | 虽然内部define可用于局部绑定,Racket提供了三种形式给予程序员在绑定方面的更多控制:let、let*和letrec。 4 | -------------------------------------------------------------------------------- /04.06.1 平行绑定:let: -------------------------------------------------------------------------------- 1 | 4.6.1 平行绑定:let 2 | 3 | let表绑定一组标识符,每个标识符都是某个表达式的结果,用于let主体: 4 | 5 | (let ([id expr] ...) body ...+) 6 | 7 | id绑定处于”平行”状态,即对于任何id,没有一个id绑定到右边的expr,但都可在body内找到。id必须被定义为彼此不同的形式。 8 | 9 | 例如: 10 | > (let ([me "Bob"]) 11 | me) 12 | "Bob" 13 | 14 | > (let ([me "Bob"] 15 | [myself "Robert"] 16 | [I "Bobby"]) 17 | (list me myself I)) 18 | '("Bob" "Robert" "Bobby") 19 | 20 | > (let ([me "Bob"] 21 | [me "Robert"]) 22 | me) 23 | eval:3:0: let: duplicate identifier 24 | at: me 25 | in: (let ((me "Bob") (me "Robert")) me) 26 | 27 | 事实上,一个id的expr不会明白自己的绑定通常对封装有用,必须转回到旧的值: 28 | 29 | > (let ([me "Tarzan"] 30 | [you "Jane"]) 31 | (let ([me you] 32 | [you me]) 33 | (list me you))) 34 | '("Jane" "Tarzan") 35 | -------------------------------------------------------------------------------- /04.06.2 相继绑定:let*: -------------------------------------------------------------------------------- 1 | 4.6.2 相继绑定:let* 2 | 3 | let*的语法和let的一样: 4 | 5 | (let* ([id expr] ...) body ...+) 6 | 7 | 不同的是,每个id可用于以后的expr,以及body内。此外,id不需要有区别,最新的绑定可见。 8 | 9 | 例如: 10 | 11 | > (let* ([x (list "Burroughs")] 12 | [y (cons "Rice" x)] 13 | [z (cons "Edgar" y)]) 14 | (list x y z)) 15 | 16 | '(("Burroughs") ("Rice" "Burroughs") ("Edgar" "Rice" "Burroughs")) 17 | > (let* ([name (list "Burroughs")] 18 | [name (cons "Rice" name)] 19 | [name (cons "Edgar" name)]) 20 | name) 21 | 22 | '("Edgar" "Rice" "Burroughs") 23 | 24 | 换言之, let*表是相当于嵌套的let表,每一个都有一个单独的绑定: 25 | 26 | > (let ([name (list "Burroughs")]) 27 | (let ([name (cons "Rice" name)]) 28 | (let ([name (cons "Edgar" name)]) 29 | name))) 30 | 31 | '("Edgar" "Rice" "Burroughs") 32 | -------------------------------------------------------------------------------- /04.06.3 递归绑定:letrec: -------------------------------------------------------------------------------- 1 | 4.6.3 递归绑定:letrec 2 | 3 | letrec的语法也和let相同: 4 | 5 | (letrec ([id expr] ...) body ...+) 6 | 7 | 而let使其其绑定只在body内被提供,let*使其绑定提供给任何后来的绑定expr,letrec使其绑定提供给所有其他expr,甚至更早的。换句话说,letrec绑定是递归的。 8 | 9 | 在一个letrec表中的expr经常大都是递归或互相递归的lambda表函数: 10 | 11 | > (letrec ([swing 12 | (lambda (t) 13 | (if (eq? (car t) 'tarzan) 14 | (cons 'vine 15 | (cons 'tarzan (cddr t))) 16 | (cons (car t) 17 | (swing (cdr t)))))]) 18 | (swing '(vine tarzan vine vine))) 19 | 20 | '(vine vine tarzan vine) 21 | 22 | > (letrec ([tarzan-near-top-of-tree? 23 | (lambda (name path depth) 24 | (or (equal? name "tarzan") 25 | (and (directory-exists? path) 26 | (tarzan-in-directory? path depth))))] 27 | [tarzan-in-directory? 28 | (lambda (dir depth) 29 | (cond 30 | [(zero? depth) #f] 31 | [else 32 | (ormap 33 | (λ (elem) 34 | (tarzan-near-top-of-tree? (path-element->string elem) 35 | (build-path dir elem) 36 | (- depth 1))) 37 | (directory-list dir))]))]) 38 | (tarzan-near-top-of-tree? "tmp" 39 | (find-system-path 'temp-dir) 40 | 4)) 41 | 42 | directory-list: could not open directory 43 | path: /var/tmp/abrt/Python-2013-12-05-03:18:26-13782 44 | system error: Permission denied; errno=13 45 | 46 | 而一个letrec表的expr是典型的lambda表达式,它们可以是任何表达式。表达式按顺序求值,在获得每个值之后,它立即与相应的id相关联。如果id在其值准备就绪之前被引用,则会引发一个错误,就像内部定义一样。 47 | 48 | > (letrec ([quicksand quicksand]) 49 | quicksand) 50 | 51 | quicksand: undefined; 52 | cannot use before initialization 53 | -------------------------------------------------------------------------------- /04.06.4 命名let: -------------------------------------------------------------------------------- 1 | 4.6.4 命名let 2 | 3 | 命名let是一个迭代和递归表。它使用与局部绑定相同的语法关键字let,但在let之后的标识符(而不是一个括号)触发不同的解析。 4 | 5 | (let proc-id ([arg-id init-expr] ...) 6 | body ...+) 7 | 8 | 命名的let表等效于 9 | 10 | (letrec ([proc-id (lambda (arg-id ...) 11 | body ...+)]) 12 | (proc-id init-expr ...)) 13 | 14 | 也就是说,一个命名的let绑定一个只在函数体中可见的函数标识符,并且用一些初始表达式的值隐式调用函数。 15 | 16 | 例如: 17 | (define (duplicate pos lst) 18 | (let dup ([i 0] 19 | [lst lst]) 20 | (cond 21 | [(= i pos) (cons (car lst) lst)] 22 | [else (cons (car lst) (dup (+ i 1) (cdr lst)))]))) 23 | 24 | > (duplicate 1 (list "apple" "cheese burger!" "banana")) 25 | '("apple" "cheese burger!" "cheese burger!" "banana") 26 | -------------------------------------------------------------------------------- /04.06.5 多值绑定:let-values,let*-values,letrec-values: -------------------------------------------------------------------------------- 1 | 4.6.5 多值绑定:let-values,let*-values,letrec-values 2 | 3 | 以define-values同样的方式绑定定义的多个结果(见《多值和define-values》)(Multiple Values and define-values),let-values、let*-values和letrec-values值绑定多个局部结果。 4 | 5 | (let-values ([(id ...) expr] ...) 6 | body ...+) 7 | 8 | (let*-values ([(id ...) expr] ...) 9 | body ...+) 10 | 11 | (letrec-values ([(id ...) expr] ...) 12 | body ...+) 13 | 14 | 每个expr必须产生许多值作为id的对应。绑定的规则是和没有-values的形式的表相同:let-values的id只绑定在body里,let*-value的id绑定在后面从句的expr里,letrec-value的id绑定是针对对所有的expr。 15 | 16 | 例如: 17 | > (let-values ([(q r) (quotient/remainder 14 3)]) 18 | (list q r)) 19 | 20 | '(4 2) 21 | -------------------------------------------------------------------------------- /04.07 条件分支: -------------------------------------------------------------------------------- 1 | 4.7 条件分支 2 | 3 | 大多数函数都可用于分支,如<和string?,结果要么产生#t要么产生#f。Racket的分支表,无论什么情况,对待任何非#f值为真。我们说一个真值(true value)意味着其它为任何非#f值。 4 | 5 | 本约定的“真值(true value)”在#f能够代替故障或表明不提供一个可选的值的地方与协议完全吻合 。(谨防过度使用这一技巧,记住一个异常通常是一个更好的机制来报告故障。) 6 | 7 | 例如,member函数具有双重职责;它可以用来查找从一个特定项目开始的列表的尾部,或者它可以用来简单地检查一个项目是否存在于列表中: 8 | 9 | > (member "Groucho" '("Harpo" "Zeppo")) 10 | #f 11 | > (member "Groucho" '("Harpo" "Groucho" "Zeppo")) 12 | '("Groucho" "Zeppo") 13 | > (if (member "Groucho" '("Harpo" "Zeppo")) 14 | 'yep 15 | 'nope) 16 | 'nope 17 | > (if (member "Groucho" '("Harpo" "Groucho" "Zeppo")) 18 | 'yep 19 | 'nope) 20 | 'yep 21 | -------------------------------------------------------------------------------- /04.07.1 简单分支:if: -------------------------------------------------------------------------------- 1 | 4.7.1 简单分支:if 2 | 3 | 在if表: 4 | 5 | (if test-expr then-expr else-expr) 6 | 7 | test-expr总是求值。如果它产生任何非#f值,然后对then-expr求值。否则,else-expr被求值。 8 | 9 | if表必须既有一个then-expr也有一个else-expr;后者不是可选的。执行(或跳过)基于一个test-expr的副作用,使用when或unless,将在后边《顺序》(Sequencing)部分描述。 10 | -------------------------------------------------------------------------------- /04.07.2 组合测试:and和or: -------------------------------------------------------------------------------- 1 | 4.7.2 组合测试:and和or 2 | 3 | Racket的and和or是语法形式,而不是函数。不像一个函数,如果前边的一个求值确定了答案,and和or表会忽略后边的表达式求值。 4 | 5 | (and expr ...) 6 | 7 | 如果其所有的expr产生#f,or表产生#f。否则,它从它的expr第一个非#f值产生结果值。作为一个特殊的情况,(or)产生#f。 8 | 9 | 例如: 10 | 11 | > (define (got-milk? lst) 12 | (and (not (null? lst)) 13 | (or (eq? 'milk (car lst)) 14 | (got-milk? (cdr lst))))) ;仅在需要时发生 15 | > (got-milk? '(apple banana)) 16 | #f 17 | > (got-milk? '(apple milk banana)) 18 | #t 19 | 20 | 如果求值达到and或or表的最后一个expr,那么expr的值直接决定and或or的结果。因此,最后一个expr是在尾部的位置,这意味着上面got-milk?函数在固定空间中运行。 21 | -------------------------------------------------------------------------------- /04.07.3 约束测试:cond: -------------------------------------------------------------------------------- 1 | 4.7.3 约束测试:cond 2 | 3 | cond表链接了一系列的测试以选择一个表达式结果。一个最近似的情况,cond语法如下: 4 | 5 | (cond [test-expr body ...+] 6 | ...) 7 | 8 | 每个test-expr求值顺序求值。如果它产生#f,相应的body被忽略,求值进程进入下一个test-expr。当一个test-expr产生一个真值,它的body求值产生的结果作为cond表的结果。并不再进一步对test-expr求值。 9 | 10 | 在cond最后的test-expr可用else代替。在求值条件里,else作为一个#t的同义词提供。但它阐明了最后的从句是为了获取所有剩余的事例。如果else没有被使用,而且可能没有test-expr产生真值;在这种情况下,该cond表达式的结果是#。 11 | 12 | 例如: 13 | > (cond 14 | [(= 2 3) (error "wrong!")] 15 | [(= 2 2) 'ok]) 16 | 'ok 17 | 18 | > (cond 19 | [(= 2 3) (error "wrong!")]) 20 | > (cond 21 | [(= 2 3) (error "wrong!")] 22 | [else 'ok]) 23 | 'ok 24 | 25 | (define (got-milk? lst) 26 | (cond 27 | [(null? lst) #f] 28 | [(eq? 'milk (car lst)) #t] 29 | [else (got-milk? (cdr lst))])) 30 | 31 | > (got-milk? '(apple banana)) 32 | #f 33 | > (got-milk? '(apple milk banana)) 34 | #t 35 | 36 | 包括以上两种从句的cond的完整语法: 37 | 38 | (cond cond-clause ...) 39 | 40 | cond-clause = [test-expr then-body ...+] 41 | | [else then-body ...+] 42 | | [test-expr => proc-expr] 43 | | [test-expr] 44 | 45 | =>变体获取test-expr真的结果并且传递给proc-expr的结果,proc-expr必须是有一个参数的函数。 46 | 47 | 48 | 例如: 49 | > (define (after-groucho lst) 50 | (cond 51 | [(member "Groucho" lst) => cdr] 52 | [else (error "not there")])) 53 | > (after-groucho '("Harpo" "Groucho" "Zeppo")) 54 | '("Zeppo") 55 | > (after-groucho '("Harpo" "Zeppo")) 56 | not there 57 | 58 | 只包括一个test-expr的从句是很少使用的。它捕获test-expr的真值的结果,并简单地返回这个结果给整个cond表达式。 59 | -------------------------------------------------------------------------------- /04.08 排序: -------------------------------------------------------------------------------- 1 | 4.8 排序 2 | 3 | Racket程序员喜欢编写尽可能少的带副作用的程序,因为纯粹的函数代码更容易测试和组成更大的程序。 4 | 5 | 然而,与外部环境的交互需要进行排序,例如在向显示器写入、打开图形窗口或在磁盘上操作文件时。 6 | -------------------------------------------------------------------------------- /04.08.1 前置影响:begin: -------------------------------------------------------------------------------- 1 | 4.8.1 前置影响:begin 2 | 3 | begin表达式排序表达式: 4 | 5 | (begin expr ...+) 6 | 7 | expr被顺序求值,并且除最后的expr结果外所有都被忽视。来自最后一个expr的结果作为begin表的结果,它是相对于begin表位于尾部的位置。 8 | 9 | 例如: 10 | 11 | (define (print-triangle height) 12 | (if (zero? height) 13 | (void) 14 | (begin 15 | (display (make-string height #\*)) 16 | (newline) 17 | (print-triangle (sub1 height))))) 18 | 19 | > (print-triangle 4) 20 | **** 21 | *** 22 | ** 23 | * 24 | 25 | 有多种表,比如lambda或cond支持一系列表达式甚至没有begin。这样的状态有时被叫做有一个隐含的begin(implicit begin)。 26 | 27 | 例如: 28 | 29 | (define (print-triangle height) 30 | (cond 31 | [(positive? height) 32 | (display (make-string height #\*)) 33 | (newline) 34 | (print-triangle (sub1 height))])) 35 | 36 | > (print-triangle 4) 37 | **** 38 | *** 39 | ** 40 | * 41 | 42 | begin表在顶层(top level)、模块级(module level)或仅在内部定义之后作为body是特殊的。在这些状态下,begin的上下文被拼接到周围的上下文中,而不是形成一个表达式。 43 | 44 | 例如: 45 | 46 | > (let ([curly 0]) 47 | (begin 48 | (define moe (+ 1 curly)) 49 | (define larry (+ 1 moe))) 50 | (list larry curly moe)) 51 | 52 | '(2 0 1) 53 | 54 | 这种拼接行为主要用于宏(macro),我们稍后将在《宏》(macro)中讨论它。 55 | -------------------------------------------------------------------------------- /04.08.2 后置影响:begin0: -------------------------------------------------------------------------------- 1 | 4.8.2 后置影响:begin0 2 | 3 | 一个begin0表达式与有一个begin表达式有相同的语法: 4 | 5 | (begin0 expr ...+) 6 | 7 | 不同的是begin0返回第一个expr的结果,而不是最后一个expr的结果。begin0表对于实现发生在一个计算之后的副作用是有用的,尤其是在计算产生了一个未知的数值结果的情况下。 8 | 9 | 例如: 10 | 11 | (define (log-times thunk) 12 | (printf "Start: ~s\n" (current-inexact-milliseconds)) 13 | (begin0 14 | (thunk) 15 | (printf "End..: ~s\n" (current-inexact-milliseconds)))) 16 | 17 | > (log-times (lambda () (sleep 0.1) 0)) 18 | Start: 1509391508010.048 19 | End..: 1509391508110.237 20 | 0 21 | > (log-times (lambda () (values 1 2))) 22 | Start: 1509391508110.958 23 | End..: 1509391508111.068 24 | 1 25 | 2 26 | -------------------------------------------------------------------------------- /04.08.3 if影响:when和unless: -------------------------------------------------------------------------------- 1 | 4.8.3 if影响:when和unless 2 | 3 | when表将if样式条件与“then”子句(并且没有“else”子句)的排序相结合: 4 | 5 | (when test-expr then-body ...+) 6 | 7 | 如果test-expr产生一个真值,那么所有的then-body被求值。最后一个then-body的结果是when表的结果。否则,没有then-body被求值而且结果是#。 8 | 9 | unless是相似的: 10 | 11 | (unless test-expr then-body ...+) 12 | 13 | 不同的是,test-expr结果是相反的:如果test-expr结果为#f时then-body被求值。 14 | 15 | 例如: 16 | 17 | (define (enumerate lst) 18 | (if (null? (cdr lst)) 19 | (printf "~a.\n" (car lst)) 20 | (begin 21 | (printf "~a, " (car lst)) 22 | (when (null? (cdr (cdr lst))) 23 | (printf "and ")) 24 | (enumerate (cdr lst))))) 25 | 26 | > (enumerate '("Larry" "Curly" "Moe")) 27 | Larry, Curly, and Moe. 28 | 29 | (define (print-triangle height) 30 | (unless (zero? height) 31 | (display (make-string height #\*)) 32 | (newline) 33 | (print-triangle (sub1 height)))) 34 | 35 | > (print-triangle 4) 36 | **** 37 | *** 38 | ** 39 | * 40 | -------------------------------------------------------------------------------- /04.09 赋值:set!: -------------------------------------------------------------------------------- 1 | 4.9 赋值:set! 2 | 3 | 使用set!赋值给变量: 4 | 5 | (set! id expr) 6 | 7 | 一个set!表达式对expr求值并改变id(它必须限制在闭括号的环境内)为结果值。set!表达式自己返回的结果是#。 8 | 9 | 例如: 10 | 11 | (define greeted null) 12 | 13 | (define (greet name) 14 | (set! greeted (cons name greeted)) 15 | (string-append "Hello, " name)) 16 | 17 | > (greet "Athos") 18 | "Hello, Athos" 19 | > (greet "Porthos") 20 | "Hello, Porthos" 21 | > (greet "Aramis") 22 | "Hello, Aramis" 23 | > greeted 24 | '("Aramis" "Porthos" "Athos") 25 | 26 | (define (make-running-total) 27 | (let ([n 0]) 28 | (lambda () 29 | (set! n (+ n 1)) 30 | n))) 31 | (define win (make-running-total)) 32 | (define lose (make-running-total)) 33 | 34 | > (win) 35 | 1 36 | > (win) 37 | 2 38 | > (lose) 39 | 1 40 | > (win) 41 | 3 42 | -------------------------------------------------------------------------------- /04.09.2 多值赋值:set!-values: -------------------------------------------------------------------------------- 1 | 4.9.2 多值赋值:set!-values 2 | 3 | set!-values表一次赋值给多个变量,给出一个生成适当的数值的表达式: 4 | 5 | (set!-values (id ...) expr) 6 | 7 | 这个表等价于使用let-values从expr接收多个结果,然后将结果使用set!单独赋值给id。 8 | 9 | 例如: 10 | 11 | (define game 12 | (let ([w 0] 13 | [l 0]) 14 | (lambda (win?) 15 | (if win? 16 | (set! w (+ w 1)) 17 | (set! l (+ l 1))) 18 | (begin0 19 | (values w l) 20 | ; swap sides... 21 | (set!-values (w l) (values l w)))))) 22 | 23 | > (game #t) 24 | 1 25 | 0 26 | > (game #t) 27 | 1 28 | 1 29 | > (game #f) 30 | 1 31 | 2 32 | -------------------------------------------------------------------------------- /04.10 引用:quote和': -------------------------------------------------------------------------------- 1 | 4.10 引用:quote和' 2 | 3 | 引用表产生一个常数: 4 | 5 | (quote datum) 6 | 7 | datum的语法在技术上被指定为read函数解析为单个元素的任何内容。quote表的值与read将产生给定的datum的值相同。 8 | 9 | datum可以是一个符号、一个布尔值、一个数字、一个(字符或字节)字符串、一个字符、一个关键字、一个空列表、一个包含更多类似值的配对(或列表),一个包含更多类似值的向量,一个包含更多类似值的哈希表,或者一个包含其它类似值的格子。 10 | 11 | 例如: 12 | 13 | > (quote apple) 14 | 'apple 15 | > (quote #t) 16 | #t 17 | > (quote 42) 18 | 42 19 | > (quote "hello") 20 | "hello" 21 | > (quote ()) 22 | '() 23 | > (quote ((1 2 3) #("z" x) . the-end)) 24 | '((1 2 3) #("z" x) . the-end) 25 | > (quote (1 2 . (3))) 26 | '(1 2 3) 27 | 28 | 正如上面最后一个示例所示,datum不需要匹配一个值的格式化的打印表。一个datum不能作为从#<开始的打印呈现,所以不能是#、#或一个程序。 29 | 30 | quote表很少用于datum的布尔值、数字或字符串本身,因为这些值的打印表可以用作常量。quote表更常用于符号和列表,当没有被引用时,它具有其他含义(标识符、函数调用等)。 31 | 32 | 表达式: 33 | 34 | 'datum 35 | 36 | 是 37 | 38 | (quote datum)速 39 | 40 | 的简写。 41 | 42 | 这个简写几乎总是用来代替quote。简写甚至应用于datum中,因此它可以生成包含quote的列表。 43 | 44 | 例如: 45 | 46 | > 'apple 47 | 'apple 48 | > '"hello" 49 | "hello" 50 | > '(1 2 3) 51 | '(1 2 3) 52 | > (display '(you can 'me)) 53 | (you can 'me) 54 | -------------------------------------------------------------------------------- /04.12 简单分派:case: -------------------------------------------------------------------------------- 1 | 4.12 简单分派:case 2 | 3 | 通过将表达式的结果与子句的值相匹配,case表分派一个子句: 4 | 5 | (case expr 6 | [(datum ...+) body ...+] 7 | ...) 8 | 9 | 每个datum将使用equal?对比expr的结果,然后相应的body被求值。case表可以为N个datum在O(log N)时间内分派正确的从句。 10 | 11 | 可以给每个从句提供多个datum,而且如果任何一个datum匹配,那么相应的body被求值。 12 | 13 | 例如: 14 | 15 | > (let ([v (random 6)]) 16 | (printf "~a\n" v) 17 | (case v 18 | [(0) 'zero] 19 | [(1) 'one] 20 | [(2) 'two] 21 | [(3 4 5) 'many])) 22 | 23 | 0 24 | 'zero 25 | 26 | 一个case表最后一个从句可以使用else,就像cond那样: 27 | 28 | 例如: 29 | 30 | > (case (random 6) 31 | [(0) 'zero] 32 | [(1) 'one] 33 | [(2) 'two] 34 | [else 'many]) 35 | 36 | 'many 37 | 38 | 对于更一般的模式匹配(但没有分派时间保证),使用match,这个会在《模式匹配》(Pattern Matching)中介绍。 39 | -------------------------------------------------------------------------------- /05 自定义的数据类型: -------------------------------------------------------------------------------- 1 | 5 自定义的数据类型 2 | 3 | 新的数据类型通常用struct表来创造,这是本章的主题。基于类的对象系统,遵循类和对象(Classes and Objects),提供了用于创建新的数据类型的另一种机制,但即使是类和对象也是结构类型的实现方式。 4 | -------------------------------------------------------------------------------- /05.1 简单的结构类型:struct: -------------------------------------------------------------------------------- 1 | 5.1 简单的结构类型:struct 2 | 3 | 一个最接近的,struct的语法是 4 | 5 | (struct struct-id (field-id ...)) 6 | 7 | 例如: 8 | 9 | (struct posn (x y)) 10 | 11 | struct表将struct-id和从struct-id和field-id构建的数值标识符绑定在一起: 12 | 13 | 1、struct-id:一个构造函数,它将一些参数作为field-id的数值,并返回结构类型的一个实例。 14 | 15 | 例如: 16 | 17 | > (posn 1 2) 18 | # 19 | 20 | 2、struct-id?:一个判断函数,它获取单个参数,如果它是结构类型的实例返回#t,否则返回#f。 21 | 22 | 例如: 23 | 24 | > (posn? 3) 25 | #f 26 | > (posn? (posn 1 2)) 27 | #t 28 | 29 | 3、struct-id-field-id:每个field-id,访问器从结构类型的一个实例中解析相应的字段值。 30 | 31 | 例如: 32 | 33 | > (posn-x (posn 1 2)) 34 | 1 35 | > (posn-y (posn 1 2)) 36 | 2 37 | 38 | 4、struct:struct-id:一个结构类型描述符,这是一个值,它体现结构类型作为最好的价值(与#:super和《更多的结构选项》(More Structure Type Options)一起作为后续讨论)。 39 | 40 | 一个struct表不限制在结构类型的实例中可以出现的字段的值类型。例如,(posn "apple" #f)过程产生一个posn实例,即使"apple"和#f对posn的实例的显性使用是无效的配套。执行字段值的约束,比如要求它们是数字,通常是合约的工作,如后面讨论的《合约(Contracts)》那样。 41 | -------------------------------------------------------------------------------- /05.2 复制和更新: -------------------------------------------------------------------------------- 1 | 5.2 复制和更新 2 | 3 | struct-copy复制一个结构并可选地更新克隆中的指定字段。这个过程有时称为功能性更新(functional update),因为结果是一个具有更新字段值的结构。但原来的结构没有被修改。 4 | 5 | (struct-copy struct-id struct-expr [field-id expr] ...) 6 | 7 | 出现在struct-copy后面的struct-id必须是由struct绑定的结构类型名称(即这个名称不能作为一个表达式直接被使用)。struct-expr必须产生结构类型的一个实例。结果是一个新实例,就像旧的结构类型一样,除这个被每个field-id标明的字段得到相应的expr的值之外。 8 | 9 | 例如: 10 | 11 | > (define p1 (posn 1 2)) 12 | > (define p2 (struct-copy posn p1 [x 3])) 13 | > (list (posn-x p2) (posn-y p2)) 14 | '(3 2) 15 | 16 | > (list (posn-x p1) (posn-x p2)) 17 | '(1 3) 18 | -------------------------------------------------------------------------------- /05.3 结构子类: -------------------------------------------------------------------------------- 1 | 5.3 结构子类 2 | 3 | struct的扩展形式可以用来定义结构子类型(structure subtype),它是一种扩展现有结构类型的结构类型: 4 | 5 | (struct struct-id super-id (field-id ...)) 6 | 7 | 这个super-id必须是由struct绑定的结构类型名称(即名称不能被作为表达式直接使用)。 8 | 9 | 例如: 10 | 11 | (struct posn (x y)) 12 | (struct 3d-posn posn (z)) 13 | 14 | 一个结构子类型继承其超类型的字段,并且子类型构造器接受这个值作为子类型字段在超类型字段的值之后。一个结构子类型的实例可以被用作这个超类型的断言和访问器。 15 | 16 | 例如: 17 | 18 | > (define p (3d-posn 1 2 3)) 19 | > p 20 | #<3d-posn> 21 | 22 | > (posn? p) 23 | #t 24 | 25 | > (3d-posn-z p) 26 | 3 27 | ; a 3d-posn has an x field, but there is no 3d-posn-x selector: 28 | 29 | > (3d-posn-x p) 30 | 3d-posn-x: undefined; 31 | cannot reference undefined identifier 32 | ; use the supertype's posn-x selector to access the x field: 33 | 34 | > (posn-x p) 35 | 1 36 | -------------------------------------------------------------------------------- /05.4 不透明结构类型与透明结构类型对比: -------------------------------------------------------------------------------- 1 | 5.4 不透明结构类型与透明结构类型对比 2 | 3 | 具有以下结构类型定义: 4 | 5 | (struct posn (x y)) 6 | 7 | 结构类型的实例以不显示字段值的任何信息的方式打印。也就是说,默认的结构类型是不透明的(opaque)。如果结构类型的访问器和修改器对一个模块保持私有,再没有其它的模块可以依赖这个类型实例的表示。 8 | 9 | 让结构型透明(transparent),在字段序列后面使用#:transparent关键字: 10 | 11 | 例如: 12 | 13 | (struct posn (x y) 14 | #:transparent) 15 | 16 | > (posn 1 2) 17 | (posn 1 2) 18 | 19 | 一个透明结构类型的实例像调用构造函数一样打印,因此它显示了结构字段值。透明结构类型也允许反射操作,比如struct?和struct-info,在其实例中使用(参见《反射和动态求值》)(Reflection and Dynamic Evaluation)。 20 | 21 | 默认情况下,结构类型是不透明的,因为不透明的结构实例提供了更多的封装保证。也就是说,一个库可以使用不透明的结构来封装数据,而库中的客户机除了在库中被允许之外,也不能操纵结构中的数据。 22 | -------------------------------------------------------------------------------- /05.5 结构的比较: -------------------------------------------------------------------------------- 1 | 5.5 结构的比较 2 | 3 | 一个通用的equal?比较自动出现在透明的结构类型的字段上,但是equal?默认仅针对不透明结构类型的实例标识: 4 | 5 | (struct glass (width height) #:transparent) 6 | 7 | > (equal? (glass 1 2) (glass 1 2)) 8 | #t 9 | 10 | (struct lead (width height)) 11 | 12 | > (define slab (lead 1 2)) 13 | > (equal? slab slab) 14 | #t 15 | 16 | > (equal? slab (lead 1 2)) 17 | #f 18 | 19 | 通过equal?支持实例比较而不需要使结构型透明,你可以使用#:methods关键字、gen:equal+hash并执行三个方法来实现: 20 | 21 | (struct lead (width height) 22 | #:methods 23 | gen:equal+hash 24 | [(define (equal-proc a b equal?-recur) 25 | ; 比较a和b 26 | (and (equal?-recur (lead-width a) (lead-width b)) 27 | (equal?-recur (lead-height a) (lead-height b)))) 28 | (define (hash-proc a hash-recur) 29 | ;计算a的第一个hash代码 30 | (+ (hash-recur (lead-width a)) 31 | (* 3 (hash-recur (lead-height a))))) 32 | (define (hash2-proc a hash2-recur) 33 | ;计算a的第二个hash代码 34 | (+ (hash2-recur (lead-width a)) 35 | (hash2-recur (lead-height a))))]) 36 | 37 | > (equal? (lead 1 2) (lead 1 2)) 38 | #t 39 | 40 | 列表中的第一个函数实现对两个lead的equal?测试;函数的第三个参数是用来代替equal?实现递归的相等测试,以便这个数据循环可以被正确处理。其它两个函数计算用于哈希表的一级和二级哈希代码: 41 | 42 | > (define h (make-hash)) 43 | > (hash-set! h (lead 1 2) 3) 44 | > (hash-ref h (lead 1 2)) 45 | 3 46 | 47 | > (hash-ref h (lead 2 1)) 48 | hash-ref: no value found for key 49 | key: # 50 | 51 | 这第一个函数提供gen:equal+hash,不需要递归比较结构的字段。例如,表示一个集合的结构类型可以通过检查集合的成员是相同的来执行相等操作,独立于内部表示的的元素顺序来实现相等。只要注意哈希函数对任何两个假定相等的结构类型都会产生相同的值。 52 | -------------------------------------------------------------------------------- /05.6 结构类型的生成性: -------------------------------------------------------------------------------- 1 | 5.6 结构类型的生成性 2 | 3 | 每次对一个struct表求值时,它就生成一个与所有现有结构类型不同的结构类型,即使某些其他结构类型具有相同的名称和字段。 4 | 5 | 这种生成性对执行抽象和执行程序是有用的,就像口译员,但小心放置struct表被多次求值的位置。 6 | 7 | 例如: 8 | 9 | (define (add-bigger-fish lst) 10 | (struct fish (size) #:transparent) ;每次都生成新的 11 | (cond 12 | [(null? lst) (list (fish 1))] 13 | [else (cons (fish (* 2 (fish-size (car lst)))) 14 | lst)])) 15 | 16 | > (add-bigger-fish null) 17 | (list (fish 1)) 18 | 19 | > (add-bigger-fish (add-bigger-fish null)) 20 | fish-size: contract violation; 21 | given value instantiates a different structure type with 22 | the same name 23 | expected: fish? 24 | given: (fish 1) 25 | 26 | (struct fish (size) #:transparent) 27 | (define (add-bigger-fish lst) 28 | (cond 29 | [(null? lst) (list (fish 1))] 30 | [else (cons (fish (* 2 (fish-size (car lst)))) 31 | lst)])) 32 | 33 | > (add-bigger-fish (add-bigger-fish null)) 34 | (list (fish 2) (fish 1)) 35 | -------------------------------------------------------------------------------- /05.7 预制结构类型: -------------------------------------------------------------------------------- 1 | 5.7 预制结构类型 2 | 3 | 虽然transparent结构类型以显示内容的方式打印,但结构的打印形式不能用于表达式中以获得结构,不像数字、字符串、符号或列表的打印形式。 4 | 5 | 预制(prefab)(“被预先制造”)结构类型是内置的类型,是已知的Racket打印机和表达式阅读器。有无限多这样的类型存在,他们索引是通过名字、字段计数、超类型以及其它细节。一个预制结构的打印形式类似于一个矢量,但它以#s开始而不是以#开始,而且打印表的第一个元素是预制结构类型的名称。 6 | 7 | 下面的示例显示具有一个字段的sprout预置结构类型的实例。第一个实例具有字段值'bean,第二个实例具有字段值'alfalfa: 8 | 9 | > '#s(sprout bean) 10 | '#s(sprout bean) 11 | 12 | > '#s(sprout alfalfa) 13 | '#s(sprout alfalfa) 14 | 15 | 像数字和字符串一样,预置结构是“自引用”,所以上面的引号是可选的: 16 | 17 | > #s(sprout bean) 18 | '#s(sprout bean) 19 | 20 | 当你随struct使用#:prefab关键字,而不是生成一个新的结构类型,你获得与现有的预制结构类型的绑定操作: 21 | 22 | > (define lunch '#s(sprout bean)) 23 | > (struct sprout (kind) #:prefab) 24 | > (sprout? lunch) 25 | #t 26 | 27 | > (sprout-kind lunch) 28 | 'bean 29 | 30 | > (sprout 'garlic) 31 | '#s(sprout garlic) 32 | 33 | 上面的字段名称kind对查找预置结构类型无关紧要,仅名称sprout和字段的数量是紧要的。同时,具有三个字段的预制结构类型sprout是一种不同于单个字段的结构类型: 34 | 35 | > (sprout? #s(sprout bean #f 17)) 36 | #f 37 | 38 | > (struct sprout (kind yummy? count) #:prefab) ;重定义 39 | > (sprout? #s(sprout bean #f 17)) 40 | #t 41 | 42 | > (sprout? lunch) 43 | #f 44 | 45 | 预制结构类型可以有另一种预制结构类型作为它的超类型,它具有可变的字段,并可以有自动字段。这些维度中的任何变化都对应于不同的预置结构类型,结构类型的名称的打印形式编码所有相关的细节。 46 | 47 | > (struct building (rooms [location #:mutable]) #:prefab) 48 | > (struct house building ([occupied #:auto]) #:prefab 49 | #:auto-value 'no) 50 | > (house 5 'factory) 51 | 52 | '#s((house (1 no) building 2 #(1)) 5 factory no) 53 | 54 | 每个预制结构类型都是透明的,但甚至比透明类型更抽象,因为可以创建实例而不必访问特定的结构类型声明或现有示例。总体而言,结构类型的不同选项提供了更抽象到更方便的各种可能性: 55 | 56 | 1、不透明的(Opaque)(默认):没有访问结构类型声明,就不能检查或创造实例。正如下一节所讨论的,构造函数和属性可以附加到结构类型上,以进一步保护或专门化其实例的行为。 57 | 58 | 2、透明的(Transparent):任何人都可以检查或创建一个没有访问结构类型声明的实例,这意味着值打印机可以显示实例的内容。然而,所有实例创建都通过一个构造函数守护程序,这样可以控制实例的内容,并且实例的行为可以通过属性进行专门化。由于结构类型是由其定义生成的,所以实例不能简单地通过结构类型的名称来生成,因此不能由表达式读取器自动生成。 59 | 60 | 3、预制(Prefab):任何人都可以在任何时候检查或创建实例,而不必事先访问结构类型声明或实例。因此,表达式读取器可以直接生成实例。实例不能具有构造函数守护程序或属性。 61 | 62 | 由于表达式读取器可以生成预制实例,所以在方便序列化比抽象更重要时它们是有用的。然而,不透明和透明的结构也可以被序列化,如果他们被serializable-struct定义,其描述见《数据类型和序列化》(Datatypes and Serialization.)。 63 | -------------------------------------------------------------------------------- /06 模块: -------------------------------------------------------------------------------- 1 | 6 模块 2 | 3 | 模块让你把Racket代码组织成多个文件和可重用的库。 4 | 5 | 6.1 模块基础知识 6 | 6.1.1 组织模块 7 | 6.1.2 库集合 8 | 6.1.3 包和集合 9 | 6.1.4 添加集合 10 | 6.2 模块的语法 11 | 6.2.1 module表 12 | 6.2.2 #lang速记法 13 | 6.2.3 子模块 14 | 6.2.4 主要的和测试的子模块 15 | 6.3 模块的路径 16 | 6.4 输入:require 17 | 6.5 输出:provide 18 | 6.6 赋值和重定义 19 | -------------------------------------------------------------------------------- /06.1 模块基础知识: -------------------------------------------------------------------------------- 1 | 6.1 模块基础知识 2 | 3 | 每个Racket模块通常驻留在自己的文件中。例如,假设文件“cake.rkt”包含以下模块: 4 | 5 | "cake.rkt" 6 | 7 | #lang racket 8 | 9 | (provide print-cake) 10 | 11 | ; draws a cake with n candles 12 | (define (print-cake n) 13 | (show " ~a " n #\.) 14 | (show " .-~a-. " n #\|) 15 | (show " | ~a | " n #\space) 16 | (show "---~a---" n #\-)) 17 | 18 | (define (show fmt n ch) 19 | (printf fmt (make-string n ch)) 20 | (newline)) 21 | 22 | 23 | 然后,其他模块可以导入“cake.rkt”以使用print-cake的函数,因为“cake.rkt”的provide行明确导出了print-cake的定义。show函数对"cake.rkt"是私有的(即它不能从其他模块被使用),因为show没有被导出。 24 | 25 | 下面的“random-cake.rkt”模块导入“cake.rkt”: 26 | 27 | "random-cake.rkt" 28 | 29 | #lang racket 30 | 31 | (require "cake.rkt") 32 | 33 | (print-cake (random 30)) 34 | 35 | 相对在导入(require "cake.rkt")内的引用“cake.rkt”的运行来说,如果“cake.rkt”和“random-cake.rkt”模块在同一个目录里。UNIX样式的相对路径用于所有平台上的相对模块引用,就像HTML页面中的相对的URL一样。 36 | -------------------------------------------------------------------------------- /06.1.1 组织模块: -------------------------------------------------------------------------------- 1 | 6.1.1 组织模块 2 | 3 | “cake.rkt”和“random-cake.rkt”示例演示如何组织一个程序模块的最常用的方法:把所有的模块文件在一个目录(也许是子目录),然后有模块通过相对路径相互引用。模块目录可以作为一个项目,因为它可以在文件系统上移动或复制到其它机器上,而相对路径则保存模块之间的连接。 4 | 5 | 另一个例子,如果你正在开发一个糖果分类程序,你可能有一个主要的“sort.rkt”模块,使用其他模块访问糖果数据库和控制分拣机。如果糖果数据库模块本身被组织成子模块以处理条码和厂家信息,那么数据库模块可以是“db/lookup.rkt”,它使用辅助模块“db/barcodes.rkt”和“db/makers.rkt”。同样,分拣机驱动程序“machine/control.rkt“可能会使用辅助模块”machine/sensors.rkt“和“machine/actuators.rkt”。 6 | 7 | https://docs.racket-lang.org/guide/pict.png 8 | 9 | “sort.rkt”模块使用相对路径“db/lookup.rkt”和“machine/control.rkt”从数据库和机器控制库导入: 10 | 11 | "sort.rkt" 12 | 13 | #lang racket 14 | (require "db/lookup.rkt" "machine/control.rkt") 15 | .... 16 | 17 | “db/lookup.rkt”模块类似地使用相对路径给它自己的源码以访问“db/barcodes.rkt”和“db/makers.rkt”模块: 18 | 19 | "db/lookup.rkt" 20 | 21 | #lang racket 22 | (require "barcode.rkt" "makers.rkt") 23 | .... 24 | 25 | 同上,“machine/control.rkt”: 26 | 27 | "machine/control.rkt" 28 | 29 | #lang racket 30 | (require "sensors.rkt" "actuators.rkt") 31 | .... 32 | 33 | Racket工具所有运行都自动使用相对路径。例如, 34 | 35 | racket sort.rkt 36 | 37 | 在命令行运行“sort.rkt”程序和自动加载并编译所需的模块。对于一个足够大的程序,从源码编译可能需要很长时间,所以使用 38 | 39 | raco make sort.rkt 40 | 41 | 编译“sort.rkt”及其所有依赖成为字节码文件。如果字节码文件存在,运行racket sort.rkt,将自动使用字节码文件。 42 | -------------------------------------------------------------------------------- /06.1.2 库集合: -------------------------------------------------------------------------------- 1 | 6.1.2 库集合 2 | 3 | 一个集合(collection)是已安装的库模块的按等级划分的组。一个集合中的模块通过一个引号引用,无后缀路径。例如,下面的模块引用“date.rkt”库,它是部分“racket”集合的一部分: 4 | 5 | #lang racket 6 | 7 | (require racket/date) 8 | 9 | (printf "Today is ~s\n" 10 | (date->string (seconds->date (current-seconds)))) 11 | 12 | 当搜索在线Racket文档时,搜索结果显示提供每个绑定的模块。或者,如果通过单击超链接到达绑定文档,则可以在绑定名称上悬停以查找哪些模块提供了它。 13 | 14 | 一个模块的引用,像racket/date,看起来像一个标识符,但它并不是和printf或date->string相同的方式对待。相反,当require发现一个被引号包括的模块的引用,它转化这个引用为基于集合的路径: 15 | 16 | 1、首先,如果这个引用路径不包含/,那么require自动添加一个“/main”给参考。例如,(require slideshow)相当于(require slideshow/main)。 17 | 18 | 2、其次,require隐式添加”.rkt”后缀给路径。 19 | 20 | 3、最后,require通过在已安装的集合中搜索路径来决定路径,而不是将路径处理为相对于封闭模块的路径。 21 | 22 | 作为一个最近似情况,集合作为文件系统目录实现。例如,“racket”集合大多位于Racket安装的“collects”目录中的“racket”目录中,如以下报告: 23 | 24 | #lang racket 25 | 26 | (require setup/dirs) 27 | 28 | (build-path (find-collects-dir) ;主集合的目录 29 | "racket") 30 | 31 | 然而,Racket安装的“collects”目录仅仅是一个require寻找目录集合的地方。其它地方包括用户指定的通过(find-user-collects-dir)报告的目录以及通过PLTCOLLECTS搜索路径配置的目录。最后,最典型的是,通过安装包(packages)找到集合。 32 | -------------------------------------------------------------------------------- /06.1.3 包和集合: -------------------------------------------------------------------------------- 1 | 6.1.3 包和集合 2 | 3 | 一个包(package)是通过Racket包管理器安装的一组库(或者预先安装在Racket分发中)。例如,racket/gui库是由“gui”包提供的,而parser-tools/lex是由“parser-tools”库提供的。 4 | 5 | Racket程序不直接针对包。相反,程序通过集合针对库,添加或删除一个包会改变可用的基于集合的库集。单个包可以为多个集合提供库,两个不同的包可以在同一集合中提供库(但不是同一个库,并且包管理器确保安装的包在该层级不冲突)。 6 | 7 | 有关包的更多信息,请参阅《Racket中的包管理》(Package Management in Racket)。 8 | -------------------------------------------------------------------------------- /06.1.4 添加集合: -------------------------------------------------------------------------------- 1 | 6.1.4 添加集合 2 | 3 | 回顾组织模块部分的糖果排序示例,假设“db/”和“machine/”中的模块需要一组常见的助手函数集。辅助函数可以放在一个“utils/”目录,同时模块“分db/”或“machine/”可以以开始于”../utils/”的相对路径访问公用模块 。只要一组模块在一个项目中协同工作,最好保持相对路径。程序员可以在不知道你的Racket配置的情况下跟踪相关路径的引用。 4 | 5 | 有些库是用于跨多个项目的,因此将库的源码保存在目录中使用是没有意义的。在这种情况下,最好的选择是添加一个新的集合。有了在一个集合中的库后,它可以通过一个封闭路径引用,就像是包括了Racket发行库的库一样。 6 | 7 | 你可以通过将文件放置在Racket安装包里或通过(get-collects-search-dirs)报告的一个目录下添加一个新的集合。或者,你可以通过设置PLTCOLLECTS环境变量添加到搜索目录列表。但最好的选择,是添加一个包。 8 | 9 | 创建包并不意味着您必须注册一个包服务器,或者执行一个将源代码复制到归档格式中的绑定步骤。创建包只意味着使用包管理器将你的库的本地访问作为当前源码位置的集合。 10 | 11 | 例如,假设你有一个目录“/usr/molly/bakery”,它包含“cake.rkt”模块(来自于本节的开始部分)和其它相关模块。为了使模块可以作为一个“bakery”集合获取,或者 12 | 13 | 1、使用raco pkg命令行工具: 14 | 15 | raco pkg install --link /usr/molly/bakery 16 | 17 | 当所提供的路径包含目录分隔符时,实际上不需要--link标记。 18 | 19 | 2、从“File”(文件)菜单使用DrRacket的DrRacket’s Package Manager(包管理器)项。在Do What I Mean面板,点击Browse...(浏览),选择“/usr/molly/bakery”目录,然后单击安装。 20 | 21 | 后来,(require bakery/cake)从任何模块将从”/usr/molly/bakery/cake.rkt“输入print-cake函数。 22 | 23 | 默认情况下,你安装的目录的名称既用作包名称,又用作包提供的集合。而且,包管理器通常默认只为当前用户安装,而不是在Racket安装的所有用户。有关更多信息,请参阅《Racket中的包管理》(Package Management in Racket)。 24 | 25 | 如果打算将库分发给其他人,请仔细选择集合和包名称。集合名称空间是分层的,但顶级集合名是全局的,包名称空间是扁平的。考虑将一次性库放在一些顶级名称,像“bakery”这种标识制造者。在制作烘焙食品库的最终集合时,使用像“bakery”这样的集合名。 26 | 27 | 在你的库之后被放入一个集合,你仍然可以使用raco make以编译库源,但更好而且更方便的是使用raco setup。raco setup命令取得一个集合名(而不是文件名)并编译集合内所有的库。此外,raco setup可以建立文档,并收集和添加文档到文档的索引,通过集合中的一个“info.rkt”模块做详细说明。有关raco setup的详细信息请看《raco setup:安装管理器》(raco setup: Installation Management)。 28 | -------------------------------------------------------------------------------- /06.2 模块的语法: -------------------------------------------------------------------------------- 1 | 6.2 模块的语法 2 | 3 | #lang在一个模块文件的开始,它开始一个对module表的简写,就像'是一种对quote表的简写。不同于',#lang简写在REPL内不能正常执行,部分是因为它必须由end-of-file终止,也因为#lang的普通写法依赖于封闭文件的名称。 4 | -------------------------------------------------------------------------------- /06.2.1 module表: -------------------------------------------------------------------------------- 1 | 6.2.1 module表 2 | 3 | 一个模块声明的普通写法形式,既可在REPL又可在一个文件中执行的是 4 | 5 | (module name-id initial-module-path 6 | decl ...) 7 | 8 | 其中的name-id是一个模块名,initial-module-path是一个初始的输入口,每个decl是一个输入口、输出口、定义或表达式。在文件的情况下,name-id通常与包含文件的名称相匹配,减去其目录路径或文件扩展名,但在模块通过其文件路径require时name-id被忽略。 9 | 10 | initial-module-path是必需的,因为即使是require表必须导入,以便在模块主体中进一步使用。换句话说,initial-module-path导入在主体内可供使用的引导语法。最常用的initial-module-path是racket,它提供了本指南中描述的大部分绑定,包括require、define和provide。另一种常用的initial-module-path是racket/base,它提供了较少的函数,但仍然是大多数最常用的函数和语法。 11 | 12 | 例如,前面一节里的“cake.rkt”例子可以写为 13 | 14 | (module cake racket 15 | (provide print-cake) 16 | 17 | (define (print-cake n) 18 | (show " ~a " n #\.) 19 | (show " .-~a-. " n #\|) 20 | (show " | ~a | " n #\space) 21 | (show "---~a---" n #\-)) 22 | 23 | (define (show fmt n ch) 24 | (printf fmt (make-string n ch)) 25 | (newline))) 26 | 27 | 此外,module表可以在REPL中求值以申明一个cake模块,不与任何文件相关联。为指向是这样一个独立模块,这样引用模块名称: 28 | 29 | 例如: 30 | 31 | > (require 'cake) 32 | > (print-cake 3) 33 | 34 | ... 35 | .-|||-. 36 | | | 37 | --------- 38 | 39 | 声明模块不会立即求值这个模块的主体定义和表达式。这个模块必须在顶层明确地被require以来触发求值。在求值被触发一次之后,后续的require不会重新对模块主体求值。 40 | 41 | 例如: 42 | 43 | > (module hi racket 44 | (printf "Hello\n")) 45 | > (require 'hi) 46 | Hello 47 | 48 | > (require 'hi) 49 | -------------------------------------------------------------------------------- /06.2.2 #lang简写: -------------------------------------------------------------------------------- 1 | 6.2.2 #lang简写 2 | 3 | #lang简写的主体没有特定的语法,因为语法是由如下#lang语言名称确定的。 4 | 5 | 在#lang racket的情况下,语法为: 6 | 7 | #lang racket 8 | decl ... 9 | 10 | 其读作如下同一内容: 11 | 12 | (module name racket 13 | decl ...) 14 | 15 | 这里的name是来自包含#lang表的文件名称。 16 | 17 | #lang racket/base表具有和#lang racket同样的语法,除了普通写法的扩展使用racket/base而不是racket。#lang scribble/manual表相反,有一个完全不同的语法,甚至看起来不像Racket,在这个指南里我们不准备去描述。 18 | 19 | 除非另有规定,一个模块是一个文档,它作为“语言”使用#lang标记法表示将以和#lang racket同样的方式扩大到module中。文档的语言名也可以直接使用module或require。 20 | -------------------------------------------------------------------------------- /06.2.3 子模块: -------------------------------------------------------------------------------- 1 | 6.2.3 子模块 2 | 3 | 一个module表可以被嵌套在一个模块内,在这种情况下,这个嵌套module表声明一个子模块。子模块可以通过外围模块使用一个引用名称直接引用。下面的例子通过从zoo子模块导入tiger打印“Tony”: 4 | 5 | "park.rkt" 6 | 7 | #lang racket 8 | 9 | (module zoo racket 10 | (provide tiger) 11 | (define tiger "Tony")) 12 | 13 | (require 'zoo) 14 | 15 | tiger 16 | 17 | 运行一个模块不是必须运行子模块。在上面的例子中,运行“park.rkt”运行它的子模块zoo仅因为“park.rkt”模块require了这个zoo子模块。否则,一个模块及其子模块可以独立运行。此外,如果“park.rkt“被编译成字节码文件(通过raco make),那么“park.rkt”代码或zoo代码可以独立下载。 18 | 19 | 子模块可以嵌套子模块,而且子模块可以被一个模块通过使用子模块路径直接引用,不同于它的外围模块。 20 | 21 | 一个module*表类似于一个嵌套的module表: 22 | 23 | (module* name-id initial-module-path-or-#f 24 | decl ...) 25 | 26 | module*表不同于module,它反转这个对于子模块和外围模块的参考的可能性: 27 | 28 | 1、用module申明的一个子模块模块可通过其外围模块require,但子模块不能require外围模块或在词法上参考外围模块的绑定。 29 | 30 | 2、用module*申明的一个子模块可以require其外围模块,但外围模块不能require子模块。 31 | 32 | 此外,一个module*表可以在initial-module-path的位置指定#f,在这种情况下,所有外围模块的绑定对子模块可见——包括没有使用provide输出的绑定。 33 | 34 | 用module*和#f申明的子模块的一个应用是通过子模块输出附加绑定,那不是通常的从模块输出: 35 | 36 | "cake.rkt" 37 | 38 | #lang racket 39 | 40 | (provide print-cake) 41 | 42 | (define (print-cake n) 43 | (show " ~a " n #\.) 44 | (show " .-~a-. " n #\|) 45 | (show " | ~a | " n #\space) 46 | (show "---~a---" n #\-)) 47 | 48 | (define (show fmt n ch) 49 | (printf fmt (make-string n ch)) 50 | (newline)) 51 | 52 | (module* extras #f 53 | (provide show)) 54 | 55 | 在这个修订的“cake.rkt”模块,show不是被一个模块输入,它采用(require "cake.rkt"),因为大部分“cake.rkt“的用户不想要那些额外的函数。一个模块可以要求extra子模块使用(require (submod "cake.rkt" extras))访问另外的隐藏的show函数。 56 | -------------------------------------------------------------------------------- /06.4 导入:require: -------------------------------------------------------------------------------- 1 | 6.4 导入:require 2 | 3 | 从另一个模块导入require表。一个require表可以出现在一个模块中,在这种情况下,它将指定模块的绑定引入到导入的模块中。一个require表也可以出现在顶层,在这种情况下,既导入绑定也实例化指定的模块;即,它对指定模块的主体和表达式求值,如果他们还没有被求值。 4 | 5 | 单个的require可以同时指定多个导入: 6 | 7 | (require require-spec ...) 8 | 9 | 在一个单一的require表里指定多个require-spec,从本质上与使用多个require,每个单独包含一个单一的require-spec是相同的。区别很小,且局限于顶层:一个独立的require可以导入一个给定标识符最多一次,而一个单独的require可以代替以前require的绑定(都是只局限于顶层,在一个模块之外)。 10 | 11 | require-spec的允许形态是递归定义的: 12 | 13 | 1、module-path 14 | 15 | 在最简单的形式中,require-spec是一个module-path(如前一节《模块路径》(Module Paths)中定义的)。在这种情况下,require所引入的绑定通过provide声明来确定,其中在每个模块通过各个module-path引用。 16 | 17 | 例如: 18 | 19 | > (module m racket 20 | (provide color) 21 | (define color "blue")) 22 | > (module n racket 23 | (provide size) 24 | (define size 17)) 25 | > (require 'm 'n) 26 | > (list color size) 27 | 28 | '("blue" 17) 29 | 30 | 2、 31 | (only-in require-spec id-maybe-renamed ...) 32 | 33 | id-maybe-renamed = id 34 | | [orig-id bind-id] 35 | 36 | 一个only-in表限制绑定设置,它将通过require-spec引入。此外,only-in选择重命名每个绑定,它被保护:在[orig-id bind-id]表里,orig-id是指一个被require-spec隐含的绑定,并且bind-id是这个在导入上下文中将被绑定的名称,以代替orig-id。 37 | 38 | 例如: 39 | 40 | > (module m (lib "racket") 41 | (provide tastes-great? 42 | less-filling?) 43 | (define tastes-great? #t) 44 | (define less-filling? #t)) 45 | > (require (only-in 'm tastes-great?)) 46 | > tastes-great? 47 | 48 | #t 49 | > less-filling? 50 | less-filling?: undefined; 51 | cannot reference undefined identifier 52 | 53 | > (require (only-in 'm [less-filling? lite?])) 54 | > lite? 55 | #t 56 | 57 | 3、(except-in require-spec id ...) 58 | 59 | 这种形式是only-in的补充:它从以require-spec指定的集合中排除指定的绑定。 60 | 61 | 4、(rename-in require-spec [orig-id bind-id] ...) 62 | 63 | 这种形式支持类似于only-in的重命名,但从require-spec中分离单独的标识符,它们没有作为一个orig-id提交。 64 | 65 | 5、(prefix-in prefix-id require-spec) 66 | 67 | 这是一个重命名的简写,prefix-id添加到用require-spec指定的每个标识符的前面。 68 | 69 | 除了only-in、except-in、rename-in和prefix-in表可以嵌套以实现更复杂的导入绑定操作。例如, 70 | 71 | (require (prefix-in m: (except-in 'm ghost))) 72 | 73 | 导入m输出的所有绑定,除ghost绑定之外,并带用m前缀的局部名字: 74 | 75 | 等价地,prefix-in可以被应用在except-in之前,只是带except-in的省略是用m前缀指定: 76 | 77 | (require (except-in (prefix-in m: 'm) m:ghost)) 78 | -------------------------------------------------------------------------------- /06.5 输出:provide: -------------------------------------------------------------------------------- 1 | 6.5 输出:provide 2 | 3 | 默认情况下,一个模块的所有定义对模块都是私有的。provide表指定定义,以使在模块require的地方可获取。 4 | 5 | (provide provide-spec ...) 6 | 7 | 一个provide表只能出现在模块级(即一个module的当前主体)中。在一个单一的provide中指定多个provide-spec,那和使用多个provide,其每一个有单一的provide-spec,明显是一样的。 8 | 9 | 每个标识符最多可以从模块中导出一次,遍及模块中的所有provide。更确切地说,每个导出的外部名称必须是不同的;相同的内部绑定可以用不同的外部名称多次导出。 10 | 11 | 允许的provide-spec形式是递归定义的: 12 | 13 | 1、identifier 14 | 15 | 在最简单的形式中,provide-spec标明一个绑定,它在被导出的模块内。绑定可以来自于局部定义,也可以来自于一个导入。 16 | 17 | (rename-out [orig-id export-id] ...) 18 | 19 | 一个rename-out表类似于只指定一个标识符,但这个导出绑定orig-id是给定一个不同的名称,export-id,给导入模块。 20 | 21 | 2、(struct-out struct-id) 22 | 23 | 一个结构表导出由(struct struct-id ....)创建的绑定。 24 | 25 | 3、(all-defined-out) 26 | 27 | all-defined-out简写导出所有的绑定,其定义在导出模块中(与导入相反)。 28 | 29 | all-defined-out简写的使用通常是被阻止的,因为它不太清楚模块的实际导出,而且因为Racket程序员习惯于认为可以自由地将定义添加到模块,而不影响其公共接口(在all-defined-out被使用时候都不是这样)。 30 | 31 | 4、(all-from-out module-path) 32 | 33 | all-from-out简写输出模块中的所有绑定,该模块使用一个基于module-path的require-spec导入。 34 | 35 | 尽管不同的module-path可以引用同一个基于文件的模块,但是带all-from-out的重复导出是明确基于module-path引用,而不是实际引用的模块。 36 | 37 | 5、(except-out provide-spec id ...) 38 | 39 | 就像provide-spec,但省略每个id的导出,其中id是要省略的绑定的外部名称。 40 | 41 | 6、(prefix-out prefix-id provide-spec) 42 | 43 | 就像provide-spec,但为每个导出的绑定添加prefix-id到外部名称的开头。 44 | -------------------------------------------------------------------------------- /06.6 赋值和重定义: -------------------------------------------------------------------------------- 1 | 6.6 赋值和重定义 2 | 3 | 在一个模块的变量定义上的set!使用,仅限于定义模块的主体。也就是说,一个模块可以改变它自己定义的值,这样的变化对于导入模块是可见的。但是,一个导入上下文不允许更改导入绑定的值。 4 | 5 | 例如: 6 | 7 | > (module m racket 8 | (provide counter increment!) 9 | (define counter 0) 10 | (define (increment!) 11 | (set! counter (add1 counter)))) 12 | > (require 'm) 13 | > counter 14 | 0 15 | 16 | > (increment!) 17 | > counter 18 | 1 19 | 20 | > (set! counter -1) 21 | set!: cannot mutate module-required identifier 22 | in: counter 23 | 24 | 在上述例子中,一个模块可以给别人提供一个改变其输出的能力,通过提供一个修改函数实现,如increment!。 25 | 26 | 禁止导入变量的分配有助于支持程序的模块化推理。例如,在模块中, 27 | 28 | (module m racket 29 | (provide rx:fish fishy-string?) 30 | (define rx:fish #rx"fish") 31 | (define (fishy-string? s) 32 | (regexp-match? rx:fish s))) 33 | 34 | fishy-string?函数将始终匹配包含“fish”的字符串,不管其它模块如何使用rx:fish绑定。从本质上来说,它帮助程序员的原因是,禁止对导入的赋值也允许更有效地执行许多程序。 35 | 36 | 在同一行中,当一个模块不包含set!,在模块中定义的特定标识符,那该标识符被认为是一个常数(constant),不能更改——即使重新声明该模块。 37 | 38 | 因此,通常不允许重新声明模块。对于基于文件的模块,简单地更改文件不会导致任何情况下的重新声明,因为基于文件的模块是按需加载的,而先前加载的声明满足将来的请求。它可以使用Racket的反射支持重新声明一个模块,而非文件模块可以重新在REPL中声明;在这种情况下,如果涉及一个以前的静态绑定,重新申明可能失败。 39 | 40 | > (module m racket 41 | (define pie 3.141597)) 42 | > (require 'm) 43 | > (module m racket 44 | (define pie 3)) 45 | 46 | define-values: assignment disallowed; 47 | cannot re-define a constant 48 | constant: pie 49 | in module: 'm 50 | 51 | 作为探索和调试目的,Racket反射层提供了一个compile-enforce-module-constants参数来使常量的执行无效。 52 | 53 | > (compile-enforce-module-constants #f) 54 | > (module m2 racket 55 | (provide pie) 56 | (define pie 3.141597)) 57 | > (require 'm2) 58 | > (module m2 racket 59 | (provide pie) 60 | (define pie 3)) 61 | > (compile-enforce-module-constants #t) 62 | > pie 63 | 64 | 3 65 | -------------------------------------------------------------------------------- /07 合约: -------------------------------------------------------------------------------- 1 | 7 合约 2 | 3 | 本章对Racket的合合约系统作了介绍。 4 | 5 | 7.1 合约和边界 6 | 7.1.1合约的违反 7 | 7.1.2 合约与模块的测试 8 | 7.1.3 嵌套合约的测试 9 | 7.2 函数的简单合约 10 | 7.2.1 ->类型 11 | 7.2.2 define/contract和->的使用 12 | 7.2.3 and和any/c 13 | 7.2.4 运转自己的合约 14 | 7.2.5 合约的高阶函数 15 | 7.2.6 ”???“的合约信息 16 | 7.2.7 解析合约的错误消息 17 | 7.3个合约的通用函数 18 | 7.3.1 可选参数 19 | 7.3.2 剩余参数 20 | 7.3.3 关键字参数 21 | 7.3.4 可选关键字参数 22 | 7.3.5 case-lambda合约 23 | 7.3.6 参数和结果的依赖 24 | 7.3.7 检查状态的变化 25 | 7.3.8 多个结果值 26 | 7.3.9 固定但静态未知数量的参数 27 | 7.4合约:一个全面的例子 28 | 7.5 结构的合约 29 | 7.5.1 对特定值的确保 30 | 7.5.2 对所有值的确保 31 | 7.5.3 数据结构的检查特性 32 | 7.6 用#:exists和#:∃抽象合约 33 | 7.7 额外的例子 34 | 7.7.1 客户管理器的组成 35 | 7.7.2 参数(简单)栈 36 | 7.7.3 字典 37 | 7.7.4 队列 38 | 7.8 建立新合约 39 | 7.8.1 合约结构属性 40 | 7.8.2 所有的警告和报警 41 | 7.9 陷阱 42 | 7.9.1 合约和eq? 43 | 7.9.2 合同的边界和define/contract 44 | 7.9.3 合约的生存期和判定 45 | 7.9.4 定义递归合合约 46 | 7.9.5 混合set!和合约 47 | -------------------------------------------------------------------------------- /07.1 合约和边界: -------------------------------------------------------------------------------- 1 | 7.1 合约和边界 2 | 3 | 如同两个商业伙伴之间的合约,软件合约是双方之间的协议。协议规定了从一方传给另一方的每一产品(或价值)的义务和保证。 4 | 5 | 因此,合约确定了双方之间的边界。当价值跨越边界时,合约监督系统执行合约检查,确保合作伙伴遵守既定合约。 6 | 7 | 在这种精神下,Racket支持合约主要在模块边界。具体来说,程序员可以附加合约来provide从句,从而对导出值的使用施加约束和承诺。例如,导出说明 8 | 9 | #lang racket 10 | 11 | (provide (contract-out [amount positive?])) 12 | (define amount ...) 13 | 14 | 承诺上述模块的所有客户,amount值将始终是正数。合约系统仔细地监测了该模块的义务。每次客户提到amount时,监视器都会检查amount值是否确实是正数。 15 | 16 | 合约库是建立在Racket语言中内部的,但是如果你想使用racket/base,你可以像这样明确地导入合约库: 17 | 18 | #lang racket/base 19 | (require racket/contract) ;现在我就可以写合约了。 20 | 21 | (provide (contract-out [amount positive?])) 22 | 23 | (define amount ...) 24 | -------------------------------------------------------------------------------- /07.1.1合约的违反: -------------------------------------------------------------------------------- 1 | 7.1.1合约的违反 2 | 3 | 如果我们将amount绑定到一个非正的数字, 4 | 5 | #lang racket 6 | 7 | (provide (contract-out [amount positive?])) 8 | 9 | (define amount 0) 10 | 11 | 然后,当需要模块时,监控系统发出违反合同的信号,并指责模块违反了承诺。 12 | 13 | 更大的错误是将amount绑定到非数值上: 14 | 15 | #lang racket 16 | 17 | (provide (contract-out [amount positive?])) 18 | 19 | (define amount 'amount) 20 | -------------------------------------------------------------------------------- /07.1.2 合约与模块的测试: -------------------------------------------------------------------------------- 1 | 7.1.2 合约与模块的测试 2 | 3 | 在这一章中的所有合同和模块(不包括那些只是跟随)是使用标准的#lang语法描述的模块。由于模块是合约中各方之间的边界,所以示例涉及多个模块。 4 | 5 | 测试与多个模块在一个单一的模块或DrRacket的定义范围内,使用Racket的子模块。例如,测试一下本节前面的示例,如下所示: 6 | 7 | #lang racket 8 | 9 | (module+ server 10 | (provide (contract-out [amount (and/c number? positive?)])) 11 | (define amount 150)) 12 | 13 | (module+ main 14 | (require (submod ".." server)) 15 | (+ amount 10)) 16 | 17 | 每个模块及其合约都用前面的module+关键字封装在圆括号中。 module后面的第一个表是模块的名称,将在随后的require语句中使用(其中每个引用都通过一个require对名称用“…”进行前缀)。 18 | -------------------------------------------------------------------------------- /07.1.3 嵌套合约的测试: -------------------------------------------------------------------------------- 1 | 7.1.3 嵌套合约的测试 2 | 3 | 在许多情况下,在模块边界上附加合约是有意义的。然而,能够以比模块更细致的方式使用合约通常是便利的。define/contract表允许这种使用: 4 | 5 | #lang racket 6 | 7 | (define/contract amount 8 | (and/c number? positive?) 9 | 150) 10 | 11 | (+ amount 10) 12 | 13 | 在这个例子中,define/contract表在amount定义和它周围的上下文之间建立了一个合约边界。换言之,这里的双方是包含它的定义和模块。 14 | 15 | 创造这些嵌套的合约边界的表的使用有时是微妙的,因为他们可能会有意想不到的影响性能或指责似乎不直观的一方。这些微妙之处在《define/contract和->的使用》(Using define/contract and ->)和《合同的边界和define/contract》(Contract boundaries and define/contract 16 | )中得到了解释。 17 | -------------------------------------------------------------------------------- /07.2 函数的简单合约: -------------------------------------------------------------------------------- 1 | 7.2 函数的简单合约 2 | 3 | 一个数学函数有一个域(domain)和一个范围(range)。域指示函数可以作为参数接受的值类型,范围指示它生成的值类型。描述函数及其域和范围的惯常的符号是 4 | 5 | f : A -> B 6 | 7 | 其中A是函数的域,B是范围。 8 | 9 | 编程语言中的函数也有域和范围,而合约可以确保函数只接收域中的值,只在其范围内生成值。A ->为函数创建一个这样的合约。一个->之后的表指定域的合约,最后指定范围的合约。 10 | 11 | 这里有一个模块,可以代表一个银行帐户: 12 | 13 | #lang racket 14 | 15 | (provide (contract-out 16 | [deposit (-> number? any)] 17 | [balance (-> number?)])) 18 | 19 | (define amount 0) 20 | (define (deposit a) (set! amount (+ amount a))) 21 | (define (balance) amount) 22 | 23 | 这个模块导出两个函数: 24 | 25 | 1、deposit,接受一个数字并返回某个未在合约中指定的值, 26 | 27 | 2、balance,返回一个指示账户当前余额的数字。 28 | 29 | 当一个模块导出一个函数时,它在两个通道之间建立一个“服务器”(server)并导入该函数的“客户机”(client)模块之间的通信通道。如果客户机模块调用该函数,它将向服务器模块发送一个值。相反,如果这样的函数调用结束,函数返回一个值,服务器模块就会将一个值发送回客户机模块。这种区分客户机-服务器的区别是很重要的,因为当出现问题时,一方或另一方应受到责备。 30 | 31 | 如果客户机模块向'millions申请存款(deposit),这将违反合约。合约监督系统会抓住这一违规行为,并责怪客户与上面的模块违反合同。与此相反,如果平衡功能返回'broke,合同监控系统将归咎于服务器模块。 32 | 33 | 一个->本身不是合约;它是一种合约组合(contract combinator),它结合其它合约构成合约。 34 | -------------------------------------------------------------------------------- /07.2.1 ->类型: -------------------------------------------------------------------------------- 1 | 7.2.1 ->类型 2 | 3 | 如果你已经习惯了数学函数,你可以选择一个合约箭头出现在函数的域和范围内,而不是在开头。如果你已经读过《如何设计程序》(How to Design Programs),你已经见过这个很多次了。事实上,你可能在其他人的代码中见过这样的合约: 4 | 5 | (provide (contract-out 6 | [deposit (number? . -> . any)])) 7 | 8 | 如果Racket的S表达式包含中间有一个符号的两个点,读取器重新安排S表达式并安置符号到前面,如《列表和Racket的语法》(Lists and Racket Syntax)叙述的那样。因此, 9 | 10 | (number? . -> . any) 11 | 12 | 只是以下内容的另一种写作方式 13 | 14 | (-> number? any) 15 | -------------------------------------------------------------------------------- /07.2.2 define|contract和->的使用: -------------------------------------------------------------------------------- 1 | 7.2.2 define/contract和->的使用 2 | 3 | 在《嵌套的合约边界测试》(Experimenting with Nested Contract Boundaries)中引入的define/contract表也可以用来定义合约中的函数。例如, 4 | 5 | (define/contract (deposit amount) 6 | (-> number? any) 7 | ; implementation goes here 8 | ....) 9 | 10 | 它用合约更早定义deposit函数。请注意,这对deposit的使用有两个潜在的重要影响: 11 | 12 | 1、由于合约总是在调用deposit时进行检查,即使在定义它的模块内,这也可能增加合约被检查的次数。这可能导致性能下降。如果函数在循环中反复调用或使用递归,尤其如此。 13 | 14 | 2、在某些情况下,当同一模块中的其它代码调用时,可以编写一个函数来接受更宽松的一组输入。对于此类用例,通过define/contract建立的合同边界就过于严格。 15 | -------------------------------------------------------------------------------- /07.2.3 and-any|c: -------------------------------------------------------------------------------- 1 | 7.2.3 and和any/c 2 | 3 | 用于deposit的any合约符合任何结果,只能在函数合约的范围内使用。代替上面的any,我们可以使用更具体的合约 void?,它表示函数总是返回(void)值。然而,void?合约要求合约监控系统每次调用函数时都要检查返回值,即使“客户机”模块不能很好地处理这个值。相反,any告诉监控系统不检查返回值,它告诉潜在客户机,“服务器”模块对函数的返回值不作任何承诺,甚至不管它是单个值或多个值。 4 | 5 | any/c合约和any类似,因为它对值没有任何要求。与any不同的是,any/c表示一个单个值,它适合用作参数合约。使用any/c作为范围合约,强迫对函数产生的一个单个值进行检查。就像这样, 6 | 7 | (-> integer? any) 8 | 9 | 描述一个函数,该函数接受一个整数并返回任意数量的值,然而 10 | 11 | (-> integer? any/c) 12 | 13 | 描述接受整数并生成单个结果的函数(但对结果没有更多说明)。以下函数 14 | 15 | (define (f x) (values (+ x 1) (- x 1))) 16 | 17 | 匹配(-> integer? any),但不匹配(-> integer? any/c)。 18 | 19 | 当从一个函数获得一个单个结果的承诺特别重要时,使用any/c作为结果的合约。当希望尽可能少地承诺(并尽可能少地检查)函数的结果时,使用any合约。 20 | -------------------------------------------------------------------------------- /07.2.5 合约的高阶函数: -------------------------------------------------------------------------------- 1 | 7.2.5 合约的高阶函数 2 | 3 | 函数合约不仅仅局限于域或范围上的简单断言。在这里论述的任何合约组合,包括函数合约本身,可作为一个函数的参数和结果的合约。 4 | 5 | 例如: 6 | 7 | (-> integer? (-> integer? integer?)) 8 | 9 | 是一份合约,描述了一个咖喱函数。它匹配接受一个参数的函数,并且接着在返回一个整数之前返回另一个接受第二个参数的函数。如果服务器使用这个合约导出一个函数make-adder,如果make-adder返回一个非函数的值,那么应归咎于服务器。如果make-adder确实返回一个函数,但得到的函数应用于一个非整数的值,则应归咎于客户机。 10 | 11 | 同样, 12 | 13 | (-> (-> integer? integer?) integer?) 14 | 15 | 描述接受其它函数作为输入的函数。如果一个服务器用它的合约导出一个函数twice,那么twice应用给一个值而不是一个带一个参数的函数,那么归咎于客户机。如果twice应用给一个带一个参数的函数,并且twice调用这个给定的函数作为值而不是一个整数,那么归咎于服务器。 16 | -------------------------------------------------------------------------------- /07.2.6 带”???“的合约信息: -------------------------------------------------------------------------------- 1 | 7.2.6 带”???“的合约信息 2 | 3 | 你写了你的模块。你增加了合约。你将它们放入接口,以便客户机程序员拥有来自接口的所有信息。这是一门艺术: 4 | 5 | > (module bank-server racket 6 | (provide 7 | (contract-out 8 | [deposit (-> (λ (x) 9 | (and (number? x) (integer? x) (>= x 0))) 10 | any)])) 11 | 12 | (define total 0) 13 | (define (deposit a) (set! total (+ a total)))) 14 | 15 | 几个客户机使用了您的模块。其他人转而使用了他们的模块。突然他们中的一个看到了这个错误消息: 16 | 17 | > (require 'bank-server) 18 | > (deposit -10) 19 | 20 | deposit: contract violation 21 | expected: ??? 22 | given: -10 23 | in: the 1st argument of 24 | (-> ??? any) 25 | contract from: bank-server 26 | blaming: top-level 27 | (assuming the contract is correct) 28 | at: eval:2.0 29 | 30 | ???在那里代表什么?如果我们有这样一个数据类型的名字,就像我们有字符串、数字等等,那不是很好吗? 31 | 32 | 在这种情况下,Racket提供了扁平命名合约(flat named contract)。在这一术语中使用“合约”表明合约是一等价值。“扁平”(flat)意味着数据集是内置数据原子类的一个子集;它们由一个断言描述,该断言接受所有的Racket值并产生布尔值。“命名”(named)部分表示我们想要做的事情,这就命名了这个合约,这样错误消息就变得明白易懂了: 33 | 34 | > (module improved-bank-server racket 35 | (provide 36 | (contract-out 37 | [deposit (-> (flat-named-contract 38 | 'amount 39 | (λ (x) 40 | (and (number? x) (integer? x) (>= x 0)))) 41 | any)])) 42 | 43 | (define total 0) 44 | (define (deposit a) (set! total (+ a total)))) 45 | 46 | 通过这个小小的更改,错误消息变得相当易读: 47 | 48 | > (require 'improved-bank-server) 49 | > (deposit -10) 50 | 51 | deposit: contract violation 52 | expected: amount 53 | given: -10 54 | in: the 1st argument of 55 | (-> amount any) 56 | contract from: improved-bank-server 57 | blaming: top-level 58 | (assuming the contract is correct) 59 | at: eval:5.0 60 | -------------------------------------------------------------------------------- /07.2.7 解析合约的错误消息: -------------------------------------------------------------------------------- 1 | 7.2.7 解析合约的错误消息 2 | 3 | 一般来说,每个合约的错误信息由六部分组成: 4 | 5 | 1、函数或方法的名称结合合约和短语“contract violation”(违反合约)或“broke its contract”(违反合约),这取决于合约是否受到客户机或服务器的违反;例如,在前一个示例中为: 6 | 7 | deposit: contract violation 8 | 9 | 2、对违反合约的准确的哪一方面的描述, 10 | 11 | expected: amount 12 | given: -10 13 | 14 | 3、完整的合约加上一个方向指明哪个方面被违反, 15 | 16 | in: the 1st argument of 17 | (-> amount any) 18 | 19 | 4、合约表达的模块(或者更广泛地说,是合同所规定的边界), 20 | 21 | contract from: improved-bank-server 22 | 23 | 5、应归咎于哪个, 24 | 25 | blaming: top-level 26 | (assuming the contract is correct) 27 | 28 | 6、以及合约出现的源程序定位。 29 | 30 | at: eval:5.0 31 | -------------------------------------------------------------------------------- /07.3 通常的函数合约: -------------------------------------------------------------------------------- 1 | 7.3 通常的函数合约 2 | 3 | ->合约构造函数为固定数量参数的函数工作,并且结果合约与输入参数无关。为了支持函数的其它类型,Racket提供额外的合约构造函数,尤其是->*和->i。 4 | -------------------------------------------------------------------------------- /07.3.1 可选参数: -------------------------------------------------------------------------------- 1 | 7.3.1 可选参数 2 | 3 | 请看一个字符串处理模块的摘录,该灵感来自于《Scheme cookbook》: 4 | 5 | #lang racket 6 | 7 | (provide 8 | (contract-out 9 | ; 用(optional) char填充给定的左右两个字符串,使其居中。 10 | [string-pad-center (->* (string? natural-number/c) 11 | (char?) 12 | string?)])) 13 | 14 | (define (string-pad-center str width [pad #\space]) 15 | (define field-width (min width (string-length str))) 16 | (define rmargin (ceiling (/ (- width field-width) 2))) 17 | (define lmargin (floor (/ (- width field-width) 2))) 18 | (string-append (build-string lmargin (λ (x) pad)) 19 | str 20 | (build-string rmargin (λ (x) pad)))) 21 | 22 | 模块导出string-pad-center,该函数用给定字符串在中心创建一个给定的width的字符串。默认的填充字符是#\space;如果客户机希望使用一个不同的字符,它可以调用string-pad-center,用第三个参数,一个char,重写默认的值。 23 | 24 | 函数定义使用可选参数,这对于这种功能是合适的。这里有趣的一点是string-pad-center的合约。 25 | 26 | 合约组合->*,要求几组合约: 27 | 28 | 1、第一个是一个对所有必需参数合约的括号组。在这个例子中,我们看到两个:string?和natural-number/c。 29 | 30 | 2、第二个是对所有可选参数合约的括号组:char?。 31 | 32 | 3、最后一个是一个单一的合约:函数的结果。 33 | 34 | 请注意,如果默认值不满足合约,则不会获得此接口的合约错误。如果不能信任你自己去正确获得初始值,则需要在边界上传递初始值。 35 | -------------------------------------------------------------------------------- /07.3.2 剩余参数: -------------------------------------------------------------------------------- 1 | 7.3.2 剩余参数 2 | 3 | max操作符至少接受一个实数,但它接受任意数量的附加参数。您可以使用剩余参数(rest argument)编写其它此类函数,例如在max-abs中: 4 | 5 | (define (max-abs n . rst) 6 | (foldr (lambda (n m) (max (abs n) m)) (abs n) rst)) 7 | 8 | 通过一个合同描述此函数需要进一步扩展->*:一个#:rest关键字在必需参数和可选参数之后指定一个参数列表合约: 9 | 10 | (provide 11 | (contract-out 12 | [max-abs (->* (real?) () #:rest (listof real?) real?)])) 13 | 14 | 正如对->*的通常情况,必需参数合约被封闭在第一对括号中,在这种情况下是一个实数。空括号表示没有可选参数(不包括剩余参数)。剩余参数合约如下:#:rest;因为所有的额外的参数必须是实数,剩余参数列表必须满足合约 (listof real?)。 15 | -------------------------------------------------------------------------------- /07.3.3 关键字参数: -------------------------------------------------------------------------------- 1 | 7.3.3 关键字参数 2 | 3 | 原来->合约构造函数也包含对关键字参数的支持。例如,考虑这个函数,它创建一个简单的GUI并向用户询问一个“yes-or-no”的问题: 4 | 5 | #lang racket/gui 6 | 7 | (define (ask-yes-or-no-question question 8 | #:default answer 9 | #:title title 10 | #:width w 11 | #:height h) 12 | (define d (new dialog% [label title] [width w] [height h])) 13 | (define msg (new message% [label question] [parent d])) 14 | (define (yes) (set! answer #t) (send d show #f)) 15 | (define (no) (set! answer #f) (send d show #f)) 16 | (define yes-b (new button% 17 | [label "Yes"] [parent d] 18 | [callback (λ (x y) (yes))] 19 | [style (if answer '(border) '())])) 20 | (define no-b (new button% 21 | [label "No"] [parent d] 22 | [callback (λ (x y) (no))] 23 | [style (if answer '() '(border))])) 24 | (send d show #t) 25 | answer) 26 | 27 | (provide (contract-out 28 | [ask-yes-or-no-question 29 | (-> string? 30 | #:default boolean? 31 | #:title string? 32 | #:width exact-integer? 33 | #:height exact-integer? 34 | boolean?)])) 35 | 36 | ask-yes-or-no-question的合同使用->,同样的方式,lambda(或基于define的函数)允许关键字在函数正式参数之前,->允许关键字先于函数合约的参数合约。在这种情况下,合约表明ask-yes-or-no-question必须得到四个关键字参数,各个关键字为#:default、#:title、#:width和#:height。在函数定义中,函数中的关键字之间的->相对顺序对函数的客户机并不重要;只有没有关键字的参数合约的相对顺序。 37 | -------------------------------------------------------------------------------- /07.3.4 可选关键字参数: -------------------------------------------------------------------------------- 1 | 7.3.4 可选关键字参数 2 | 3 | 当然,ask-yes-or-no-question(从上一个问题中引来)中有许多参数有合理的默认值,应该是可选的: 4 | 5 | (define (ask-yes-or-no-question question 6 | #:default answer 7 | #:title [title "Yes or No?"] 8 | #:width [w 400] 9 | #:height [h 200]) 10 | ...) 11 | 12 | 要指定这个函数的合约,我们需要再次使用->*。它支持关键字,正如你在可选参数和强制参数部分中所期望的一样。在这种情况下,我们有强制关键字#:default和可选关键字#:title、#:width和#:height。所以,我们像这样写合约: 13 | 14 | (provide (contract-out 15 | [ask-yes-or-no-question 16 | (->* (string? 17 | #:default boolean?) 18 | (#:title string? 19 | #:width exact-integer? 20 | #:height exact-integer?) 21 | 22 | boolean?)])) 23 | 24 | 也就是说,我们在第一节中使用了强制关键字,并在第二部分中选择了可选关键字。 25 | -------------------------------------------------------------------------------- /07.3.5 case-lambda合约: -------------------------------------------------------------------------------- 1 | 7.3.5 case-lambda合约 2 | 3 | 用case-lambda定义的函数可能对其参数施加不同的约束,这取决于其提供了多少。例如,report-cost函数可以将一对数字或字符串转换为一个新字符串: 4 | 5 | (define report-cost 6 | (case-lambda 7 | [(lo hi) (format "between $~a and $~a" lo hi)] 8 | [(desc) (format "~a of dollars" desc)])) 9 | 10 | 11 | > (report-cost 5 8) 12 | "between $5 and $8" 13 | > (report-cost "millions") 14 | "millions of dollars" 15 | 16 | 合约对这样的函数用case->构成组合,这种结合对多个函数合约是必要的: 17 | 18 | (provide (contract-out 19 | [report-cost 20 | (case-> 21 | (integer? integer? . -> . string?) 22 | (string? . -> . string?))])) 23 | 24 | 如你所见,report-cost合约合并了两个函数合约,这与解释其函数所需的子句一样多。 25 | -------------------------------------------------------------------------------- /07.3.7 状态变化的检查: -------------------------------------------------------------------------------- 1 | 7.3.7 状态变化的检查 2 | 3 | ->i合约的组合也可以确保函数按照一定的约束只修改状态。例如,考虑这个合约(它是从框架中函数preferences:add-panel首选项中添加的略微简化的版本): 4 | 5 | (->i ([parent (is-a?/c area-container-window<%>)]) 6 | [_ (parent) 7 | (let ([old-children (send parent get-children)]) 8 | (λ (child) 9 | (andmap eq? 10 | (append old-children (list child)) 11 | (send parent get-children))))]) 12 | 13 | 它表示函数接受单个参数,命名为parent并且parent必须是匹配接口area-container-window<%>。 14 | 15 | 范围合约确保函数只通过在列表前面添加一个新的子代来修改parent的子类。这是通过使用_代替正常的标识符,它告诉合约库的范围合约并不依赖于任何结果的值,因此合约计算表达式后,_库在函数被调用时,而不是返回时。因此,调用get-children方法之前发生在合约下的函数调用。当合约下的函数返回时,它的结果作为child传递,并且合约确保函数返回后的孩子与函数调用之前的孩子相同,但是在列表前面还有一个孩子。 16 | 17 | 要看一个集中在这一点上的无实用价值的示例的差异,请考虑这个程序: 18 | 19 | #lang racket 20 | (define x '()) 21 | (define (get-x) x) 22 | (define (f) (set! x (cons 'f x))) 23 | (provide 24 | (contract-out 25 | [f (->i () [_ (begin (set! x (cons 'ctc x)) any/c)])] 26 | [get-x (-> (listof symbol?))])) 27 | 28 | 如果你需要这个模块,调用f,然后get-x结果会'(f ctc)。相反,如果f的合同是 29 | 30 | (->i () [res (begin (set! x (cons 'ctc x)) any/c)]) 31 | 32 | (只改变下划线res),然后get-x结果会是'(ctc f)。 33 | -------------------------------------------------------------------------------- /07.3.8 多个结果值: -------------------------------------------------------------------------------- 1 | 7.3.8 多个结果值 2 | 3 | 函数split接受char列表和传递所发生的 #\newline的第一次出现在字符串(如果有)和其余的列表: 4 | 5 | (define (split l) 6 | (define (split l w) 7 | (cond 8 | [(null? l) (values (list->string (reverse w)) '())] 9 | [(char=? #\newline (car l)) 10 | (values (list->string (reverse w)) (cdr l))] 11 | [else (split (cdr l) (cons (car l) w))])) 12 | (split l '())) 13 | 14 | 它是一个典型的多值函数,通过遍历单个列表返回两个值。 15 | 16 | 这种函数的合约可以使用普通函数箭头->,那么当它作为最后结果出现时,->特别地处理values: 17 | 18 | (provide (contract-out 19 | [split (-> (listof char?) 20 | (values string? (listof char?)))])) 21 | 22 | 这种函数的合约也可以使用->*: 23 | 24 | (provide (contract-out 25 | [split (->* ((listof char?)) 26 | () 27 | (values string? (listof char?)))])) 28 | 29 | 30 | 31 | 和以前一样,与->*参数的合约被封装在一对额外的圆括号中(并且必须总是这样包装),而空的括号表示没有可选参数。结果的合约是内部的values:字符串和字符列表。 32 | 33 | 现在,假设我们还希望确保第一个结果split是给定列表格式中给定单词的前缀。在这种情况下,我们需要使用->i合同的组合: 34 | 35 | (define (substring-of? s) 36 | (flat-named-contract 37 | (format "substring of ~s" s) 38 | (lambda (s2) 39 | (and (string? s2) 40 | (<= (string-length s2) (string-length s)) 41 | (equal? (substring s 0 (string-length s2)) s2))))) 42 | 43 | (provide 44 | (contract-out 45 | [split (->i ([fl (listof char?)]) 46 | (values [s (fl) (substring-of? (list->string fl))] 47 | [c (listof char?)]))])) 48 | 49 | 像->*、->i组合使用函数中的参数来创建范围的合约。是的,它不只是返回一个合约,而是函数产生值的数量:每个值的一个合约。在这种情况下,第二个合约和以前一样,确保第二个结果是char列表。与此相反,第一个合约增强旧的,因此结果是给定单词的前缀。 50 | 51 | 当然,这个合约检查是很值得的。这里有一个稍微廉价一点的版本: 52 | 53 | (provide 54 | (contract-out 55 | [split (->i ([fl (listof char?)]) 56 | (values [s (fl) (string-len/c (length fl))] 57 | [c (listof char?)]))])) 58 | -------------------------------------------------------------------------------- /07.3.9 固定但静态未知数量的参数: -------------------------------------------------------------------------------- 1 | 7.3.9 固定但静态未知数量的参数 2 | 3 | 想象一下你自己为一个函数写了一个合约,这个函数接受了另一个函数和一个字列表,最终数值前者应用于后者。如果给定的函数的数量匹配给定列表的长度,你的程序就有困难。 4 | 5 | 考虑这个n-step函数: 6 | 7 | ; (number ... -> (union #f number?)) (listof number) -> void 8 | (define (n-step proc inits) 9 | (let ([inc (apply proc inits)]) 10 | (when inc 11 | (n-step proc (map (λ (x) (+ x inc)) inits))))) 12 | 13 | n-step参数是proc,proc函数的结果要么是数字要么是假,或者一个列表。然后应用proc到inits列表中。只要proc返回一个数值,n-step对待数值为每个在其数字inits和递归的增量值。当proc返回false时,循环停止。 14 | 15 | 这里有两个应用: 16 | 17 | ; nat -> nat 18 | (define (f x) 19 | (printf "~s\n" x) 20 | (if (= x 0) #f -1)) 21 | (n-step f '(2)) 22 | 23 | ; nat nat -> nat 24 | (define (g x y) 25 | (define z (+ x y)) 26 | (printf "~s\n" (list x y z)) 27 | (if (= z 0) #f -1)) 28 | 29 | (n-step g '(1 1)) 30 | 31 | 一个n-step的合约必须指定aspects的两方面行为:其数量必须在inits里包括元素的数量,它必须返回一个数值或#f。后者是容易的,前者是困难的。乍一看,这似乎表明合约分配variable-arity给了proc: 32 | 33 | (->* () 34 | #:rest (listof any/c) 35 | (or/c number? false/c)) 36 | 37 | 然而,这个合约说函数必须接受任意数量的参数,而不是特定的但不确定的数值。因此,应用n-step到(lambda (x) x) and (list 1) 违约,因为给定的函数只接受一个参数。 38 | 39 | 正确的合约采用unconstrained-domain->组合,其仅指定函数的范围,而不是它的域。可以将本合同的数量测试指定正确的合约结合n-step: 40 | 41 | (provide 42 | (contract-out 43 | [n-step 44 | (->i ([proc (inits) 45 | (and/c (unconstrained-domain-> 46 | (or/c false/c number?)) 47 | (λ (f) (procedure-arity-includes? 48 | f 49 | (length inits))))] 50 | [inits (listof number?)]) 51 | () 52 | any)])) 53 | -------------------------------------------------------------------------------- /07.5 结构的合约: -------------------------------------------------------------------------------- 1 | 7.5 结构的合约 2 | 3 | 模块以两种方式处理结构。首先它们导出struct的定义,即创造一种明确方法的结构的能力,存取它们的字段,修改它们,并使这类结构和领域内的每一种值有区别。其次,有时模块导出特定的结构,并希望它的字段包含某种类型的值。本节说明如何使用合约保护结构。 4 | -------------------------------------------------------------------------------- /07.5.1 对特定值的确保: -------------------------------------------------------------------------------- 1 | 7.5.1 对特定值的确保 2 | 3 | 如果你的模块定义了一个变量为一个结构,那么你可以使用struct/c指定结构的形态: 4 | 5 | #lang racket 6 | (require lang/posn) 7 | 8 | (define origin (make-posn 0 0)) 9 | 10 | (provide (contract-out 11 | [origin (struct/c posn zero? zero?)])) 12 | 13 | 在这个例子中,该模块导入一个代表位置的库,它导出了一个posn结构。其中的posns创建并导出所代表的网格起点,即(0,0)。 14 | -------------------------------------------------------------------------------- /07.5.2 对所有值的确保: -------------------------------------------------------------------------------- 1 | 7.5.2 对所有值的确保 2 | 3 | 这《如何设计程序》(How to Design Programs)本书教授了posn应该只包含在它们两个字段里的数值。有了合约,我们将执行以下非正式数据定义: 4 | 5 | #lang racket 6 | (struct posn (x y)) 7 | 8 | (provide (contract-out 9 | [struct posn ((x number?) (y number?))] 10 | [p-okay posn?] 11 | [p-sick posn?])) 12 | 13 | (define p-okay (posn 10 20)) 14 | (define p-sick (posn 'a 'b)) 15 | 16 | 这个模块导出整个结构的定义:posn、posn?、posn-x、posn-y、set-posn-x!和set-posn-y!。 每个函数强制执行或承诺posn结构的这两个字段是数值——当这些值在模块范围内传递时。因此,如果一个客户机对10和'a调用posn,合约系统就发出违反合约的信号。 17 | 18 | 然而,posn模块内的p-sick的创建,并不违反合约。posn函数是在内部使用,所以'a和'b不跨约模块范围。同样,当p-sick跨越posn的范围时,合约承诺了posn?,别的什么也没有。特别是,p-sick的字段数是数值这个检查完全不需要。 19 | 20 | 对模块范围的合约检查意味着p-okay和p-sick从客户机的角度看起来相似,直到客户机引用以下片断: 21 | 22 | #lang racket 23 | (require lang/posn) 24 | 25 | ... (posn-x p-sick) ... 26 | 27 | 使用posn-x是客户机可以找到一个posn包含x字段的唯一途径。对posn-x应用程序发送p-sick回传给posn模块并且结果值——'a这里——回传给客户机,再跨越模块范围。在这一点上,合约系统发现承诺被打破了。具体来说,posn-x没有返回一个数值但却返回了一个符号,因此应归咎于它。 28 | 29 | 这个具体的例子表明,对违背合约的解释并不总是指明错误的来源。好消息是,错误位于posn模块。坏消息是这种解释是误导性的。虽然这是真的,posn-x产生一个符号而不是一个数值,它是程序员的责任,他从符号创建了posn,即程序员添加了以下内容 30 | 31 | (define p-sick (posn 'a 'b)) 32 | 33 | 到模块中。所以,当你在寻找基于违反合同的bug时,记住这个例子。 34 | 35 | 如果我们想修复p-sick的合约这样的错误,它是当sick被导出时被引发的,一个单独的改变就足够了: 36 | 37 | (provide 38 | (contract-out 39 | ... 40 | [p-sick (struct/c posn number? number?)])) 41 | 42 | 更确切地说,代替作为一个普通的posn?导出p-sick的方式,我们使用struct/c合约对组件进行强制约束。 43 | -------------------------------------------------------------------------------- /07.6 用#:exists和#:∃抽象合约: -------------------------------------------------------------------------------- 1 | 7.6 用#:exists和#:∃抽象合约 2 | 3 | 合约系统提供了可以保护抽象的存在性合约,确保模块的客户机不能依赖于精确表达选择,以便你有利于你的数据结构。 4 | 5 | contract-out表允许你写作: 6 | 7 | #:∃ name-of-a-new-contract 8 | 9 | 作为其从句之一。这个声明介绍变量name-of-a-new-contract,绑定到一个新的合约,隐藏关于它保护的值的信息。 10 | 11 | 作为一个例子,考虑这个(简单)一个队列数据结构的实现: 12 | 13 | #lang racket 14 | (define empty '()) 15 | (define (enq top queue) (append queue (list top))) 16 | (define (next queue) (car queue)) 17 | (define (deq queue) (cdr queue)) 18 | (define (empty? queue) (null? queue)) 19 | 20 | (provide 21 | (contract-out 22 | [empty (listof integer?)] 23 | [enq (-> integer? (listof integer?) (listof integer?))] 24 | [next (-> (listof integer?) integer?)] 25 | [deq (-> (listof integer?) (listof integer?))] 26 | [empty? (-> (listof integer?) boolean?)])) 27 | 28 | 本代码实现了一个单纯的列表成员队列,这意味着数据结构的客户机可能对数据结构直接使用car和cdr(也许偶然地),从而在描述里的任何改变(用更有效的描述来说是支持分期常量时间入队和出队操作)可能会破坏客户机代码。 29 | 30 | 为确保队列的描述是抽象的,我们可以在contract-out表达式里使用#:∃,就像这样: 31 | 32 | (provide 33 | (contract-out 34 | #:∃ queue 35 | [empty queue] 36 | [enq (-> integer? queue queue)] 37 | [next (-> queue integer?)] 38 | [deq (-> queue queue)] 39 | [empty? (-> queue boolean?)])) 40 | 41 | 现在,如果数据结构的客户机尝试使用car和cdr,他们会收到一个错误,而不是去摆弄的队列的内部成员。 42 | 43 | 也可以参见《合约和判断的生存期》(Exists Contracts and Predicates)。 44 | -------------------------------------------------------------------------------- /07.7 额外的例子: -------------------------------------------------------------------------------- 1 | 7.7 额外的例子 2 | 3 | 本节说明了Racket合约执行的当前状态,与一系列来自于《合约设计》(Design by Contract)的例子。 4 | 5 | 由米切尔(Mitchell)和麦金(McKim)的合约DbC设计原则源于1970年代风格的代数规范。DbC的总体目标是指定一个在观察成员里的代数的构造者。当我们将米切尔和麦金的术语用形式表达,我们用最适用的方法,我们保留他们的术语“类”(classes)和“对象”(objects): 6 | 7 | 1、从命令中分离查询。 8 | 9 | 查询返回结果,但不会改变对象的可观察属性。命令更改对象的可见属性,但不返回结果。在应用程序实现中,命令通常返回同一个类的新对象。 10 | 11 | 2、从派生查询中分离基本查询。 12 | 13 | 派生查询返回一个可根据基本查询计算的结果。 14 | 15 | 3、对于每个派生查询,编写一个后置条件契约,该契约根据基本查询指定结果。 16 | 17 | 4、对于每个命令,编写一个后置条件合约,指定基本查询中对可见属性的更改。 18 | 19 | 5、对于每个查询和命令,决定一个合适的条件合约。 20 | 21 | 以下各部分对应于米切尔和麦金的书中的一章(但不是所有章节显示在这里)。我们建议您先阅读合约(在第一个模块的结尾),然后是实现(在第一个模块中),然后是测试模块(在每个部分的结尾)。 22 | 23 | 米切尔和麦金利用Eiffel语言作为底层的编程语言,采用传统的命令式编程风格。我们的长期目标是将他们的例子变成Racket的程序,Racket面向结构及Racket的类系统势在必行。 24 | 25 | 注:模仿米切尔和麦金的非正式的概念parametericity(参数化多态性),我们用一流的合约。在一些地方,使用一流的合约提高了对米切尔和麦金的设计(见注释接口)。 26 | -------------------------------------------------------------------------------- /07.9 陷阱: -------------------------------------------------------------------------------- 1 | 7.9 陷阱 2 | -------------------------------------------------------------------------------- /07.9.1 合约和eq?: -------------------------------------------------------------------------------- 1 | 7.9.1 合约和eq? 2 | 3 | 一般来说,在程序中添加一个合约既应该使程序的行为保持不变,也应该表明违反合约的行为。这几乎是真正的Racket合约,只有一个例外:eq?。 4 | 5 | eq?程序的设计是快速的,并且没有提供太多的确保方式,除非它返回true,这意味着这两个值在所有方面都是相同的。在内部,这是作为一个低级的指针相等实现的,因此它公开了如何实现Racket的信息(以及如何实现合约)。 6 | 7 | 用eq?合约交互是糟糕的,因为函数合约检查是在内部作为包装函数实现的。例如,考虑这个模块: 8 | 9 | #lang racket 10 | 11 | (define (make-adder x) 12 | (if (= 1 x) 13 | add1 14 | (lambda (y) (+ x y)))) 15 | (provide (contract-out 16 | [make-adder (-> number? (-> number? number?))])) 17 | 18 | 它的导出make-adder函数,它是通常咖喱附加函数,除了当它的输入是1时,它返回Racket的add1。 19 | 20 | 你可能希望这样: 21 | 22 | (eq? (make-adder 1) 23 | (make-adder 1)) 24 | 25 | 应该返回#t,但却没有。如果合约被改成了any/c(或者甚至是(-> number? any/c)),那么eq?调用将返回#t。 26 | 27 | 教训:不要对有合约的值使用eq?。 28 | -------------------------------------------------------------------------------- /07.9.2 合约的范围和define|contract: -------------------------------------------------------------------------------- 1 | 7.9.2 合约的范围和define/contract 2 | 3 | 合约的范围是被define/contract所确定,它创建了一个嵌套的合约范围,有时不直观。当具有合约的多个函数或其它值相互作用时,情况尤其如此。例如,考虑这两个相互作用的函数: 4 | 5 | > (define/contract (f x) 6 | (-> integer? integer?) 7 | x) 8 | > (define/contract (g) 9 | (-> string?) 10 | (f "not an integer")) 11 | > (g) 12 | 13 | f: contract violation 14 | 15 | expected: integer? 16 | 17 | given: "not an integer" 18 | 19 | in: the 1st argument of 20 | 21 | (-> integer? integer?) 22 | 23 | contract from: (function f) 24 | 25 | blaming: top-level 26 | 27 | (assuming the contract is correct) 28 | 29 | at: eval:2.0 30 | 31 | 人们可能会认为,g可能被归咎于破坏与f的合约条件。然而,它们不是。相反,f和g之间的访问是通过封闭模块的顶层进行的。 32 | 33 | 更确切地说,f和模块的顶层有(-> integer? integer?)合约协调它们的相互作用,g和顶层有(-> string?)协调它们之间的相互作用,但是f和g之间没有直接的合约,这意味着g主体中的f的引用实际上是模块的顶层的职责,而不是g的。换句话说,函数f被赋予g,g与顶层之间没有约定,因此顶层应被归咎。 34 | 35 | 如果我们想增加g和顶层之间的合约,我们可以使用define/contract的#:freevar申明并看到预期的归咎: 36 | 37 | > (define/contract (f x) 38 | (-> integer? integer?) 39 | x) 40 | > (define/contract (g) 41 | (-> string?) 42 | #:freevar f (-> integer? integer?) 43 | (f "not an integer")) 44 | > (g) 45 | 46 | f: contract violation 47 | 48 | expected: integer? 49 | 50 | given: "not an integer" 51 | 52 | in: the 1st argument of 53 | 54 | (-> integer? integer?) 55 | 56 | contract from: top-level 57 | 58 | blaming: (function g) 59 | 60 | (assuming the contract is correct) 61 | 62 | at: eval:6.0 63 | 64 | 教训:如果两个值与合约应相互作用,把它们放在与模块范围内的合约不同的模块中或使用#:freevar。 65 | -------------------------------------------------------------------------------- /07.9.3 合约的生存期和判定: -------------------------------------------------------------------------------- 1 | 7.9.3 合约的生存期和判定 2 | 3 | 很像上面的eq?例子,#:∃合约可以改变一个程序的行为。 4 | 5 | 具体来说,对#:∃合约的null?判断(和许多其它判断)返回#f,并改变其中一个合同的any/c意味着null?现在可能反而返回#t,导致在任意不同的行为上依赖于这个布尔值,这可能在程序持续影响。 6 | 7 | #lang racket/exists package: base 8 | 9 | 解决上述问题,racket/exists库行为就像racket,但当提供#:∃合约时判断会发出错误信号。 10 | 11 | 教训:不要使用判断#:∃合约,但是如果你并不确定,用racket/exists在是安全的。 12 | -------------------------------------------------------------------------------- /07.9.4 定义递归合合约: -------------------------------------------------------------------------------- 1 | 7.9.4 定义递归合约 2 | 3 | 定义自参考合约时,很自然地去使用define。例如,人们可能试图在这样的流上写一份合约: 4 | 5 | > (define stream/c 6 | (promise/c 7 | (or/c null? 8 | (cons/c number? stream/c)))) 9 | 10 | stream/c: undefined; 11 | 12 | cannot reference undefined identifier 13 | 14 | 不幸的是,这不起作用,因为在定义之前需要stream/c的值。换句话说,所有的组合都渴望对它们的参数求值,即使它们不接受这些值。 15 | 16 | 相反,使用 17 | 18 | (define stream/c 19 | (promise/c 20 | (or/c 21 | null? 22 | (cons/c number? (recursive-contract stream/c))))) 23 | 24 | recursive-contract的使用延迟对标识符stream/c的求值,直到第一次检查完合约之后,足够长的时间才能确保stream/c被定义。 25 | 26 | 请参阅《检查数据结构的属性》(Checking Properties of Data Structures)。 27 | -------------------------------------------------------------------------------- /07.9.5 混合set!和合约: -------------------------------------------------------------------------------- 1 | 7.9.5 混合set!和合约 2 | 3 | 合约库假定变量通过contract-out导出没有被分配到,但没有强制执行。因此,如果您尝试set!这些变量,你可能会感到惊讶。考虑下面的例子: 4 | 5 | > (module server racket 6 | (define (inc-x!) (set! x (+ x 1))) 7 | (define x 0) 8 | (provide (contract-out [inc-x! (-> void?)] 9 | [x integer?]))) 10 | > (module client racket 11 | (require 'server) 12 | 13 | (define (print-latest) (printf "x is ~s\n" x)) 14 | 15 | (print-latest) 16 | (inc-x!) 17 | (print-latest)) 18 | 19 | > (require 'client) 20 | 21 | x is 0 22 | x is 0 23 | 24 | 两个调用print-latest打印0,即使x的值已经增加(并且在模块x内可见)。 25 | 26 | 为了解决这个问题,导出访问函数,而不是直接导出变量,像这样: 27 | 28 | #lang racket 29 | 30 | (define (get-x) x) 31 | (define (inc-x!) (set! x (+ x 1))) 32 | (define x 0) 33 | (provide (contract-out [inc-x! (-> void?)] 34 | [get-x (-> integer?)])) 35 | 36 | 教训:这是一个我们将在以后的版本中讨论的bug。 37 | -------------------------------------------------------------------------------- /08 输入和输出: -------------------------------------------------------------------------------- 1 | 8 输入和输出 2 | 3 | Racket端口对应Unix中流的概念(不要与racket/stream中的流混淆) 4 | 5 | 一个Racket端口代表一个数据源或数据池,例如一个文件、一个终端、一个TCP连接或者一个内存中字符串。端口提供顺序的访问,在那里数据能够被分批次地读或写,而不需要数据被一次性接受或生成。更具体地,一个输入端口(input port)代表一个程序能从中读取数据的源,一个输出端口(output port)代表一个程序能够向其中输出的数据池。 6 | 7 | 8.1 端口的种类 8 | 8.2 默认端口 9 | 8.3 读和写Racket数据 10 | 8.4 数据类型和序列化 11 | 8.5 字节、字符和编码 12 | 8.6 输入/输出模式 13 | -------------------------------------------------------------------------------- /08.2 默认端口: -------------------------------------------------------------------------------- 1 | 对于大多数简单IO函数,目标端口是一可选参数,默认值为当前的输入/输出端口。此外,错误信息被写入当前错误端口,这也是一个输出端口。current-input-port, current-output-port和current-error-port返回当前相关端口。 2 | 3 | 例子: 4 | > (display "Hi") 5 | Hi 6 | > (display "Hi" (current-output-port)) ; the same 7 | Hi 8 | 9 | 如果你通过终端打开racket程序,当前输入、输出、错误端口会连接至终端。更一般地,它们会连接到系统级的输入、输出、错误。在本指引中,例子将输出以紫色显示,错误信息以红色斜体显示。 10 | 11 | 例子: 12 | (define (swing-hammer) 13 | (display "Ouch!" (current-error-port))) 14 | 15 | > (swing-hammer) 16 | Ouch! 17 | 18 | 当前端口这类函数实际上是参数,代表它们的值能够通过parameterize设置。 19 | 20 | 查看 动态绑定:parameterize 一节以获取参数的介绍 21 | 22 | 例子: 23 | > (let ([s (open-output-string)]) 24 | (parameterize ([current-error-port s]) 25 | (swing-hammer) 26 | (swing-hammer) 27 | (swing-hammer)) 28 | (get-output-string s)) 29 | "Ouch!Ouch!Ouch!" 30 | -------------------------------------------------------------------------------- /08.3 读和写Racket数据: -------------------------------------------------------------------------------- 1 | 就像在内建数据类型中提到的,Racket提供三种方式打印内建值类型的实例: 2 | 3 | print, 以在REPL环境下的结果打印其值 4 | write, 以在输出上调用read反向产生打印值 5 | display, 缩小待输出值,至少对以字符/字节为主的数据类型,仅保留字符/字节部分,否则行为等同于write 6 | 7 | 这里有一些例子: 8 | > (print 1/2) 9 | 1/2 10 | > (print #\x) 11 | #\x 12 | > (print "hello") 13 | "hello" 14 | > (print #"goodbye") 15 | #"goodbye" 16 | > (print '|pea pod|) 17 | '|pea pod| 18 | > (print '("i" pod)) 19 | '("i" pod) 20 | > (print write) 21 | # 22 | 23 | > (write 1/2) 24 | 1/2 25 | > (write #\x) 26 | #\x 27 | > (write "hello") 28 | "hello" 29 | > (write #"goodbye") 30 | #"goodbye" 31 | > (write '|pea pod|) 32 | |pea pod| 33 | > (write '("i" pod)) 34 | ("i" pod) 35 | > (write write) 36 | # 37 | 38 | > (display 1/2) 39 | 1/2 40 | > (display #\x) 41 | x 42 | > (display "hello") 43 | hello 44 | > (display #"goodbye") 45 | goodbye 46 | > (display '|pea pod|) 47 | pea pod 48 | > (display '("i" pod)) 49 | (i pod) 50 | > (display write) 51 | # 52 | 53 | 总的来说,print对应Racket语法的表达层,write对应阅读层,display大致对应字符层。 54 | 55 | printf支持数据与文本的简单格式。在printf支持的格式字符串中,~a display下一个参数,~s write下一个参数,而~v print下一个参数。 56 | 57 | 例子: 58 | (define (deliver who when what) 59 | (printf "Items ~a for shopper ~s: ~v" who when what)) 60 | 61 | > (deliver '("list") '("John") '("milk")) 62 | Items (list) for shopper ("John"): '("milk") 63 | 64 | 使用write后,与display和read不同的是,许多类型的数据可以经由read重新读入。相同类型经print处理的值也能够被read解析,但是结果包含额外的引号形式,因为经print后的形式会被以表达式形式读入。 65 | 66 | 例子: 67 | > (define-values (in out) (make-pipe)) 68 | > (write "hello" out) 69 | > (read in) 70 | "hello" 71 | > (write '("alphabet" soup) out) 72 | > (read in) 73 | '("alphabet" soup) 74 | > (write #hash((a . "apple") (b . "banana")) out) 75 | > (read in) 76 | '#hash((a . "apple") (b . "banana")) 77 | > (print '("alphabet" soup) out) 78 | > (read in) 79 | ''("alphabet" soup) 80 | > (display '("alphabet" soup) out) 81 | > (read in) 82 | '(alphabet soup) 83 | -------------------------------------------------------------------------------- /08.4 字符类型和序列化: -------------------------------------------------------------------------------- 1 | prefab类型(查看预置结构类型章节)自动支持序列化:它们可被写入输出流,其副本可被由输入流读入: 2 | > (define-values (in out) (make-pipe)) 3 | > (write #s(sprout bean) out) 4 | > (read in) 5 | '#s(sprout bean) 6 | 7 | 使用struct创建的其它结构类型,提供较prefab类型更多的抽象,通常使用#<....>记号(对于不透明结构类型)或使用#(....)矢量记号(对于透明结构类型)作为输出。两种的输出结果都不能以结构类型反向读入。 8 | > (struct posn (x y)) 9 | > (write (posn 1 2)) 10 | # 11 | > (define-values (in out) (make-pipe)) 12 | > (write (posn 1 2) out) 13 | > (read in) 14 | UNKNOWN::0: read: bad syntax `#<' 15 | > (struct posn (x y) #:transparent) 16 | > (write (posn 1 2)) 17 | #(struct:posn 1 2) 18 | > (define-values (in out) (make-pipe)) 19 | > (write (posn 1 2) out) 20 | > (define v (read in)) 21 | > v 22 | '#(struct:posn 1 2) 23 | > (posn? v) 24 | #f 25 | > (vector? v) 26 | #t 27 | 28 | serializable-struct定义了能够被序列化为值,可供write/read写入或存储的类型。序列化的结果可被反序列化为原始结构类的实例。序列化形式与函数经由racket/serialize提供。 29 | 30 | 例子: 31 | > (require racket/serialize) 32 | > (serializable-struct posn (x y) #:transparent) 33 | > (deserialize (serialize (posn 1 2))) 34 | (posn 1 2) 35 | > (write (serialize (posn 1 2))) 36 | ((3) 1 ((#f . deserialize-info:posn-v0)) 0 () () (0 1 2)) 37 | > (define-values (in out) (make-pipe)) 38 | > (write (serialize (posn 1 2)) out) 39 | > (deserialize (read in)) 40 | (posn 1 2) 41 | 42 | 除了struct绑定的名字外,serializable-struct绑定具有反序列化信息的标识符,会自动由模块上下文提供反序列化标识符。当值被反序列化时,反序列化标识符会经由反射被访问。 43 | -------------------------------------------------------------------------------- /08.5 字节、字符和编码: -------------------------------------------------------------------------------- 1 | 类似read-line, read, display, write这样的函数的工作以字符为单位(对应Unicode标量值)。概念上来说,它们经由read-char与write-char实现。 2 | 3 | 更初级一点,端口读写字节而非字符。read-byte与write-byte读写原始字节。其它函数,例如read-bytes-line,建立在字节操作而非字符操作。 4 | 5 | 事实上,read-char和write-char函数概念上由read-byte和write-byte实现。当一个字节的值小于128,它将对应于一个ASCII字符。任何其它的字节会被视为UTF-8序列的一部分,而UTF-8则是字节形式编码Unicode标量值的标准方式之一(具有将ASCII字符原样映射的优点)。此外,一个单次的read-char可能会调用多次read-byte,一个标准的write-char可能生成多个输出字节。 6 | 7 | read-char和write-char操作总使用UTF-8编码。如果你有不同编码的文本流,或想以其它编码生成文本流,使用reencode-input-port或reencode-output-port。reencode-input-port将一种你指定编码的输入流转换为UTF-8流;以这种方式,read-char能够察觉UTF-8编码,即使原始编码并非如此。应当注意,read-byte也看到重编码后的数据,而非原始字节流。 8 | -------------------------------------------------------------------------------- /08.6 IO模式: -------------------------------------------------------------------------------- 1 | 如果你想处理文件中独立的各行,你可以使用in-lines: 2 | 3 | > (define (upcase-all in) 4 | (for ([l (in-lines in)]) 5 | (display (string-upcase l)) 6 | (newline))) 7 | > (upcase-all (open-input-string 8 | (string-append 9 | "Hello, World!\n" 10 | "Can you hear me, now?"))) 11 | HELLO, WORLD! 12 | CAN YOU HEAR ME, NOW? 13 | 14 | 如果你想确定“hello”是否在文件中存在,你可以搜索独立各行,但是更简便的方法是对流应用一正则表达式(查看正则表达式章节): 15 | 16 | > (define (has-hello? in) 17 | (regexp-match? #rx"hello" in)) 18 | > (has-hello? (open-input-string "hello")) 19 | #t 20 | > (has-hello? (open-input-string "goodbye")) 21 | #f 22 | 23 | 如果你想将一个端口拷贝至另一个,使用来自racket/port的copy-port,它能够在很多的数据可用时有效传输大的块,也能够在小的块全部就绪时立刻传输: 24 | 25 | > (define o (open-output-string)) 26 | > (copy-port (open-input-string "broom") o) 27 | > (get-output-string o) 28 | "broom" 29 | -------------------------------------------------------------------------------- /09 正则表达式: -------------------------------------------------------------------------------- 1 | 9 正则表达式 2 | 3 | 一个正则表达式(regexp)值封装一个模式,描述的是一个字符串或字节字符串。当你调用像regexp-match函数时,正则表达式匹配器尝试对另一个字符串或字节字符串(一部分)匹配这种模式,我们将其称为文本字符串(text string)。文本字符串被视为原始文本,而不是模式。 4 | 5 | 9.1 写regexp模式 6 | 9.2 匹配正则表达式模式 7 | 9.3 基本申明 8 | 9.4 字符和字符类 9 | 9.4.1 常用的字符类 10 | 9.4.2 POSIX字符类 11 | 9.5 量词 12 | 9.6 集群 13 | 9.6.1 反向引用 14 | 9.6.2 非捕获集群 15 | 9.6.3 回绕 16 | 9.7 交替 17 | 9.8 回溯 18 | 9.9 向前看和后面看 19 | 9.9.1 前瞻 20 | 9.9.2 追溯 21 | 9.10 一个扩展示例 22 | -------------------------------------------------------------------------------- /09.01 写regexp模式: -------------------------------------------------------------------------------- 1 | 9.1 写regexp模式 2 | 3 | 一个字符串或字节字符串可以直接用作一个正则表达式模式,也可以#rx形成字面上的正则表达式值。例如,#rx"abc"是一个基于正则表达式值的字符串,并且#rx#"abc"是一个基于正则表达式值的字节字符串。或者,一个字符串或字节字符串可以以#px做前缀,如在#px"abc"中一样,稍微扩展字符串中模式的语法。 4 | 5 | 在一个正则表达式模式的大多数角色都是相匹配的文本字符串中出现的自己。因此,该模式#rx"abc"匹配在演替中的一个字符串中包含的字符a、b和C。其它角色扮演的元字符(metacharacters)和字符序列作为元序列(metasequences)。也就是说,它们指定的东西不是字面上的自我。例如,在模式#rx"a.c"中,字符a和C代表它们自己,但元字符.可以匹配任何字符。因此,该模式#rx"a.c"在演替里匹配一个a,任意字符,和C。 6 | 7 | 如果我们需要匹配字符.本身,我们可以在它前面加上一个\。字符序列\.结果就是一个元序列,因为它不匹配它本身而只是.。所以,在演替里匹配a、.和C,我们使用正则表达式模式#rx"a\\.c"。C”;双\字符是一个Racket字符串神器,它不是正则表达式模式自己的。 8 | 9 | 正则表达式函数接受一个字符串或字节字符串并产生一个正则表达式的值。当你使用正则表达式构建模式以匹配多个字符串,因为一个模式在它可以被使用在一个匹配之前被编译成了一个正则表达式的值。这个pregexp函数就像regexp,但使用扩展语法。正则表达式值做为#rx或#px的字面形式,被编译一次,尽管当它们可读时。 10 | 11 | regexp-quote函数接受任意的字符串并返回一个模式匹配原始字符串。特别是,在输入字符串中的字符,可以作为正则表达式元字符用一个反斜杠转义,所以只有它们自己使他们安全地匹配。 12 | 13 | > (regexp-quote "cons") 14 | 15 | "cons" 16 | > (regexp-quote "list?") 17 | 18 | "list\\?" 19 | 20 | regexp-quote函数在从一个混合的正则表达式字符串和字面的字符串构建一个完整的正则表达式是有用的。 21 | -------------------------------------------------------------------------------- /09.10 一个扩展示例: -------------------------------------------------------------------------------- 1 | 9.10 一个扩展示例 2 | 3 | 这是一个从《Friedl’s Mastering Regular Expressions》(189页)来的扩展的例子,涵盖了本章中介绍的许多特征。问题是要修饰一个正则表达式,它将匹配任何且唯一的IP地址或点缀四周:四个数字被三个点分开,每个数字在0和255之间。 4 | 5 | 首先,我们定义了一个子正则表达式n0-255以匹配0到255: 6 | 7 | > (define n0-255 8 | (string-append 9 | "(?:" 10 | "\\d|" ; 0 到 9 11 | "\\d\\d|" ; 00 到 99 12 | "[01]\\d\\d|" ; 000 到 199 13 | "2[0-4]\\d|" ; 200 到 249 14 | "25[0-5]" ; 250 到 255 15 | ")")) 16 | 17 | 前两个替代只得到所有的一位数和两位数。因为0-padding是允许的,我们要匹配1和01。我们当得到3位数字时要小心,因为数字255以上必须排除。因此,我们的修饰替代,得到000至199,然后200至249,最后250至255。 18 | 19 | IP地址是一个字符串,包括四个n0-255用三个点分隔。 20 | 21 | > (define ip-re1 22 | (string-append 23 | "^" ; 前面什么都没有 24 | n0-255 ; 第一个n0-255, 25 | "(?:" ; 接着是子模式 26 | "\\." ; 一个点跟着 27 | n0-255 ; 一个n0-255, 28 | ")" ; 29 | "{3}" ; 恰好重复三遍 30 | "$")) 31 | ;后边什么也没有 32 | 33 | 让我们试试看: 34 | 35 | > (regexp-match (pregexp ip-re1) "1.2.3.4") 36 | 37 | '("1.2.3.4") 38 | > (regexp-match (pregexp ip-re1) "55.155.255.265") 39 | 40 | #f 41 | 42 | 这很好,除此之外我们还有 43 | 44 | > (regexp-match (pregexp ip-re1) "0.00.000.00") 45 | 46 | '("0.00.000.00") 47 | 48 | 所有的零序列都不是有效的IP地址!用向前查找救援。在开始匹配ip-re1之前,我们向前查找以确保我们没有零。我们可以用正向前查找去确保有非零的数字。 49 | 50 | > (define ip-re 51 | (pregexp 52 | (string-append 53 | "(?=.*[1-9])" ; 确保这里是一个非0数 54 | ip-re1))) 55 | 56 | 或者我们可以用负前查找确保前面不是只由零和点组成。 57 | 58 | > (define ip-re 59 | (pregexp 60 | (string-append 61 | "(?![0.]*$)" ; 不仅是0和点 62 | ; (注意: .在[...]里面不是匹配器) 63 | ip-re1))) 64 | 65 | 正则表达式ip-re会匹配所有的并且唯一的IP地址。 66 | 67 | > (regexp-match ip-re "1.2.3.4") 68 | 69 | '("1.2.3.4") 70 | > (regexp-match ip-re "0.0.0.0") 71 | 72 | #f 73 | -------------------------------------------------------------------------------- /09.3 基本申明: -------------------------------------------------------------------------------- 1 | 9.3 基本申明 2 | 3 | 论断(assertions)^和$分别标识文本字符串的开头和结尾,它们确保对它们临近的一个或其它文本字符串的结束正则表达式匹配: 4 | 5 | > (regexp-match-positions #rx"^contact" "first contact") 6 | 7 | #f 8 | 9 | 以上正则表达式匹配失败是因为contact没有出现在文本字符串的开始。在 10 | 11 | > (regexp-match-positions #rx"laugh$" "laugh laugh laugh laugh") 12 | 13 | '((18 . 23)) 14 | 15 | 中,正则表达式匹配的最后的laugh。 16 | 17 | 元序列\b坚称一个字的范围存在,但这元序列只能与#px语法一起工作。在 18 | 19 | > (regexp-match-positions #px"yack\\b" "yackety yack") 20 | 21 | '((8 . 12)) 22 | 23 | 在yackety的yack不在字边界结束,所以不匹配。第二yack在字边界结束,所以匹配。 24 | 25 | 元序列\B(也只有#px)对\b有相反的影响;它断言字边界不存在。在 26 | 27 | > (regexp-match-positions #px"an\\B" "an analysis") 28 | 29 | '((3 . 5)) 30 | 31 | an不在单词边界结束,是匹配的。 32 | -------------------------------------------------------------------------------- /09.4 字符和字符类: -------------------------------------------------------------------------------- 1 | 9.4 字符和字符类 2 | 3 | 通常,在正则表达式中的字符匹配相同文本字符串中的字符。有时使用正则表达式元序列引用单个字符是有必要的或方便的。例如,元序列\.匹配句点字符。 4 | 5 | 元字符.匹配任意字符(除了在多行模式中换行,参见《》(Cloisters)): 6 | 7 | > (regexp-match #rx"p.t" "pet") 8 | 9 | '("pet") 10 | 11 | 上面的模式也匹配pat、pit、pot、put和p8t,但不匹配peat或pfffft。 12 | 13 | 字符类(character class)匹配来自于一组字符中的任何一个字符。一个典型的格式,这是括号字符类(bracketed character class)[...],它匹配任何一个来自包含在括号内的非空序列的字符。因此,#rx"p[aeiou]t"匹配pat、pet、pit、pot、put,别的都不匹配。 14 | 15 | 在括号内,一个-介于两个字符之间指定字符之间的Unicode范围。例如,#rx"ta[b-dgn-p]"匹配tab、tac、ad、tag、tan、tao和tap。 16 | 17 | 在左括号后一个初始的^将通过剩下的内容反转指定的集合;也就是说,它指定识别在括号内字符集以外的字符集。例如,#rx"do[^g]"匹配所有以do开始但不是dog的三字符序列。 18 | 19 | 注意括号内的元字符^,它在括号里边的意义与在外边的意义截然不同。大多数其它的元字符(.、*、+、?等等)当在括号内的时候不再是元字符,即使你一直不予承认以求得内心平静。一个-是一个元字符,仅当它在括号内并且当它既不是括号之间的第一个字符也不是最后一个字符时。 20 | 21 | 括号内的字符类不能包含其它括号字符类(虽然它们包含字符类的某些其它类型,见下)。因此,在一个括号内的字符类里的一个[不必是一个元字符;它可以代表自身。比如,#rx"[a[b]"匹配a、[和b。 22 | 23 | 此外,由于空括号字符类是不允许的,一个]在开左括号后立即出现也不比是一个元字符。比如,#rx"[]ab]"匹配]、a和b。 24 | -------------------------------------------------------------------------------- /09.4.1 常用的字符类: -------------------------------------------------------------------------------- 1 | 9.4.1 常用的字符类 2 | 3 | 在#px语法里,一些标准的字符类可以方便地表示为元序列以代替明确的括号内的表达式:\d匹配一个数字(与[0-9]同样);\s匹配一个ASCII空白字符;而\w匹配一个可以是“字(word)”的一部分的字符。 4 | 5 | 这些元序列的大写版本代表相应的字符类的反转:\D匹配一个非数字,\S匹配一个非空格字符,而\W匹配一个非“字”字符。 6 | 7 | 在把这些元序列放进一个Racket字符串里时,记得要包含一个双反斜杠: 8 | 9 | > (regexp-match #px"\\d\\d" 10 | "0 dear, 1 have 2 read catch 22 before 9") 11 | 12 | '("22") 13 | 14 | 这些字符类可以在括号表达式中使用。比如,#px"[a-z\\d]"匹配一个小写字母或数字。 15 | -------------------------------------------------------------------------------- /09.4.2 POSIX字符类: -------------------------------------------------------------------------------- 1 | 9.4.2 POSIX字符类 2 | 3 | POSIX(可移植性操作系统接口)字符类是一种特殊形式的形如[:...:]的元序列,它只能用在#px语法中的一个括号表达式内。POSIX类支持 4 | 5 | [:alnum:] — ASCII字母和数字 6 | 7 | [:alpha:] — ASCII字母 8 | 9 | [:ascii:] — ASCII字符 10 | 11 | [:blank:] — ASCII等宽的空格:空格和tab 12 | 13 | [:cntrl:] — “控制”字符:ASCII 0到32 14 | 15 | [:digit:] — ASCII数字,像\d 16 | 17 | [:graph:] — ASCII图形字符 18 | 19 | [:lower:] — ASCII小写字母 20 | 21 | [:print:] — ASCII图形字符加等宽空白 22 | 23 | [:space:] — ASCII空白,像\s 24 | 25 | [:upper:] — ASCII大写字母 26 | 27 | [:word:] — ASCII字母和_,像\w 28 | 29 | [:xdigit:] — ASCII十六进制数字 30 | 31 | 例如,在#px"[[:alpha:]_]"匹配一个字母或下划线。 32 | 33 | > (regexp-match #px"[[:alpha:]_]" "--x--") 34 | 35 | '("x") 36 | > (regexp-match #px"[[:alpha:]_]" "--_--") 37 | 38 | '("_") 39 | > (regexp-match #px"[[:alpha:]_]" "--:--") 40 | 41 | #f 42 | 43 | POSIX类符号只适用于在括号表达式内。例如,[:alpha:],当不在括号表达式内时,不会被当做字母类读取。确切地说,它是(从以前的原则)包含字符:、a、l、p、h的字符类。 44 | 45 | > (regexp-match #px"[:alpha:]" "--a--") 46 | 47 | '("a") 48 | > (regexp-match #px"[:alpha:]" "--x--") 49 | 50 | #f 51 | -------------------------------------------------------------------------------- /09.5 量词: -------------------------------------------------------------------------------- 1 | 9.5 量词 2 | 3 | 量词(quantifier)*、+和?分别匹配:前面的子模式的零个或多个,一个或多个以及零个或一个实例。 4 | 5 | > (regexp-match-positions #rx"c[ad]*r" "cadaddadddr") 6 | 7 | '((0 . 11)) 8 | > (regexp-match-positions #rx"c[ad]*r" "cr") 9 | 10 | '((0 . 2)) 11 | > (regexp-match-positions #rx"c[ad]+r" "cadaddadddr") 12 | 13 | '((0 . 11)) 14 | > (regexp-match-positions #rx"c[ad]+r" "cr") 15 | 16 | #f 17 | > (regexp-match-positions #rx"c[ad]?r" "cadaddadddr") 18 | 19 | #f 20 | > (regexp-match-positions #rx"c[ad]?r" "cr") 21 | 22 | '((0 . 2)) 23 | > (regexp-match-positions #rx"c[ad]?r" "car") 24 | 25 | '((0 . 3)) 26 | 27 | 在#px语法里,你可以使用括号指定比*、+、?更精细的调整量: 28 | 29 | 1、量词{m}精确匹配前面子模式的m实例;m必须是一个非负整数。 30 | 31 | 2、量词{m,n}匹配至少m并至多n个实例。m和n是非负整数,m小于或等于n。你可以省略一个或两个都省略,在这种情况下m默认为0,n到无穷大。 32 | 33 | 很明显,+和?是{1,}和{0,1}的缩写,*是{,}的缩写,和{0,}一样。 34 | 35 | > (regexp-match #px"[aeiou]{3}" "vacuous") 36 | 37 | '("uou") 38 | > (regexp-match #px"[aeiou]{3}" "evolve") 39 | 40 | #f 41 | > (regexp-match #px"[aeiou]{2,3}" "evolve") 42 | 43 | #f 44 | > (regexp-match #px"[aeiou]{2,3}" "zeugma") 45 | 46 | '("eu") 47 | 48 | 迄今为止所描述的量词都是贪婪的:它们匹配最大的实例数,还会导致对整个模式的总体匹配。 49 | 50 | > (regexp-match #rx"<.*>" " ") 51 | 52 | '(" ") 53 | 54 | 为了使这些量词为非贪婪,给它们追加?。非贪婪量词匹配满足需要的最小实例数,以确保整体匹配。 55 | 56 | > (regexp-match #rx"<.*?>" " ") 57 | 58 | '("") 59 | 60 | 非贪婪量词分别为:*?、+?、??、{m}?、{m,n}?。注意匹配字符?的这两种使用。 61 | -------------------------------------------------------------------------------- /09.6 聚类: -------------------------------------------------------------------------------- 1 | 9.6 聚类 2 | 3 | 聚类(Cluster)——文内的括号(...)——确定封闭模式作为一个单一的实体。它使匹配去捕获匹配项,或字符串的一部分匹配子模式,除了整体匹配之外: 4 | 5 | > (regexp-match #rx"([a-z]+) ([0-9]+), ([0-9]+)" "jan 1, 1970") 6 | 7 | '("jan 1, 1970" "jan" "1" "1970") 8 | 9 | 聚类也导致以下量词对待整个封闭的模式作为一个实体: 10 | 11 | > (regexp-match #rx"(pu )*" "pu pu platter") 12 | 13 | '("pu pu " "pu ") 14 | 15 | 返回的匹配项数量总是等于指定的正则表达式子模式的数量,即使一个特定的子模式恰好匹配多个子字符串或根本没有子串。 16 | 17 | > (regexp-match #rx"([a-z ]+;)*" "lather; rinse; repeat;") 18 | 19 | '("lather; rinse; repeat;" " repeat;") 20 | 21 | 在这里,*量化子模式匹配的三次,但这是返回的最后一个匹配项。 22 | 23 | 对一个量化的模式来说不匹配也是可能的,甚至是对整个模式匹配。在这种情况下,失败的匹配项通过#f体现。 24 | 25 | > (define date-re 26 | ; match ‘month year' or ‘month day, year'; 27 | ; subpattern matches day, if present 28 | #rx"([a-z]+) +([0-9]+,)? *([0-9]+)") 29 | > (regexp-match date-re "jan 1, 1970") 30 | 31 | '("jan 1, 1970" "jan" "1," "1970") 32 | > (regexp-match date-re "jan 1970") 33 | 34 | '("jan 1970" "jan" #f "1970") 35 | -------------------------------------------------------------------------------- /09.6.1 后向引用: -------------------------------------------------------------------------------- 1 | 9.6.1 后向引用 2 | 3 | 子匹配可用于插入字符串参数的regexp-replace和regexp-replace*程序。插入的字符串可以使用\n为后向引用(backreference)返回第n个匹配项,这是子字符串,它匹配第n个子模式。一个\0引用整个匹配,它也可以指定为\&。 4 | 5 | > (regexp-replace #rx"_(.+?)_" 6 | "the _nina_, the _pinta_, and the _santa maria_" 7 | "*\\1*") 8 | 9 | "the *nina*, the _pinta_, and the _santa maria_" 10 | > (regexp-replace* #rx"_(.+?)_" 11 | "the _nina_, the _pinta_, and the _santa maria_" 12 | "*\\1*") 13 | 14 | "the *nina*, the *pinta*, and the *santa maria*" 15 | > (regexp-replace #px"(\\S+) (\\S+) (\\S+)" 16 | "eat to live" 17 | "\\3 \\2 \\1") 18 | 19 | "live to eat" 20 | 21 | 使用\\在插入字符串指定转义符。同时,\$代表空字符串,并且对从紧随其后的数字分离后向引用\n是有用的。 22 | 23 | 反向引用也可以用在#px模式以返回模式中的一个已经匹配的子模式。\n代表第n个子匹配的精确重复。注意这个\0、在插入字符串是有用的,在regexp模式内没有道理,因为整个正则表达式不匹配而无法回到它。 24 | 25 | > (regexp-match #px"([a-z]+) and \\1" 26 | "billions and billions") 27 | 28 | '("billions and billions" "billions") 29 | 30 | 注意,后向引用不是简单地重复以前的子模式。而这是一个特别的被子模式所匹配的子串的重复 。 31 | 32 | 在上面的例子中,后向引用只能匹配billions。它不会匹配millions,即使子模式追溯到——([a-z]+)——这样做会没有问题: 33 | 34 | > (regexp-match #px"([a-z]+) and \\1" 35 | "billions and millions") 36 | 37 | #f 38 | 39 | 下面的示例标记数字字符串中所有立即重复的模式: 40 | 41 | > (regexp-replace* #px"(\\d+)\\1" 42 | "123340983242432420980980234" 43 | "{\\1,\\1}") 44 | 45 | "12{3,3}40983{24,24}3242{098,098}0234" 46 | 47 | 下面的示例修正了两个单词: 48 | 49 | > (regexp-replace* #px"\\b(\\S+) \\1\\b" 50 | (string-append "now is the the time for all good men to " 51 | "to come to the aid of of the party") 52 | "\\1") 53 | 54 | "now is the time for all good men to come to the aid of the party" 55 | -------------------------------------------------------------------------------- /09.6.2 非捕捉簇: -------------------------------------------------------------------------------- 1 | 9.6.2 非捕捉簇 2 | 3 | 它通常需要指定一个簇(通常为量化)但不触发子匹配项的信息捕捉。这种簇称为非捕捉(non-capturing)。要创建非簇,请使用(?:以代替(作为簇开放器。 4 | 5 | 在下面的例子中,一个非簇消除了“目录”部分的一个给定的UNIX路径名,并获取簇标识。 6 | 7 | > (regexp-match #rx"^(?:[a-z]*/)*([a-z]+)$" 8 | "/usr/local/bin/racket") 9 | 10 | '("/usr/local/bin/racket" "racket") 11 | -------------------------------------------------------------------------------- /09.6.3 回廊: -------------------------------------------------------------------------------- 1 | 9.6.3 回廊 2 | 3 | 一个非捕捉簇?和:之间的位置称为回廊(cloister)。你可以把修饰符放在这儿,有可能会使簇子模式被特别处理。这个修饰符i使子模式匹配时不区分大小写: 4 | 5 | > (regexp-match #rx"(?i:hearth)" "HeartH") 6 | 7 | '("HeartH") 8 | 9 | 修饰符m使子模式在多行模式匹配,在.的位置不匹配换行符,^仅在一个新行后可以匹配,而$仅在一个新行前可以匹配。 10 | 11 | > (regexp-match #rx"." "\na\n") 12 | 13 | '("\n") 14 | > (regexp-match #rx"(?m:.)" "\na\n") 15 | 16 | '("a") 17 | > (regexp-match #rx"^A plan$" "A man\nA plan\nA canal") 18 | 19 | #f 20 | > (regexp-match #rx"(?m:^A plan$)" "A man\nA plan\nA canal") 21 | 22 | '("A plan") 23 | 24 | 你可以在回廊里放置多个修饰符: 25 | 26 | > (regexp-match #rx"(?mi:^A Plan$)" "a man\na plan\na canal") 27 | 28 | '("a plan") 29 | 30 | 在修饰符前的一个减号反转它的意思。因此,你可以在子类中使用-i以翻转案例不由封闭簇造导致。 31 | 32 | > (regexp-match #rx"(?i:the (?-i:TeX)book)" 33 | "The TeXbook") 34 | 35 | '("The TeXbook") 36 | 37 | 上述正表达式将允许任何针对the和book的外壳,但它坚持认为Tex有不同的包装。 38 | -------------------------------------------------------------------------------- /09.7 替代: -------------------------------------------------------------------------------- 1 | 9.7 替代 2 | 3 | 你可以通过用|分隔它们来指定列表的替代子模式。在最近的封闭簇里|分隔子模式(或在整个模式字符串里,假如没有封闭括号)。 4 | 5 | > (regexp-match #rx"f(ee|i|o|um)" "a small, final fee") 6 | 7 | '("fi" "i") 8 | > (regexp-replace* #rx"([yi])s(e[sdr]?|ing|ation)" 9 | (string-append 10 | "analyse an energising organisation" 11 | " pulsing with noisy organisms") 12 | "\\1z\\2") 13 | 14 | "analyze an energizing organization pulsing with noisy organisms" 15 | 16 | 不过注意,如果你想使用簇仅仅是指定替代子模式列表,却不想指定匹配项,那么使用(?:代替(。 17 | 18 | > (regexp-match #rx"f(?:ee|i|o|um)" "fun for all") 19 | 20 | '("fo") 21 | 22 | 注意替代的一个重要事情是,最左匹配替代不管长短 。因此,如果一个替代是后一个替代的前缀,后者可能没有机会匹配。 23 | 24 | > (regexp-match #rx"call|call-with-current-continuation" 25 | "call-with-current-continuation") 26 | 27 | '("call") 28 | 29 | 为了让较长的替代在匹配中有一个镜头,把它放在较短的一个之前: 30 | 31 | > (regexp-match #rx"call-with-current-continuation|call" 32 | "call-with-current-continuation") 33 | 34 | '("call-with-current-continuation") 35 | 36 | 在任何情况下,对于整个正则表达式的整体匹配总是倾向于整体的不匹配。在下面这里,较长的替代仍然更好,因为它的较短的前缀不能产生整体匹配。 37 | 38 | > (regexp-match 39 | #rx"(?:call|call-with-current-continuation) constrained" 40 | "call-with-current-continuation constrained") 41 | 42 | '("call-with-current-continuation constrained") 43 | -------------------------------------------------------------------------------- /09.8 回溯: -------------------------------------------------------------------------------- 1 | 9.8 回溯 2 | 3 | 我们已经明白贪婪的量词匹配的次数最多,但压倒一切的优先级才是整体匹配的成功。考虑以下内容 4 | 5 | > (regexp-match #rx"a*a" "aaaa") 6 | 7 | '("aaaa") 8 | 9 | 这个正则表达式包括两个子正则表达式:a*,其次是a。子正则表达式a*不允许匹配文本字符串aaaa内的所有的四个a,即使*是一个贪婪量词也一样。它可能仅匹配前面的三个,剩下最后一个给第二子正则表达式。这确保了完整的正则表达式匹配成功。 10 | 11 | 正则表达式匹配器通过一个称为回溯(backtracking)的过程实现来这个。匹配器暂时允许贪婪量词匹配所有四个a,但当整体匹配处于岌岌可危的状态变得清晰时,它回溯(backtracks)到一个不那么贪婪的三个a的匹配。如果这也失败了,与以下调用一样 12 | 13 | > (regexp-match #rx"a*aa" "aaaa") 14 | 15 | '("aaaa") 16 | 17 | 匹配器回溯甚至更进一步。只有当所有可能的回溯尝试都没有成功时,整体失败才被承认。 18 | 19 | 回溯并不局限于贪婪量词。非贪婪量词匹配尽可能少的情况下,为了达到整体匹配,逐步回溯会有越来越多的实例。这里替代的回溯也更有向右替代的倾向,当局部成功的向左替代一旦失败则会产生一个整体的匹配。 20 | 21 | 有时禁用回溯是有效的。例如,我们可能希望作出选择,或者我们知道尝试替代是徒劳的。一个非回溯正则表达式在(?>...)里是封闭的。 22 | 23 | > (regexp-match #rx"(?>a+)." "aaaa") 24 | 25 | #f 26 | 27 | 在这个调用里,子正则表达式?>a+贪婪地匹配所有四个a,并且回溯的机会被拒绝。因此,整体匹配被拒绝。这个正则表达式的效果因此对一个或多个a的匹配被某些明确无a(non-a)的予以继承。 28 | -------------------------------------------------------------------------------- /09.9 前后查找: -------------------------------------------------------------------------------- 1 | 9.9 前后查找 2 | 3 | 在你的模式里你可以有判断,前后查找(look ahead or behind)确认子模式是否出现。这些“围绕查找”的判断通过设置子模式检查一个簇,它的主要字符是:?=(正向前查找),?!(负向前查找),?<=(正向后查找),? (regexp-match-positions #rx"grey(?=hound)" 6 | "i left my grey socks at the greyhound") 7 | 8 | '((28 . 32)) 9 | 10 | 正则表达式#rx"grey(?=hound)"匹配灰grey,但前提是它后面紧跟着hound。因此,文本字符串中的第一个grey不匹配。 11 | 12 | 用?!反向前查找窥探以提前确保其子模式不可能匹配。 13 | 14 | > (regexp-match-positions #rx"grey(?!hound)" 15 | "the gray greyhound ate the grey socks") 16 | 17 | '((27 . 31)) 18 | 19 | 正则表达式#rx"grey(?!hound)"匹配grey,但只有hound不跟着它才行。因此grey仅仅在socks之前才匹配。 20 | -------------------------------------------------------------------------------- /09.9.2 向后查找: -------------------------------------------------------------------------------- 1 | 9.9.2 向后查找 2 | 3 | 用?<=正向后查找检查其子模式可以立即向文本字符串的当前位置左侧匹配。 4 | 5 | > (regexp-match-positions #rx"(?<=grey)hound" 6 | "the hound in the picture is not a greyhound") 7 | 8 | '((38 . 43)) 9 | 10 | 正则表达式#rx"(?<=grey)hound"匹配hound,但前提是它是先于grey的。 11 | 12 | 用? (regexp-match-positions #rx"(? (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (/ 1 0))))))) 6 | 7 | /: division by zero 8 | 9 | 但如果控制逃逸”所有的出路“,为什么REPL在一个错误被打印之后能够继续运行?你可能会认为这是因为REPL把每一个互动封装进了with-handlers表里,它抓取了所有的异常,但这确实不是原因。 10 | 11 | 实际的原因是,REPL用一个提示封装了互动,有效地用一个逃逸位置标记求值上下文。如果一个异常没有被捕获,那么关于异常的信息被打印印刷,然后求值中止到最近的封闭提示。更确切地说,每个提示有提示标签(prompt tag),并有指定的默认提示标签,未捕获的异常处理程序使用中止。 12 | 13 | call-with-continuation-prompt函数用一个给定的提示标签设置提示,然后在提示符下对一个给定的thunk求值。default-continuation-prompt-tag函数返回默认提示标记。abort-current-continuation函数转义到具有给定提示标记的最近的封闭提示符。 14 | 15 | > (define (escape v) 16 | (abort-current-continuation 17 | (default-continuation-prompt-tag) 18 | (lambda () v))) 19 | > (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (escape 0))))))) 20 | 21 | 0 22 | > (+ 1 23 | (call-with-continuation-prompt 24 | (lambda () 25 | (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (escape 0)))))))) 26 | (default-continuation-prompt-tag))) 27 | 28 | 1 29 | 30 | 在上面的escape中,值V被封装在一个过程中,该过程在转义到封闭提示符后被调用。 31 | 32 | 提示和中止看起来非常像异常处理和引发。事实上,提示和中止本质上是一种更原始的异常形式,与with-handlers和raise都是按提示执行和中止。更原始形式的权力与操作符名称中的“延续(continuation)”一词有关,我们将在下一节中讨论。 33 | -------------------------------------------------------------------------------- /10.3 延续: -------------------------------------------------------------------------------- 1 | 10.3 延续 2 | 3 | 延续(continuation)是一个值,该值封装了表达式的求值上下文。call-with-composable-continuation函数从当前函数调用和运行到最近的外围提示捕获当前延续。(记住,每个REPL互动都是隐含地封装在REPL提示中。) 4 | 5 | 例如,在下面内容里 6 | 7 | (+ 1 (+ 1 (+ 1 0))) 8 | 9 | 在求值0的位置时,表达式上下文包含三个嵌套的加法表达式。我们可以通过更改0来获取上下文,然后在返回0之前获取延续: 10 | 11 | > (define saved-k #f) 12 | > (define (save-it!) 13 | (call-with-composable-continuation 14 | (lambda (k) ; k is the captured continuation 15 | (set! saved-k k) 16 | 0))) 17 | > (+ 1 (+ 1 (+ 1 (save-it!)))) 18 | 19 | 3 20 | 21 | 保存在save-k中的延续封装程序上下文(+ 1 (+ 1 (+ 1 ?))),?代表插入结果值的位置——因为在save-it!被调用时这是表达式上下文。延续被封装从而其行为类似于函数(lambda (v) (+ 1 (+ 1 (+ 1 v)))): 22 | 23 | > (saved-k 0) 24 | 25 | 3 26 | > (saved-k 10) 27 | 28 | 13 29 | > (saved-k (saved-k 0)) 30 | 31 | 6 32 | 33 | 通过call-with-composable-continuation捕获延续是动态确定的,没有语法。例如,用 34 | 35 | > (define (sum n) 36 | (if (zero? n) 37 | (save-it!) 38 | (+ n (sum (sub1 n))))) 39 | > (sum 5) 40 | 41 | 15 42 | 43 | 在saved-k里延续成为(lambda (x) (+ 5 (+ 4 (+ 3 (+ 2 (+ 1 x)))))): 44 | 45 | > (saved-k 0) 46 | 47 | 15 48 | > (saved-k 10) 49 | 50 | 25 51 | 52 | 在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),因为他们没有一个内置的abort。 53 | 54 | 作为一个延续是多么有用的例子,请参见《更多,用Racket进行系统编程》(More: Systems Programming with Racket)。对于具体的控制操作符,它有比这里描述的原语更恰当的名字,请参见《racket/control》部分。 55 | -------------------------------------------------------------------------------- /11 迭代和推导: -------------------------------------------------------------------------------- 1 | 11 迭代和推导 2 | 3 | 用于语法形式的for家族支持对序列进行迭代。 列表,向量,字符串,字节字符串,输入端口和散列表都可以用作序列,像in-range的构造函数可以提供更多类型的序列。 4 | 5 | for的变种累积迭代结果以不同的方式,但它们都具有相同的语法形状。 现在简化了,for的语法是 6 | 7 | (for ([id sequence-expr] ...) 8 | body ...+) 9 | 10 | for循环遍历由sequence-expr生成的序列。 对于序列的每个元素,将元素绑定到id,然后输出bodys的效果。 11 | 12 | 示列: 13 | > (for ([i '(1 2 3)]) 14 | (display i)) 15 | 16 | 123 17 | > (for ([i "abc"]) 18 | (printf "~a..." i)) 19 | 20 | a...b...c... 21 | > (for ([i 4]) 22 | (display i)) 23 | 24 | 0123 25 | 26 | for的for / list变体更像Racket。 它将body结果累积到一个列表中,而不是输出body仅仅一种效果。 在更多的技术术语中,for / list实现了列表内容的理解。 27 | 28 | 示列: 29 | > (for/list ([i '(1 2 3)]) 30 | (* i i)) 31 | 32 | '(1 4 9) 33 | > (for/list ([i "abc"]) 34 | i) 35 | 36 | '(#\a #\b #\c) 37 | > (for/list ([i 4]) 38 | i) 39 | 40 | '(0 1 2 3) 41 | 42 | for的完整语法可容纳多个序列并行迭代,for*变体可以嵌套迭代,而不是并行运行。 for和for *积累body的更多变体以不同的方式产生。 在所有这些变体中,包含迭代的谓词都可以包含在绑定中。 43 | 44 | 不过,在详细讨论变量的变化之前,最好先看看生成有趣示例的序列生成器的种类。 45 | -------------------------------------------------------------------------------- /11.01 序列构造: -------------------------------------------------------------------------------- 1 | 11.1 序列构造 2 | 3 | in-range 函数生成数字序列,给定一个可选的起始数字(默认为0),序列结束前的数字和一个可选的步长(默认为1)。 直接使用非负整数k作为序列是(范围内k)的简写。 4 | 例如: 5 | > (for ([i 3]) 6 | (display i)) 7 | 8 | 012 9 | > (for ([i (in-range 3)]) 10 | (display i)) 11 | 12 | 012 13 | > (for ([i (in-range 1 4)]) 14 | (display i)) 15 | 16 | 123 17 | > (for ([i (in-range 1 4 2)]) 18 | (display i)) 19 | 20 | 13 21 | > (for ([i (in-range 4 1 -1)]) 22 | (display i)) 23 | 24 | 432 25 | > (for ([i (in-range 1 4 1/2)]) 26 | (printf " ~a " i)) 27 | 28 | 1 3/2 2 5/2 3 7/2 29 | 30 | in-naturals函数是相似的,除了起始数字必须是一个确切的非负整数(默认为0),步长总是1,没有上限。 for循环只使用in-naturals将永远不会终止,除非正文表达引发异常或以其他方式退出。 31 | 例如: 32 | > (for ([i (in-naturals)]) 33 | (if (= i 10) 34 | (error "too much!") 35 | (display i))) 36 | 37 | 0123456789 38 | too much! 39 | 40 | stop-before和stop-after函数构造一个给定序列和判断式的新序列。 新序列就像给定的序列,但是在判断式返回true的第一个元素之前或之后立即被截断。 41 | 例如: 42 | > (for ([i (stop-before "abc def" 43 | char-whitespace?)]) 44 | (display i)) 45 | 46 | abc 47 | 48 | 像in-list,in-vector和in-string这样的序列构造函数只是简单地使用list,vector或string作为序列。 和in-range一样,这些构造函数在给定错误类型的值时会引发异常,并且由于它们会避免运行时调度来确定序列类型,因此可以实现更高效的代码生成; 有关更多信息,请参阅迭代性能。 49 | 例如: 50 | > (for ([i (in-string "abc")]) 51 | (display i)) 52 | 53 | abc 54 | > (for ([i (in-string '(1 2 3))]) 55 | (display i)) 56 | 57 | in-string: contract violation 58 | 59 | expected: string 60 | 61 | given: '(1 2 3) 62 | -------------------------------------------------------------------------------- /11.02 for和for*: -------------------------------------------------------------------------------- 1 | 11.2 for和for* 2 | 3 | 更完整的语法是 4 | 5 | (for (clause ...) 6 | body ...+) 7 | 8 | clause = [id sequence-expr] 9 | | #:when boolean-expr 10 | | #:unless boolean-expr 11 | 12 | 当多个[id sequence-expr]从句在for表里提供时,相应的序列并行遍历: 13 | 14 | > (for ([i (in-range 1 4)] 15 | [chapter '("Intro" "Details" "Conclusion")]) 16 | (printf "Chapter ~a. ~a\n" i chapter)) 17 | 18 | Chapter 1. Intro 19 | 20 | Chapter 2. Details 21 | 22 | Chapter 3. Conclusion 23 | 24 | 对于并行序列,for表达式在任何序列结束时停止迭代。这种行为允许in-naturals创造数值的无限序列,可用于索引: 25 | 26 | > (for ([i (in-naturals 1)] 27 | [chapter '("Intro" "Details" "Conclusion")]) 28 | (printf "Chapter ~a. ~a\n" i chapter)) 29 | 30 | Chapter 1. Intro 31 | 32 | Chapter 2. Details 33 | 34 | Chapter 3. Conclusion 35 | 36 | for*表具有与for语法相同的语法,嵌套多个序列,而不是并行运行它们: 37 | 38 | > (for* ([book '("Guide" "Reference")] 39 | [chapter '("Intro" "Details" "Conclusion")]) 40 | (printf "~a ~a\n" book chapter)) 41 | 42 | Guide Intro 43 | 44 | Guide Details 45 | 46 | Guide Conclusion 47 | 48 | Reference Intro 49 | 50 | Reference Details 51 | 52 | Reference Conclusion 53 | 54 | 因此,for*是对嵌套for的一个简写,以同样的方式let*是一个let嵌套的简写。 55 | 56 | 一个clause的#:when boolean-expr表是另一个简写。仅当boolean-expr产生一个真值时它允许body求值: 57 | 58 | > (for* ([book '("Guide" "Reference")] 59 | [chapter '("Intro" "Details" "Conclusion")] 60 | #:when (not (equal? chapter "Details"))) 61 | (printf "~a ~a\n" book chapter)) 62 | 63 | Guide Intro 64 | 65 | Guide Conclusion 66 | 67 | Reference Intro 68 | 69 | Reference Conclusion 70 | 71 | 一个带#:when的boolean-expr可以适用于任何上述迭代绑定。在一个for表里,仅仅如果在前面绑定的迭代测试是嵌套的时,这个范围是有意义的;因此,用#:when隔离绑定是多重嵌套的,而不是平行的,甚至于用for也一样。 72 | 73 | > (for ([book '("Guide" "Reference" "Notes")] 74 | #:when (not (equal? book "Notes")) 75 | [i (in-naturals 1)] 76 | [chapter '("Intro" "Details" "Conclusion" "Index")] 77 | #:when (not (equal? chapter "Index"))) 78 | (printf "~a Chapter ~a. ~a\n" book i chapter)) 79 | 80 | Guide Chapter 1. Intro 81 | 82 | Guide Chapter 2. Details 83 | 84 | Guide Chapter 3. Conclusion 85 | 86 | Reference Chapter 1. Intro 87 | 88 | Reference Chapter 2. Details 89 | 90 | Reference Chapter 3. Conclusion 91 | 92 | 一个#:unless从句和一个#:when从句是类似的,但仅当boolean-expr产生一个非值时body求值。 93 | -------------------------------------------------------------------------------- /11.03 for|list和for*|list: -------------------------------------------------------------------------------- 1 | 11.3 for/list和for*/list 2 | 3 | for/list表具有与for相同的语法,它对body求值以获取进入新构造列表的值: 4 | 5 | > (for/list ([i (in-naturals 1)] 6 | [chapter '("Intro" "Details" "Conclusion")]) 7 | (string-append (number->string i) ". " chapter)) 8 | 9 | '("1. Intro" "2. Details" "3. Conclusion") 10 | 11 | 在一个for-list表中的一个#:when从句随着body的求值修整结果列表: 12 | 13 | > (for/list ([i (in-naturals 1)] 14 | [chapter '("Intro" "Details" "Conclusion")] 15 | #:when (odd? i)) 16 | chapter) 17 | 18 | '("Intro" "Conclusion") 19 | 20 | #:when的这种修剪行为使用for/list比for更有用。而对for来说扁平的when表通常是满足需要的,一个for/list里的一个when表达表会导致结果列表包含 #以代替省略列表元素。 21 | 22 | for*/list表类似于for*,嵌套多个迭代: 23 | 24 | > (for*/list ([book '("Guide" "Ref.")] 25 | [chapter '("Intro" "Details")]) 26 | (string-append book " " chapter)) 27 | 28 | '("Guide Intro" "Guide Details" "Ref. Intro" "Ref. Details") 29 | 30 | for*/list表与嵌套for/list表不太一样。嵌套for/list将生成一个列表的列表,而不是一个扁平列表。非常像#:when,那么,for*/list的嵌套比for*的嵌套更有用。 31 | -------------------------------------------------------------------------------- /11.04 for|vector和for*|vector: -------------------------------------------------------------------------------- 1 | 11.4 for/vector和for*/vector 2 | 3 | for/vector表可以使用与for/list表相同的语法,但是对body的求值进入一个新构造的向量而不是列表: 4 | 5 | > (for/vector ([i (in-naturals 1)] 6 | [chapter '("Intro" "Details" "Conclusion")]) 7 | (string-append (number->string i) ". " chapter)) 8 | 9 | '#("1. Intro" "2. Details" "3. Conclusion") 10 | 11 | 12 | for*/vector表的行为类似,但迭代嵌套和for*一样。 13 | 14 | for/vector和for*/vector表也允许构造向量的长度,在预先提供的情况下。由此产生的迭代可以比for/vector或for*/vector更有效地执行: 15 | 16 | > (let ([chapters '("Intro" "Details" "Conclusion")]) 17 | (for/vector #:length (length chapters) ([i (in-naturals 1)] 18 | [chapter chapters]) 19 | (string-append (number->string i) ". " chapter))) 20 | 21 | '#("1. Intro" "2. Details" "3. Conclusion") 22 | 23 | 如果提供了一个长度,当vector被填充或被请求完成时迭代停止,而无论哪个先来。如果所提供的长度超过请求的迭代次数,则向量中的剩余槽被初始化为make-vector的缺省参数。 24 | -------------------------------------------------------------------------------- /11.05 for|and和for|or: -------------------------------------------------------------------------------- 1 | 11.5 for/and和for/or 2 | 3 | for/and表用and组合迭代结果,一旦遇到#f就停止: 4 | 5 | > (for/and ([chapter '("Intro" "Details" "Conclusion")]) 6 | (equal? chapter "Intro")) 7 | 8 | #f 9 | 10 | for/or表用or组合迭代结果,一旦遇到真值立即停止: 11 | 12 | > (for/or ([chapter '("Intro" "Details" "Conclusion")]) 13 | (equal? chapter "Intro")) 14 | 15 | #t 16 | 17 | 与通常一样,for*/and和for*/or表提供与嵌套迭代相同的功能。 18 | -------------------------------------------------------------------------------- /11.06 for|first和for|last: -------------------------------------------------------------------------------- 1 | 11.6 for/first和for/last 2 | 3 | for/first表返回第一次对body进行求值的结果,跳过了进一步的迭代。这个带有一个#:when从句的表是最非常有用的。 4 | 5 | > (for/first ([chapter '("Intro" "Details" "Conclusion" "Index")] 6 | #:when (not (equal? chapter "Intro"))) 7 | chapter) 8 | 9 | "Details" 10 | 11 | 如果body求值进行零次,那么结果是#f。 12 | 13 | for/last表运行所有迭代,返回最后一次迭代的值(或如果没有迭代运行返回#f): 14 | 15 | > (for/last ([chapter '("Intro" "Details" "Conclusion" "Index")] 16 | #:when (not (equal? chapter "Index"))) 17 | chapter) 18 | 19 | "Conclusion" 20 | 21 | 通常, for*/first和for*/last表提供和嵌套迭代相同的工具: 22 | 23 | > (for*/first ([book '("Guide" "Reference")] 24 | [chapter '("Intro" "Details" "Conclusion" "Index")] 25 | #:when (not (equal? chapter "Intro"))) 26 | (list book chapter)) 27 | 28 | '("Guide" "Details") 29 | > (for*/last ([book '("Guide" "Reference")] 30 | [chapter '("Intro" "Details" "Conclusion" "Index")] 31 | #:when (not (equal? chapter "Index"))) 32 | (list book chapter)) 33 | 34 | '("Reference" "Conclusion") 35 | -------------------------------------------------------------------------------- /11.07 for|fold和for*fold: -------------------------------------------------------------------------------- 1 | 11.7 for/fold和for*fold 2 | 3 | for/fold表是合并迭代结果的一种非常通用的方法。它的语法与原来的for语法略有不同,因为必须在开始时声明累积变量: 4 | 5 | (for/fold ([accum-id init-expr] ...) 6 | (clause ...) 7 | body ...+) 8 | 9 | 在简单的情况下,仅提供一个 [accum-id init-expr],那么for/fold的结果是accum-id的最终值,并启动了init-expr的值。在clause和body、accum-id可参照获得其当前值,并且最后的body为下一次迭代的提供accum-id值。 10 | 11 | 例如: 12 | 13 | > (for/fold ([len 0]) 14 | ([chapter '("Intro" "Conclusion")]) 15 | (+ len (string-length chapter))) 16 | 17 | 15 18 | > (for/fold ([prev #f]) 19 | ([i (in-naturals 1)] 20 | [chapter '("Intro" "Details" "Details" "Conclusion")] 21 | #:when (not (equal? chapter prev))) 22 | (printf "~a. ~a\n" i chapter) 23 | chapter) 24 | 25 | 1. Intro 26 | 27 | 2. Details 28 | 29 | 4. Conclusion 30 | 31 | "Conclusion" 32 | 33 | 当多个accum-id被指定,那么最后的body必须产生多值,每一个对应accum-id. for/fold的表达本身产生多值给结果。 34 | 35 | 例如: 36 | 37 | > (for/fold ([prev #f] 38 | [counter 1]) 39 | ([chapter '("Intro" "Details" "Details" "Conclusion")] 40 | #:when (not (equal? chapter prev))) 41 | (printf "~a. ~a\n" counter chapter) 42 | (values chapter 43 | (add1 counter))) 44 | 45 | 1. Intro 46 | 47 | 2. Details 48 | 49 | 3. Conclusion 50 | 51 | "Conclusion" 52 | 53 | 4 54 | -------------------------------------------------------------------------------- /11.08 多值序列: -------------------------------------------------------------------------------- 1 | 11.8 多值序列 2 | 3 | 同样,一个函数或表达式可以生成多个值,序列的单个迭代可以生成多个元素。例如,作为一个序列的哈希表生成两个迭代的两个值:一个键和一个值。 4 | 5 | 同样,let-values将多个结果绑定到多个标识符,for能将多个序列元素绑定到多个迭代标识符: 6 | 7 | > (for ([(k v) #hash(("apple" . 1) ("banana" . 3))]) 8 | (printf "~a count: ~a\n" k v)) 9 | 10 | apple count: 1 11 | 12 | banana count: 3 13 | 14 | 这种扩展到多值绑定对所有变量都适用。例如,对于列表嵌套迭代,构建一个列表,也可以处理多值序列: 15 | 16 | > (for*/list ([(k v) #hash(("apple" . 1) ("banana" . 3))] 17 | [(i) (in-range v)]) 18 | k) 19 | 20 | '("apple" "banana" "banana" "banana") 21 | -------------------------------------------------------------------------------- /11.10 迭代性能: -------------------------------------------------------------------------------- 1 | 11.10 迭代性能 2 | 3 | 理想情况下,作为递归函数调用,一个for迭代的运行速度应该与手工编写的循环一样快。然而,手写循环通常是针对特定类型的数据,如列表。在这种情况下,手写循环直接使用选择器,比如car和cdr,而不是处理所有序列表并分派给合适的迭代器。 4 | 5 | 当足够的信息反复提供给迭代序列时,for表可以提供手写循环的性能。具体来说,从句应具有下列fast-clause表之一: 6 | 7 | fast-clause = [id fast-seq] 8 | | [(id) fast-seq] 9 | | [(id id) fast-indexed-seq] 10 | | [(id ...) fast-parallel-seq] 11 | 12 | fast-seq = (in-range expr) 13 | | (in-range expr expr) 14 | | (in-range expr expr expr) 15 | | (in-naturals) 16 | | (in-naturals expr) 17 | | (in-list expr) 18 | | (in-vector expr) 19 | | (in-string expr) 20 | | (in-bytes expr) 21 | | (in-value expr) 22 | | (stop-before fast-seq predicate-expr) 23 | | (stop-after fast-seq predicate-expr) 24 | 25 | fast-indexed-seq = (in-indexed fast-seq) 26 | | (stop-before fast-indexed-seq predicate-expr) 27 | | (stop-after fast-indexed-seq predicate-expr) 28 | 29 | fast-parallel-seq = (in-parallel fast-seq ...) 30 | | (stop-before fast-parallel-seq predicate-expr) 31 | | (stop-after fast-parallel-seq predicate-expr) 32 | 33 | 例如: 34 | 35 | > (time (for ([i (in-range 100000)]) 36 | (for ([elem (in-list '(a b c d e f g h))]) ;较慢 37 | (void)))) 38 | 39 | cpu time: 7 real time: 2 gc time: 0 40 | > (time (for ([i (in-range 100000)]) 41 | (for ([elem '(a b c d e f g h)]) ;较慢 42 | (void)))) 43 | 44 | cpu time: 216 real time: 56 gc time: 0 45 | > (time (let ([seq (in-list '(a b c d e f g h))]) 46 | (for ([i (in-range 100000)]) 47 | (for ([elem seq]) ;较慢 48 | (void))))) 49 | 50 | cpu time: 227 real time: 59 gc time: 0 51 | 52 | 上面的语法是不完整的,因为提供良好性能的语法模式集是可扩展的,就像序列值集一样。序列构造器的文档应该说明直接使用for从句的性能优势。 53 | -------------------------------------------------------------------------------- /13 类和对象: -------------------------------------------------------------------------------- 1 | 13 类和对象 2 | 3 | 类表达式表示一类值,就像lambda表达式一样: 4 | 5 | (class superclass-expr decl-or-expr ...) 6 | 7 | superclass-expr确定为新类的基类。每个decl-or-expr既是一个声明,关系到对方法、字段和初始化参数,也是一个表达式,每次求值就实例化类。换句话说,与方法之类的构造器不同,类具有与字段和方法声明交错的初始化表达式。 8 | 9 | 按照惯例,类名以%结束。内置根类是object%。下面的表达式用公共方法get-size、grow和eat创建一个类: 10 | 11 | (class object% 12 | (init size) ; 初始化参数 13 | 14 | (define current-size size) ; 字段 15 | 16 | (super-new) ; 基类初始化 17 | 18 | (define/public (get-size) 19 | current-size) 20 | 21 | (define/public (grow amt) 22 | (set! current-size (+ amt current-size))) 23 | 24 | (define/public (eat other-fish) 25 | (grow (send other-fish get-size)))) 26 | 27 | 当通过new表实例化类时,size的初始化参数必须通过一个命名参数提供: 28 | 29 | (new (class object% (init size) ....) [size 10]) 30 | 31 | 当然,我们还可以命名类及其实例: 32 | 33 | (define fish% (class object% (init size) ....)) 34 | (define charlie (new fish% [size 10])) 35 | 36 | 在fish%的定义中,current-size是一个以size值初始化参数开头的私有字段。像size这样的初始化参数只有在类实例化时才可用,因此不能直接从方法引用它们。与此相反,current-size字段可用于方法。 37 | 38 | 在fish%中的(super-new)表达式调用基类的初始化。在这种情况下,基类是object%,它没有带初始化参数也没有执行任何工作;必须使用super-new,因为一个类总必须总是调用其基类的初始化。 39 | 40 | 初始化参数、字段声明和表达式如(super-new)可以以类(class)中的任何顺序出现,并且它们可以与方法声明交织在一起。类中表达式的相对顺序决定了实例化过程中的求值顺序。例如,如果一个字段的初始值需要调用一个方法,它只有在基类初始化后才能工作,然后字段声明必须放在super-new调用后。以这种方式排序字段和初始化声明有助于规避不可避免的求值。方法声明的相对顺序对求值没有影响,因为方法在类实例化之前被完全定义。 41 | -------------------------------------------------------------------------------- /13.1 方法: -------------------------------------------------------------------------------- 1 | 13.1 方法 2 | 3 | fish%中的三个define/public声明都引入了一种新方法。声明使用与Racket函数相同的语法,但方法不能作为独立函数访问。调用fish%对象的grow方法需要send表: 4 | 5 | > (send charlie grow 6) 6 | > (send charlie get-size) 7 | 8 | 16 9 | 10 | 在fish%中,自方法可以被像函数那样调用,因为方法名在作用域中。例如,fish%中的eat方法直接调用grow方法。在类中,试图以除方法调用以外的任何方式使用方法名会导致语法错误。 11 | 12 | 在某些情况下,一个类必须调用由基类提供但不能被重写的方法。在这种情况下,类可以使用带this的send来访问该方法: 13 | 14 | (define hungry-fish% (class fish% (super-new) 15 | (define/public (eat-more fish1 fish2) 16 | (send this eat fish1) 17 | (send this eat fish2)))) 18 | 19 | 另外,类可以声明一个方法使用inherit(继承)的存在,该方法将方法名引入到直接调用的作用域中: 20 | 21 | define hungry-fish% (class fish% (super-new) 22 | (inherit eat) 23 | (define/public (eat-more fish1 fish2) 24 | (eat fish1) (eat fish2)))) 25 | 26 | 在inherit声明中,如果fish%没有提供一个eat方法,那么在对hungry-fish%类表的求值中会出现一个错误。与此相反,用(send this ....),直到eat-more方法被调和send表被求值前不会发出错误信号。因此,inherit是首选。 27 | 28 | send的另一个缺点是它比inherit效率低。一个方法的请求通过send调用寻找在运行时在目标对象的类的方法,使send类似于java方法调用接口。相反,基于inherit的方法调用使用一个类的方法表中的偏移量,它在类创建时计算。 29 | 30 | 为了在从方法类之外调用方法时实现与继承方法调用类似的性能,程序员必须使用generic(泛型)表,它生成一个特定类和特定方法的generic方法,用send-generic调用: 31 | 32 | (define get-fish-size (generic fish% get-size)) 33 | 34 | 35 | > (send-generic charlie get-fish-size) 36 | 37 | 16 38 | > (send-generic (new hungry-fish% [size 32]) get-fish-size) 39 | 40 | 32 41 | > (send-generic (new object%) get-fish-size) 42 | 43 | generic:get-size: target is not an instance of the generic's 44 | 45 | class 46 | 47 | target: (object) 48 | 49 | class name: fish% 50 | 51 | 粗略地说,表单将类和外部方法名转换为类方法表中的位置。如上一个例子所示,通过泛型方法发送检查它的参数是泛型类的一个实例。 52 | 53 | 54 | 是否在class内直接调用方法,通过泛型方法,或通过send,方法以通常的方式重写工程: 55 | 56 | (define picky-fish% (class fish% (super-new) 57 | (define/override (grow amt) 58 | 59 | (super grow (* 3/4 amt))))) 60 | (define daisy (new picky-fish% [size 20])) 61 | 62 | 63 | > (send daisy eat charlie) 64 | > (send daisy get-size) 65 | 66 | 32 67 | 68 | 在picky-fish%的grow方法是用define/override声明的,而不是define/public,因为grow是作为一个重写的申明的意义。如果grow已经用define/public声明,那么在对类表达式求值时会发出一个错误,因为fish%已经提供了grow。 69 | 70 | 使用define/override也允许通过super调用调用重写的方法。例如,grow在picky-fish%实现使用super代理给基类的实现。 71 | -------------------------------------------------------------------------------- /13.2 初始化参数: -------------------------------------------------------------------------------- 1 | 13.2 初始化参数 2 | 3 | 因为picky-fish%申明没有任何初始化参数,任何初始化值在(new picky-fish% ....)里提供都被传递给基类的初始化,即传递给fish%。子类可以在super-new调用其基类时提供额外的初始化参数,这样的初始化参数会优先于参数提供给new。例如,下面的size-10-fish%类总是产生大小为10的鱼: 4 | 5 | (define size-10-fish% (class fish% (super-new [size 10]))) 6 | 7 | 8 | > (send (new size-10-fish%) get-size) 9 | 10 | 10 11 | 12 | 就size-10-fish%来说,用new提供一个size初始化参数会导致初始化错误;因为在super-new里的size优先,size提供给new没有目标申明。 13 | 14 | 如果class表声明一个默认值,则初始化参数是可选的。例如,下面的default-10-fish%类接受一个size的初始化参数,但如果在实例里没有提供值那它的默认值是10: 15 | 16 | (define default-10-fish% (class fish% 17 | (init [size 10]) 18 | (super-new [size size]))) 19 | 20 | 21 | > (new default-10-fish%) 22 | 23 | (object:default-10-fish% ...) 24 | > (new default-10-fish% [size 20]) 25 | 26 | (object:default-10-fish% ...) 27 | 28 | 在这个例子中,super-new调用传递它自己的size值作为size初始化初始化参数传递给基类。 29 | -------------------------------------------------------------------------------- /13.3 内部和外部名称: -------------------------------------------------------------------------------- 1 | 13.3 内部和外部名称 2 | 3 | 在default-10-fish%中size的两个使用揭示了类成员标识符的双重身份。当size是new或super-new中的一个括号对的第一标识符,size是一个外部名称(external name),象征性地匹配到类中的初始化参数。当size作为一个表达式出现在default-10-fish%中,size是一个内部名称(internal name),它是词法作用域。类似地,对继承的eat方法的调用使用eat作为内部名称,而一个eat的send的使用作为一个外部名称。 4 | 5 | class表的完整语法允许程序员为类成员指定不同的内部和外部名称。由于内部名称是本地的,因此可以重命名它们,以避免覆盖或冲突。这样的改名不总是必要的,但重命名缺乏的解决方法可以是特别繁琐。 6 | -------------------------------------------------------------------------------- /13.4 接口(Interface): -------------------------------------------------------------------------------- 1 | 13.4 接口(Interface) 2 | 3 | 接口对于检查一个对象或一个类实现一组具有特定(隐含)行为的方法非常有用。接口的这种使用有帮助的,即使没有静态类型系统(那是java有接口的主要原因)。 4 | 5 | Racket中的接口通过使用interface表创建,它只声明需要去实现的接口的方法名称。接口可以扩展其它接口,这意味着接口的实现会自动实现扩展接口。 6 | 7 | (interface (superinterface-expr ...) id ...) 8 | 9 | 为了声明一个实现一个接口的类,必须使用class*表代替class: 10 | 11 | (class* superclass-expr (interface-expr ...) decl-or-expr ...) 12 | 13 | 例如,我们不必强制所有的fish类都是源自于fish%,我们可以定义fish-interface并改变fish%类来声明它实现了fish-interface: 14 | 15 | (define fish-interface (interface () get-size grow eat)) 16 | (define fish% (class* object% (fish-interface) ....)) 17 | 18 | 如果fish%的定义不包括get-size、grow和eat方法,那么在class*表求值时会出现错误,因为实现fish-interface接口需要这些方法。 19 | 20 | is-a?判断接受一个对象作为它的第一个参数,同时类或接口作为它的第二个参数。当给了一个类,无论对象是该类的实例或者派生类的实例,is-a?都执行检查。当给一个接口,无论对象的类是否实现接口,is-a?都执行检查。另外,implementation?判断检查给定类是否实现给定接口。 21 | -------------------------------------------------------------------------------- /13.5 Final、Augment和Inner: -------------------------------------------------------------------------------- 1 | 13.5 Final、Augment和Inner 2 | 3 | 在java中,一个class表的方法可以被指定为最终的(final),这意味着一个子类不能重写方法。一个最终方法是使用public-final或override-final申明,取决于声明是为一个新方法还是一个重写实现。 4 | 5 | 在允许与不允许任意完全重写的两个极端之间,类系统还支持Beta类型的可扩展(augmentable)方法。一个带pubment声明的方法类似于public,但方法不能在子类中重写;它仅仅是可扩充。一个pubment方法必须显式地使用inner调用一个扩展(如果有);一个子类使用augment扩展方法,而不是使用override。 6 | 7 | 一般来说,一个方法可以在类派生的扩展模式和重写模式之间进行切换。augride方法详述表明了一个扩展,这里这个扩展本身在子类中是可重写的的方法(虽然这个基类的实现不能重写)。同样,overment重写一个方法并使得重写的实现变得可扩展。 8 | -------------------------------------------------------------------------------- /13.6 控制外部名称的范围: -------------------------------------------------------------------------------- 1 | 13.6 控制外部名称的范围 2 | 3 | 正如《内部和外部名称》(Internal and External Names)所指出的,类成员既有内部名称,也有外部名称。成员定义在本地绑定内部名称,此绑定可以在本地重命名。与此相反,外部名称默认情况下具有全局范围,成员定义不绑定外部名称。相反,成员定义指的是外部名称的现有绑定,其中成员名绑定到成员键(member key);一个类最终将成员键映射到方法、字段和初始化参数。 4 | 5 | 回头看hungry-fish%类表达式: 6 | 7 | (define hungry-fish% (class fish% .... 8 | (inherit eat) 9 | (define/public (eat-more fish1 fish2) 10 | (eat fish1) (eat fish2)))) 11 | 12 | 在求值过程中,hungry-fish%类和fish%类指相同的eat的全局绑定。在运行时,在hungry-fish%中调用eat是通过共享绑定到eat的方法键和fish%中的eat方法相匹配。 13 | 14 | 对外部名称的默认绑定是全局的,但程序员可以用define-member-name表引入外部名称绑定。 15 | 16 | (define-member-name id member-key-expr) 17 | 18 | 特别是,通过使用(generate-member-key)作为member-key-expr,外部名称可以为一个特定的范围局部化,因为生成的成员键范围之外的访问。换句话说,定义成员名称给外部名称一种私有包范围,但从包中概括为Racket中的任意绑定范围。 19 | 20 | 例如,下面的fish%类和pond%类通过一个get-depth方法配合,只有这个配合类可以访问: 21 | 22 | (define-values (fish% pond%) ; two mutually recursive classes 23 | (let () 24 | (define-member-name get-depth (generate-member-key)) 25 | (define fish% 26 | (class .... 27 | (define my-depth ....) 28 | (define my-pond ....) 29 | (define/public (dive amt) 30 | (set! my-depth 31 | (min (+ my-depth amt) 32 | (send my-pond get-depth)))))) 33 | (define pond% 34 | (class .... 35 | (define current-depth ....) 36 | (define/public (get-depth) current-depth))) 37 | (values fish% pond%))) 38 | 39 | 外部名称在名称空间中,将它们与其它Racket名称分隔开。这个单独的命名空间被隐式地用于send中的方法名、在new中的初始化参数名称,或成员定义中的外部名称。特殊表 member-name-key提供对任意表达式位置外部名称的绑定的访问:(member-name-key id)在当前范围内生成id的成员键绑定。 40 | 41 | 成员键值主要用于define-member-name表。通常,(member-name-key id)捕获id的方法键,以便它可以在不同的范围内传递到define-member-name的使用。这种能力证明推广混合是有用的,作为接下来的讨论。 42 | -------------------------------------------------------------------------------- /13.7 混合(mixin): -------------------------------------------------------------------------------- 1 | 13.7 混合(mixin) 2 | 3 | 因为类(class)是一种表达表,而不是如同在Smalltalk和java里的一个顶级的声明,一个类表可以嵌套在任何词法范围内,包括lambda(λ)。其结果是一个mixin,即,一个类的扩展,是相对于它的基类的参数化。 4 | 5 | 例如,我们可以参数化picky-fish%类来覆盖它的基类从而定义picky-mixin: 6 | 7 | (define (picky-mixin %) 8 | (class % (super-new) 9 | (define/override (grow amt) (super grow (* 3/4 amt))))) 10 | (define picky-fish% (picky-mixin fish%)) 11 | 12 | Smalltalk风格类和Racket类之间的许多小的差异有助于混合的有效利用。特别是,define/override的使用使得picky-mixin期望一个类带有一个grow方法更明确。如果picky-mixin应用于一个没有grow方法的类,一旦应用picky-mixin则会发出一个错误的信息。 13 | 14 | 同样,当应用混合时使用继承(inherit)执行“方法实体(method existence)”的要求: 15 | 16 | (define (hungry-mixin %) 17 | (class % (super-new) 18 | (inherit eat) 19 | (define/public (eat-more fish1 fish2) 20 | (eat fish1) 21 | (eat fish2)))) 22 | 23 | mixin的优势是,我们可以很容易地将它们结合起来以创建新的类,其共享的实现不适合一个继承层次——没有多继承相关的歧义。配备picky-mixin和hungry-mixin,为饥饿(hungry)创造了一个类,但挑剔的鱼(picky fish)是直截了当的: 24 | 25 | (define picky-hungry-fish% 26 | (hungry-mixin (picky-mixin fish%))) 27 | 28 | 关键词初始化参数的使用是mixin的易于使用的重点。例如,picky-mixin和hungry-mixin可以通过合适的eat方法和grow方法增加任何类,因为它们在它们的super-new表达式里没有指定初始化参数也没有添加东西: 29 | 30 | (define person% 31 | (class object% 32 | (init name age) 33 | .... 34 | (define/public (eat food) ....) 35 | (define/public (grow amt) ....))) 36 | (define child% (hungry-mixin (picky-mixin person%))) 37 | (define oliver (new child% [name "Oliver"] [age 6])) 38 | 39 | 最后,对类成员的外部名称的使用(而不是词法作用域标识符)使得混合使用很方便。添加picky-mixin到person%运行,因为这个名字eat和grow匹配,在fish%和person%里没有任何eat和grow的优先申明可以是同样的方法。当成员名称意外碰撞后,此特性是一个潜在的缺陷;一些意外冲突可以通过限制外部名称作用域来纠正,就像在《控制外部名称的范围(Controlling the Scope of External Names)》所讨论的那样。 40 | -------------------------------------------------------------------------------- /13.7.1 混合和接口: -------------------------------------------------------------------------------- 1 | 13.7.1 混合和接口 2 | 3 | 使用implementation?,picky-mixin可以要求其基类实现grower-interface,这可以是由fish%和person%实现: 4 | 5 | (define grower-interface (interface () grow)) 6 | (define (picky-mixin %) 7 | (unless (implementation? % grower-interface) 8 | (error "picky-mixin: not a grower-interface class")) 9 | (class % ....)) 10 | 11 | 另一个使用带混合的接口是标记类通过混合产生,因此,混合实例可以被识别。换句话说,is-a?不能在一个混合上体现为一个函数运行,但它可以识别为一个接口(有点像一个特定的接口),它总是被混合所实现。例如,通过picky-mixin生成的类可以被picky-interface所标记,使是is-picky?去判定: 12 | 13 | (define picky-interface (interface ())) 14 | (define (picky-mixin %) 15 | (unless (implementation? % grower-interface) 16 | (error "picky-mixin: not a grower-interface class")) 17 | (class* % (picky-interface) ....)) 18 | (define (is-picky? o) 19 | (is-a? o picky-interface)) 20 | -------------------------------------------------------------------------------- /13.7.2 mixin表: -------------------------------------------------------------------------------- 1 | 13.7.2 mixin表 2 | 3 | 为执行混合而编纂lambda-plus-class模式,包括对混合的定义域和值域接口的使用,类系统提供了一个mixin宏: 4 | 5 | (mixin (interface-expr ...) (interface-expr ...) 6 | decl-or-expr ...) 7 | 8 | interface-expr的第一个集合确定混合的定义域,第二个集合确定值域。就是说,扩张是一个函数,它测试是否一个给定的基类实现interface-expr的第一个序列,并产生一个类实现interface-expr的第二个序列。其它要求,如在基类继承的方法的存在,然后检查mixin表的class扩展。例如: 9 | 10 | > (define choosy-interface (interface () choose?)) 11 | > (define hungry-interface (interface () eat)) 12 | > (define choosy-eater-mixin 13 | (mixin (choosy-interface) (hungry-interface) 14 | (inherit choose?) 15 | (super-new) 16 | (define/public (eat x) 17 | (cond 18 | [(choose? x) 19 | (printf "chomp chomp chomp on ~a.\n" x)] 20 | [else 21 | (printf "I'm not crazy about ~a.\n" x)])))) 22 | > (define herring-lover% 23 | (class* object% (choosy-interface) 24 | (super-new) 25 | (define/public (choose? x) 26 | (regexp-match #px"^herring" x)))) 27 | > (define herring-eater% (choosy-eater-mixin herring-lover%)) 28 | > (define eater (new herring-eater%)) 29 | > (send eater eat "elderberry") 30 | 31 | I'm not crazy about elderberry. 32 | > (send eater eat "herring") 33 | 34 | chomp chomp chomp on herring. 35 | > (send eater eat "herring ice cream") 36 | 37 | chomp chomp chomp on herring ice cream. 38 | 39 | 混合不仅覆盖方法,并引入公共方法,它们也可以扩展方法,引入扩展的方法,添加一个可重写的扩展,并添加一个可扩展的覆盖——所有这些事一个类都能完成(参见《Final, Augment, and Inner》部分)。 40 | -------------------------------------------------------------------------------- /13.7.3 参数化的混合: -------------------------------------------------------------------------------- 1 | 13.7.3 参数化的混合 2 | 3 | 正如在《控制外部名称的范围》(Controlling the Scope of External Names)中指出的,外部名称可以用define-member-name绑定。这个工具允许一个混合用定义或使用的方法概括。例如,我们可以通过对eat的外部成员键的使用参数化hungry-mixin: 4 | 5 | (define (make-hungry-mixin eat-method-key) 6 | (define-member-name eat eat-method-key) 7 | (mixin () () (super-new) 8 | (inherit eat) 9 | (define/public (eat-more x y) (eat x) (eat y)))) 10 | 11 | 获得一个特定的hungry-mixin,我们必须应用这个函数到一个成员键,它指向一个适当的eat方法,我们可以获得member-name-key的使用: 12 | 13 | ((make-hungry-mixin (member-name-key eat)) 14 | (class object% .... (define/public (eat x) 'yum))) 15 | 16 | 以上,我们应用hungry-mixin给一个匿名类,它提供eat,但我们也可以把它和一个提供Chomp的类组合,相反: 17 | 18 | ((make-hungry-mixin (member-name-key chomp)) 19 | (class object% .... (define/public (chomp x) 'yum))) 20 | -------------------------------------------------------------------------------- /13.8 特征(trait): -------------------------------------------------------------------------------- 1 | 13.8 特征(trait) 2 | 3 | 一个特征类似于一个mixin,它封装了一组方法添加到一个类里。一个特征不同于一个mixin,它自己的方法是可以用特征运算符操控的,比如trait-sum(合并这两个特征的方法)、trait-exclude(从一个特征中移除方法)以及trait-alias(添加一个带有新名字的方法的拷贝;它不重定向到对任何旧名字的调用)。 4 | 5 | 混合和特征之间的实际差别是两个特征可以组合,即使它们包括了共有的方法,而且即使两者的方法都可以合理地覆盖其它方法。在这种情况下,程序员必须明确地解决冲突,通常通过混淆方法,排除方法,以及合并使用别名的新特性。 6 | 7 | 假设我们的fish%程序员想要定义两个类扩展,spots和stripes,每个都包含get-color方法。鱼(fish)的斑点色(spot)不应该覆盖的条纹色(stripe),反之亦然;相反,一个spots+stripes-fish%应结合两种颜色,这是不可能的如果spots和stripes是普通混合实现。然而,如果spots和stripes作为特征来实现,它们可以组合在一起。首先,我们在每个特征中给get-color起一个别名为一个不冲突的名称。第二,get-color方法从两者中移除,只有别名的特征被合并。最后,新特征用于创建一个类,它基于这两个别名引入自己的get-color方法,生成所需的spots+stripes扩展。 8 | -------------------------------------------------------------------------------- /13.8.1 混合集的特征: -------------------------------------------------------------------------------- 1 | 13.8.1 混合集的特征 2 | 3 | 在Racket里实现特征的一个自然的方法是如同一组混合,每个特征方法带一个mixin。例如,我们可以尝试如下定义spots和stripes的特征,使用关联列表来表示集合: 4 | 5 | (define spots-trait 6 | (list (cons 'get-color 7 | (lambda (%) (class % (super-new) 8 | (define/public (get-color) 9 | 'black)))))) 10 | (define stripes-trait 11 | (list (cons 'get-color 12 | (lambda (%) (class % (super-new) 13 | (define/public (get-color) 14 | 'red)))))) 15 | 16 | 一个集合的表示,如上面所述,允许trait-sum和trait-exclude做为简单操作;不幸的是,它不支持trait-alias运算符。虽然一个混合可以在关联表里复制,混合有一个固定的方法名称,例如,get-color,而且混合不支持方法重命名操作。支持trait-alias,我们必须在扩展方法名上参数化混合,同样地eat在参数化混合(Parameterized Mixins)中进行参数化。 17 | 18 | 为了支持trait-alias操作,spots-trait应表示为: 19 | 20 | (define spots-trait 21 | (list (cons (member-name-key get-color) 22 | (lambda (get-color-key %) 23 | (define-member-name get-color get-color-key) 24 | (class % (super-new) 25 | (define/public (get-color) 'black)))))) 26 | 27 | 当spots-trait中的get-color方法是给get-trait-color的别名并且get-color方法被去除,由此产生的特性如下: 28 | 29 | (list (cons (member-name-key get-trait-color) 30 | (lambda (get-color-key %) 31 | (define-member-name get-color get-color-key) 32 | (class % (super-new) 33 | (define/public (get-color) 'black))))) 34 | 35 | 应用特征T到一个类C和获得一个派生类,我们用((trait->mixin T) C)。trait->mixin函数用给混合的方法和部分C扩展的键提供每个T的混合: 36 | 37 | (define ((trait->mixin T) C) 38 | (foldr (lambda (m %) ((cdr m) (car m) %)) C T)) 39 | 40 | 因此,当上述特性与其它特性结合,然后应用到类中时,get-color的使用将成为外部名称get-trait-color的引用。 41 | -------------------------------------------------------------------------------- /13.8.2 特征的继承与超越: -------------------------------------------------------------------------------- 1 | 13.8.2 特征的继承与超越 2 | 3 | 特性的这个第一个实现支持trait-alias,它支持一个调用自身的特性方法,但是它不支持调用彼此的特征方法。特别是,假设一个spot-fish的市场价值取决于它的斑点颜色: 4 | 5 | (define spots-trait 6 | (list (cons (member-name-key get-color) ....) 7 | (cons (member-name-key get-price) 8 | (lambda (get-price %) .... 9 | (class % .... 10 | (define/public (get-price) 11 | .... (get-color) ....)))))) 12 | 13 | 在这种情况下,spots-trait的定义失败,因为get-color是不在get-price混合范围之内。事实上,当特征应用于一个类时依赖于混合程序的顺序,当get-price混合应用于类时get-color方法可能不可获得。因此添加一个(inherit get-color)申明给get-price混合并不解决问题。 14 | 15 | 一种解决方案是要求在像get-price方法中使用(send this get-color)。这种更改是有效的,因为send总是延迟方法查找,直到对方法的调用被求值。然而,延迟查找比直接调用更为昂贵。更糟糕的是,它也延迟检查get-color方法是否存在。 16 | 17 | 第二个,实际上,并且有效的解决方案是改变特征编码。具体来说,我们代表每个方法作为一对混合:一个引入方法,另一个实现它。当一个特征应用于一个类,所有的引入方法混合首先被应用。然后实现方法混合可以使用inherit去直接访问任何引入的方法。 18 | 19 | (define spots-trait 20 | (list (list (local-member-name-key get-color) 21 | (lambda (get-color get-price %) .... 22 | (class % .... 23 | (define/public (get-color) (void)))) 24 | (lambda (get-color get-price %) .... 25 | (class % .... 26 | (define/override (get-color) 'black)))) 27 | (list (local-member-name-key get-price) 28 | (lambda (get-price get-color %) .... 29 | (class % .... 30 | (define/public (get-price) (void)))) 31 | (lambda (get-color get-price %) .... 32 | (class % .... 33 | (inherit get-color) 34 | (define/override (get-price) 35 | .... (get-color) ....)))))) 36 | 37 | 有了这个特性编码, trait-alias添加一个带新名称的新方法,但它不会改变对旧方法的任何引用。 38 | -------------------------------------------------------------------------------- /13.8.3 trait(特征)表: -------------------------------------------------------------------------------- 1 | 13.8.3 trait(特征)表 2 | 3 | 通用特性模式显然对程序员直接使用来说太复杂了,但很容易在trait宏中编译: 4 | 5 | (trait trait-clause ...) 6 | 7 | 在可选项中的ids继承从句在expr方法中直接引用是有效的,并且它们必须提供其它特征或者基础类,其特征被最终应用。 8 | 9 | 使用这个表结合特征操作符,如trait-sum、trait-exclude、trait-alias和trait->mixin,我们可以实现spots-trait和stripes-trait作为所需。 10 | 11 | (define spots-trait 12 | (trait 13 | (define/public (get-color) 'black) 14 | (define/public (get-price) ... (get-color) ...))) 15 | 16 | (define stripes-trait 17 | (trait 18 | (define/public (get-color) 'red))) 19 | 20 | (define spots+stripes-trait 21 | (trait-sum 22 | (trait-exclude (trait-alias spots-trait 23 | get-color get-spots-color) 24 | get-color) 25 | (trait-exclude (trait-alias stripes-trait 26 | get-color get-stripes-color) 27 | get-color) 28 | (trait 29 | (inherit get-spots-color get-stripes-color) 30 | (define/public (get-color) 31 | .... (get-spots-color) .... (get-stripes-color) ....)))) 32 | -------------------------------------------------------------------------------- /13.9 类合约: -------------------------------------------------------------------------------- 1 | 13.9 类合约 2 | 3 | 由于类是值,它们可以跨越合约边界,我们可能希望用合约保护给定类的一部分。为此,使用class/c表。class/C表具有许多子表,其描述关于字段和方法两种类型的合约:有些通过实例化对象影响使用,有些影响子类。 4 | -------------------------------------------------------------------------------- /14 单元(部件): -------------------------------------------------------------------------------- 1 | 14 单元(部件) 2 | 3 | 单元组织程序分成独立的编译和可重用的组件(component)。一个单元类似于过程,因为这两个都是用于抽象的一级值。虽然程序对表达式中的值进行抽象,但在集合定义中对名称进行抽象。正如一个过程被调用来对它的表达式求值,表达式把实际的参数作为给它的正式参数,一个单元被调用来对它的定义求值,这个定义给出其导入变量的实际引用。但是,与过程不同的是,在调用之前,一个单元的导入变量可以部分地与另一个单元的导出变量链接。链接将多个单元合并成单个复合单元。复合单元本身导入将传播到链接单元中未解决的导入变量的变量,并从链接单元中重新导出一些变量以进一步链接。 4 | 5 | 14.1 签名和单元 6 | 14.2 调用单元 7 | 14.3 连接单元 8 | 14.4 一级单元 9 | 14.5 全-module签名和单元 10 | 14.6 单元合约 11 | 14.6.1 给签名添加合约 12 | 14.6.2 给单元添加合约 13 | 14.7 unit与module的比较 14 | -------------------------------------------------------------------------------- /14.1 签名和单元: -------------------------------------------------------------------------------- 1 | 14.1 签名和单元 2 | 3 | 单元的接口用签名(signature)来描述。每个签名都使用define-signature定义(通常在模块中)。例如,下面的签名,放在一个“toy-factory-sig.rkt”的文件中,描述了一个组件的导出(export),它实现了一个玩具厂: 4 | 5 | "toy-factory-sig.rkt" 6 | 7 | #lang racket 8 | 9 | (define-signature toy-factory^ 10 | (build-toys ; (integer? -> (listof toy?)) 11 | repaint ; (toy? symbol? -> toy?) 12 | toy? ; (any/c -> boolean?) 13 | toy-color)) ; (toy? -> symbol?) 14 | 15 | (provide toy-factory^) 16 | 17 | 一个toy-factory^签名的实现是用define-unit来写的,它定义了一个名为toy-factory^的导出从句: 18 | 19 | "simple-factory-unit.rkt" 20 | 21 | #lang racket 22 | 23 | (require "toy-factory-sig.rkt") 24 | 25 | (define-unit simple-factory@ 26 | (import) 27 | (export toy-factory^) 28 | 29 | (printf "Factory started.\n") 30 | 31 | (define-struct toy (color) #:transparent) 32 | 33 | (define (build-toys n) 34 | (for/list ([i (in-range n)]) 35 | (make-toy 'blue))) 36 | 37 | (define (repaint t col) 38 | (make-toy col))) 39 | 40 | (provide simple-factory@) 41 | 42 | toy-factory^签名也可以被一个单元引用,它需要一个玩具工厂来实施其它某些东西。在这种情况下,toy-factory^将以导入(import)从句命名。例如,玩具店可以从玩具厂买到玩具。(假设为了一个有趣的例子,商店只愿意出售特定颜色的玩具)。 43 | 44 | "toy-store-sig.rkt" 45 | 46 | #lang racket 47 | 48 | (define-signature toy-store^ 49 | (store-color ; (-> symbol?) 50 | stock! ; (integer? -> void?) 51 | get-inventory)) ; (-> (listof toy?)) 52 | 53 | (provide toy-store^) 54 | "toy-store-unit.rkt" 55 | 56 | #lang racket 57 | 58 | (require "toy-store-sig.rkt" 59 | "toy-factory-sig.rkt") 60 | 61 | (define-unit toy-store@ 62 | (import toy-factory^) 63 | (export toy-store^) 64 | 65 | (define inventory null) 66 | 67 | (define (store-color) 'green) 68 | 69 | (define (maybe-repaint t) 70 | (if (eq? (toy-color t) (store-color)) 71 | t 72 | (repaint t (store-color)))) 73 | 74 | (define (stock! n) 75 | (set! inventory 76 | (append inventory 77 | (map maybe-repaint 78 | (build-toys n))))) 79 | 80 | (define (get-inventory) inventory)) 81 | 82 | (provide toy-store@) 83 | 84 | 请注意,“toy-store-unit.rkt“输入”toy-factory-sig.rkt”,而不是“simple-factory-unit.rkt”。因此,toy-store@单元只依赖于玩具工厂的规格,而不是具体的实施。 85 | -------------------------------------------------------------------------------- /14.2 调用单元: -------------------------------------------------------------------------------- 1 | 14.2 调用单元 2 | 3 | simple-factory@单元没有导入,因此可以使用invoke-unit直接调用它: 4 | 5 | > (require "simple-factory-unit.rkt") 6 | > (invoke-unit simple-factory@) 7 | Factory started. 8 | 9 | 但是,invoke-unit表并不能使主体定义可用,因此我们不能在这家工厂制造任何玩具。define-values/invoke-unit表将签名的标识符绑定到实现签名的一个单元(要调用的)提供的值: 10 | 11 | > (define-values/invoke-unit/infer simple-factory@) 12 | Factory started. 13 | 14 | > (build-toys 3) 15 | (list (toy 'blue) (toy 'blue) (toy 'blue)) 16 | 17 | 由于simple-factory@导出toy-factory^签名,toy-factory^的每个标识符都是由define-values/invoke-unit/infer表定义的。表名称的/infer部分表明,由声明约束的标识符是从simple-factory@推断出来的。 18 | 19 | 在定义toy-factory^的标识后,我们还可以调用toy-store@,它导入toy-factory^以产生toy-store^: 20 | 21 | > (require "toy-store-unit.rkt") 22 | > (define-values/invoke-unit/infer toy-store@) 23 | > (get-inventory) 24 | '() 25 | 26 | > (stock! 2) 27 | > (get-inventory) 28 | (list (toy 'green) (toy 'green)) 29 | 30 | 同样,/infer部分define-values/invoke-unit/infer确定toy-store@导入toy-factory^,因此它提供与toy-factory^中的名称匹配的顶级绑定,如导入toy-store@。 31 | -------------------------------------------------------------------------------- /14.3 链接单元: -------------------------------------------------------------------------------- 1 | 14.3 链接单元 2 | 3 | 我们可以借助玩具工厂的合作使我们的玩具店玩具经济性更有效,不需要重新创建。相反,玩具总是使用商店的颜色来制造,而工厂的颜色是通过导入toy-store^来获得的: 4 | 5 | "store-specific-factory-unit.rkt" 6 | 7 | #lang racket 8 | 9 | (require "toy-store-sig.rkt" 10 | "toy-factory-sig.rkt") 11 | 12 | (define-unit store-specific-factory@ 13 | (import toy-store^) 14 | (export toy-factory^) 15 | 16 | (define-struct toy () #:transparent) 17 | 18 | (define (toy-color t) (store-color)) 19 | 20 | (define (build-toys n) 21 | (for/list ([i (in-range n)]) 22 | (make-toy))) 23 | 24 | (define (repaint t col) 25 | (error "cannot repaint"))) 26 | 27 | (provide store-specific-factory@) 28 | 29 | 要调用store-specific-factory@,我们需要toy-store^绑定供及给单元。但是为了通过调用toy-store@来获得toy-store^的绑定,我们需要一个玩具工厂!单元实现是相互依赖的,我们不能在另一个之前调用那一个。 30 | 31 | 解决方案是将这些单元链接(link)在一起,然后调用组合单元。define-compound-unit/infer表将任意数量的单元链接成一个组合单元。它可以从相连的单元中进行导入和导出,并利用其它链接单元的导出来满足各单元的导入。 32 | 33 | > (require "toy-factory-sig.rkt") 34 | > (require "toy-store-sig.rkt") 35 | > (require "store-specific-factory-unit.rkt") 36 | > (define-compound-unit/infer toy-store+factory@ 37 | (import) 38 | (export toy-factory^ toy-store^) 39 | (link store-specific-factory@ 40 | toy-store@)) 41 | 42 | 上边总的结果是一个单元toy-store+factory@,其导出既是toy-factory^也是toy-store^。从每个导入和导出的签名中推断出store-specific-factory@和toy-store@之间的联系。 43 | 44 | 这个单元没有导入,所以我们可以随时调用它: 45 | 46 | > (define-values/invoke-unit/infer toy-store+factory@) 47 | > (stock! 2) 48 | > (get-inventory) 49 | (list (toy) (toy)) 50 | 51 | > (map toy-color (get-inventory)) 52 | '(green green) 53 | -------------------------------------------------------------------------------- /14.5 完整的-module签名和单元: -------------------------------------------------------------------------------- 1 | 14.5 完整的-module签名和单元 2 | 3 | 在程序中使用的单元,模块如“toy-factory-sig.rkt”和“simple-factory-unit.rkt”是常见的。racket/signature和racket/unit模块的名称可以作为语言来避免大量的样板模块、签名和单元申明文本。 4 | 5 | 例如,“toy-factory-sig.rkt”可以写为 6 | 7 | #lang racket/signature 8 | 9 | build-toys ; (integer? -> (listof toy?)) 10 | repaint ; (toy? symbol? -> toy?) 11 | toy? ; (any/c -> boolean?) 12 | toy-color ; (toy? -> symbol?) 13 | 14 | 签名toy-factory^是自动从模块中提供的,它通过用^从文件名“toy-factory-sig.rkt”置换“-sig.rkt”后缀来推断。 15 | 16 | 同样,“simple-factory-unit.rkt”模块可以写为 17 | 18 | #lang racket/unit 19 | 20 | (require "toy-factory-sig.rkt") 21 | 22 | (import) 23 | (export toy-factory^) 24 | 25 | (printf "Factory started.\n") 26 | 27 | (define-struct toy (color) #:transparent) 28 | 29 | (define (build-toys n) 30 | (for/list ([i (in-range n)]) 31 | (make-toy 'blue))) 32 | 33 | (define (repaint t col) 34 | (make-toy col)) 35 | 36 | 单元simple-factory@是自动从模块中提供,它通过用@从文件名“simple-factory-unit.rkt”置换“-unit.rkt“后缀来推断。 37 | -------------------------------------------------------------------------------- /14.6 单元合约: -------------------------------------------------------------------------------- 1 | 14.6 单元合约 2 | 3 | 有两种用合约保护单元的方法。一种方法在编写新的签名时是有用的,另一种方法当一个单元必须符合已经存在的签名时就可以处理这种情况。 4 | -------------------------------------------------------------------------------- /14.6.1 给签名添加合约: -------------------------------------------------------------------------------- 1 | 14.6.1 给签名添加合约 2 | 3 | 当合约添加到签名时,实现该签名的所有单元都受到这些合约的保护。toy-factory^签名的以下版本添加了前面说明中写过的合约: 4 | 5 | "contracted-toy-factory-sig.rkt" 6 | 7 | #lang racket 8 | 9 | (define-signature contracted-toy-factory^ 10 | ((contracted 11 | [build-toys (-> integer? (listof toy?))] 12 | [repaint (-> toy? symbol? toy?)] 13 | [toy? (-> any/c boolean?)] 14 | [toy-color (-> toy? symbol?)]))) 15 | 16 | (provide contracted-toy-factory^) 17 | 18 | 现在我们采用以前实现的simple-factory@,并实现toy-factory^的这个版本来代替: 19 | 20 | "contracted-simple-factory-unit.rkt" 21 | 22 | #lang racket 23 | 24 | (require "contracted-toy-factory-sig.rkt") 25 | 26 | (define-unit contracted-simple-factory@ 27 | (import) 28 | (export contracted-toy-factory^) 29 | 30 | (printf "Factory started.\n") 31 | 32 | (define-struct toy (color) #:transparent) 33 | 34 | (define (build-toys n) 35 | (for/list ([i (in-range n)]) 36 | (make-toy 'blue))) 37 | 38 | (define (repaint t col) 39 | (make-toy col))) 40 | 41 | (provide contracted-simple-factory@) 42 | 43 | 和以前一样,我们可以调用我们的新单元并绑定导出,这样我们就可以使用它们。然而这次,滥用导出引起相应的合约错误。 44 | 45 | > (require "contracted-simple-factory-unit.rkt") 46 | > (define-values/invoke-unit/infer contracted-simple-factory@) 47 | Factory started. 48 | 49 | > (build-toys 3) 50 | (list (toy 'blue) (toy 'blue) (toy 'blue)) 51 | 52 | > (build-toys #f) 53 | build-toys: contract violation 54 | 55 | expected: integer? 56 | 57 | given: #f 58 | 59 | in: the 1st argument of 60 | 61 | (-> integer? (listof toy?)) 62 | 63 | contract from: 64 | 65 | (unit contracted-simple-factory@) 66 | 67 | blaming: top-level 68 | 69 | (assuming the contract is correct) 70 | 71 | at: eval:34.0 72 | 73 | > (repaint 3 'blue) 74 | repaint: contract violation 75 | 76 | expected: toy? 77 | 78 | given: 3 79 | 80 | in: the 1st argument of 81 | 82 | (-> toy? symbol? toy?) 83 | 84 | contract from: 85 | 86 | (unit contracted-simple-factory@) 87 | 88 | blaming: top-level 89 | 90 | (assuming the contract is correct) 91 | 92 | at: eval:34.0 93 | -------------------------------------------------------------------------------- /14.7 unit(单元)与module(模块)的比较: -------------------------------------------------------------------------------- 1 | 14.7 unit(单元)与module(模块)的比较 2 | 3 | 作为模块的一个表,unit是对module的补充: 4 | 5 | 1、module表主要用于管理通用命名空间。例如,它允许一个代码片段是专指来自racket/base的car运算——其中一个提取内置配对数据类型的一个实例的第一个元素——而不是任何其它带car名字的函数。换句话说,module构造允许你引用你想要的绑定。 6 | 7 | 2、unit表是参数化的代码片段相对于大多数运行时的值的任意种类。例如,它允许一个代码片段与一个接受单个参数的car函数一起工作,其中特定函数在稍后通过将片段连接到另一个参数被确定。换句话说,unit结构允许你引用满足某些规范的一个绑定。 8 | 9 | 除其他外,lambda和class表还允许对稍后选择的值进行代码参数化。原则上,其中任何一项都可以以其他任何方式执行。在实践中,每个表都提供了某些便利——例如允许重写方法或者特别是对值的特别简单的应用——使它们适合不同的目的。 10 | 11 | 从某种意义上说,module表比其它表更为基础。毕竟,没有module提供的命名空间管理,程序片段不能可靠地引用lambda、class或unit表。同时,由于名称空间管理与单独的扩展和编译密切相关,module边界以独立的编译边界结束,在某种程度上阻止了片段之间的相互依赖关系。出于类似的原因,module不将接口与实现分开。 12 | 13 | 使用unit的情况为,在module本身几乎可以运行时,但当独立编译的部分必须相互引用时,或当你想要在接口(interface)(即,需要在扩展和编译时间被知道的部分)和实现(implementation)(即,运行时部分)之间有一个更强健的隔离时。更普遍使用unit的情况是,当你需要在函数、数据类型和类上参数化代码时,以及当参数代码本身提供定义以和其它参数代码链接时。 14 | -------------------------------------------------------------------------------- /15 反射和动态求值: -------------------------------------------------------------------------------- 1 | 15 反射和动态求值 2 | 3 | Racket是一个动态的语言。它提供了许多用于加载、编译、甚至在运行时构造新代码的工具。 4 | 5 | 15.1 eval 6 | 15.1.1 本地域 7 | 15.1.2 命名空间 8 | 15.1.3 命名空间和模块 9 | 15.2 操纵的命名空间 10 | 15.2.1 创建和安装命名空间 11 | 15.2.2 共享数据和代码的命名空间 12 | 15.3 脚本求值和使用load 13 | -------------------------------------------------------------------------------- /15.1 eval: -------------------------------------------------------------------------------- 1 | 15.1 eval 2 | 3 | eval函数构成一个表达或定义的表达(如“引用(quoted)”表或句法对象(syntax object))并且对它进行求值: 4 | 5 | > (eval '(+ 1 2)) 6 | 3 7 | 8 | eval函数的表达式可以动态构造: 9 | 10 | > (define (eval-formula formula) 11 | (eval `(let ([x 2] 12 | [y 3]) 13 | ,formula))) 14 | > (eval-formula '(+ x y)) 15 | 5 16 | 17 | > (eval-formula '(+ (* x y) y)) 18 | 9 19 | 20 | 当然,如果我们只是想计算表达式给出x和y的值,我们不需要eval。更直接的方法是使用一级函数: 21 | 22 | > (define (apply-formula formula-proc) 23 | (formula-proc 2 3)) 24 | > (apply-formula (lambda (x y) (+ x y))) 25 | 5 26 | 27 | > (apply-formula (lambda (x y) (+ (* x y) y))) 28 | 9 29 | 30 | 然而,譬如,如果表达式样(+ x y)和(+ (* x y) y)是从用户提供的文件中读取,然后eval可能是适当的。同样地,REPL读取表达式,由用户输入,使用eval求值。 31 | 32 | 一样地,在整个模块中eval往往直接或间接地使用。例如,程序可以在定义域中用dynamic-require读取一个模块,这基本上是一个封装在eval中的动态加载模块的代码。 33 | -------------------------------------------------------------------------------- /15.1.1 本地域: -------------------------------------------------------------------------------- 1 | 15.1.1 本地域 2 | 3 | eval函数不能看到上下文中被调用的局部绑定。例如,调用在一个非引用的let表中的eval以对一个公式求值不会使得值x和y可见: 4 | 5 | > (define (broken-eval-formula formula) 6 | (let ([x 2] 7 | [y 3]) 8 | (eval formula))) 9 | > (broken-eval-formula '(+ x y)) 10 | x: undefined; 11 | 12 | cannot reference undefined identifier 13 | 14 | eval函数不能看到X和Y的绑定,正是因为它是一个函数,并且Racket是词法作用域的语言。想象一下如果eval被实现为 15 | 16 | (define (eval x) 17 | (eval-expanded (macro-expand x))) 18 | 19 | 那么在eval-expanded被调用的这个点上,X最近的绑定是表达式求值,不是broken-eval-formula中的let绑定。词法范围防止这样的困惑和脆弱的行为,从而防止eval表看到上下文中被调用的局部绑定。 20 | 21 | 你可以想象,即使在broken-eval-formula不能看到局部绑定,这里实际上必须是一个X到2和Y到3的数据结构映射,以及你想办法得到那些数据结构。事实上,没有这样的数据结构存在;编译器可以自由地在编译时替换带有2的x的每一个使用,因此在运行时的任何具体意义上都不存在x的局部绑定。即使变量不能通过常量折叠消除,通常也可以消除变量的名称,而保存局部值的数据结构与从名称到值的映射不一样。 22 | -------------------------------------------------------------------------------- /15.1.2 命名空间(Namespace): -------------------------------------------------------------------------------- 1 | 15.1.2 命名空间(Namespace) 2 | 3 | 由于eval不能从它调用的上下文中看到绑定,另一种机制是需要确定动态可获得的绑定。一个命名空间(namespace)是一个一级的值,它封装了用于动态求值的可获得绑定。 4 | 5 | 一些函数,如eval,接受一个可选的命名空间参数。通常,动态操作所使用的命名空间是current-namespace参数所确定的当前命名空间(current namespace)。 6 | 7 | 当eval在REPL中使用时,当前命名空间是REPL使用于求值表达式中的一个。这就是为什么下面的互动设计成功通过eval访问X的原因: 8 | 9 | > (define x 3) 10 | > (eval 'x) 11 | 3 12 | 13 | 相反,尝试以下简单的模块并直接在DrRacket里或提供文件作为命令行参数给racket运行它: 14 | 15 | #lang racket 16 | 17 | (eval '(cons 1 2)) 18 | 19 | 这失败是因为初始当前命名空间是空的。当你在交互模式下运行racket(见《交互模式(Interactive Mode)》)时,初始的命名空间是用racket模块的导出初始化的,但是当你直接运行一个模块时,初始的命名空间开始为空。 20 | 21 | 在一般情况下,用任何命名空间安装结果来使用eval一个坏主意。相反,明确地创建一个命名空间并安装它以调用eval: 22 | 23 | #lang racket 24 | 25 | (define ns (make-base-namespace)) 26 | (eval '(cons 1 2) ns) ; 运行 27 | 28 | make-base-namespace函数创建一个命名空间,该命名空间是用racket/base导出初始化的。后一部分《操作命名空间(Manipulating Namespaces)》提供了关于创建和配置名称空间的更多信息。 29 | -------------------------------------------------------------------------------- /15.1.3 命名空间和模块: -------------------------------------------------------------------------------- 1 | 15.1.3 命名空间和模块 2 | 3 | 为let绑定,词法范围意味着eval不能自动看到一个调用它的module(模块)的定义。然而,和let绑定不同的是,Racket提供了一种将模块反射到一个namespace(命名空间)的方法。 4 | 5 | module->namespace函数接受一个引用的模块路径(module path),并生成一个命名空间,用于对表达式和定义求值,就像它们出现在module主体中一样: 6 | 7 | > (module m racket/base 8 | (define x 11)) 9 | > (require 'm) 10 | > (define ns (module->namespace ''m)) 11 | > (eval 'x ns) 12 | 11 13 | 14 | module->namespace函数对来自于模块之外的模块是最有用的,在这里模块的全名是已知的。然而,在module表内,模块的全名可能不知道,因为它可能取决于在最终加载时模块源位于何处。 15 | 16 | 在module内,使用define-namespace-anchor声明模块上的反射钩子,并使用namespace-anchor->namespace在模块的命名空间中滚动: 17 | 18 | #lang racket 19 | 20 | (define-namespace-anchor a) 21 | (define ns (namespace-anchor->namespace a)) 22 | 23 | (define x 1) 24 | (define y 2) 25 | 26 | (eval '(cons x y) ns) ; produces (1 . 2) 27 | -------------------------------------------------------------------------------- /15.2 操纵的命名空间: -------------------------------------------------------------------------------- 1 | 15.2 操纵的命名空间 2 | 3 | 命名空间封装两条信息: 4 | 5 | 1、从标识符到绑定的映射。例如,一个命名空间可以将标识符lambda映射到lambda表。一个“空”的命名空间是一个映射之一,它映射每个标识符到一个未初始化的顶层变量。 6 | 7 | 2、从模块名称到模块声明和实例的映射。 8 | 9 | 第一个映射是用于对在一个顶层上下文中的表达式求值,如(eval '(lambda (x) (+ x 1)))中的。第二个映射是用于定位模块,例如通过dynamic-require。对(eval '(require racket/base))的调用通常使用两部分:标识符映射确定require的绑定;如果它原来的意思是require,那么模块映射用于定位racket/base模块。 10 | 11 | 从核心Racket运行系统的角度来看,所有求值都是反射性的。执行从初始的命名空间包含一些原始的模块,并进一步由命令行上或在REPL提供指定加载的文件和模块。顶层require表和define表调整标识符映射,模块声明(通常根据require表加载)调整模块映射。 12 | -------------------------------------------------------------------------------- /15.2.1 创建和安装命名空间: -------------------------------------------------------------------------------- 1 | 15.2.1 创建和安装命名空间 2 | 3 | 函数make-empty-namespace创建一个新的空命名空间。由于命名空间确实是空的,所以它不能首先用来求值任何顶级表达式——甚至不能求值(require racket)。特别地, 4 | 5 | (parameterize ([current-namespace (make-empty-namespace)]) 6 | (namespace-require 'racket)) 7 | 8 | 失败,因为命名空间不包括建立racket的原始模块。 9 | 10 | 为了使命名空间有用,必须从现有命名空间中附加(attached)一些模块。附加模块通过从现有的命名空间的映射传递复制条目(模块及它的所有导入)调整模块名称映射到实例。通常情况下,而不是仅仅附加原始模块——其名称和组织有可能发生变化——附加一个高级模块,如racket或racket/base。 11 | 12 | make-base-empty-namespace函数提供一个空的命名空间,除非附加了racket/base。生成的命名空间仍然是“空的”,在这个意义上,绑定名称空间部分的标识符没有映射;只有模块映射已经填充。然而,通过初始模块映射,可以加载更多模块。 13 | 14 | 一个用make-base-empty-namespace创建的命名空间适合于许多基本的动态任务。例如,假设my-dsl库实现了一个特定定义域的语言,你希望在其中执行来自用户指定文件的命令。一个用make-base-empty-namespace的命名空间足以启动: 15 | 16 | (define (run-dsl file) 17 | (parameterize ([current-namespace (make-base-empty-namespace)]) 18 | (namespace-require 'my-dsl) 19 | (load file))) 20 | 21 | 注意,current-namespace参数不影响像在parameterize主体中的namespace-require那样的标识符的意义。这些标识符从封闭上下文(可能是一个模块)获得它们的含义。只有对代码具有动态性的表达式,如加载(load)的文件的内容,通过参数化(parameterize)影响。 22 | 23 | 在上面的例子中,一个微妙的一点是使用(namespace-require 'my-dsl)代替(eval '(require my-dsl))。后者不会运行,因为eval需要对在命名空间中的require获得意义,并且命名空间的标识符映射最初是空的。与此相反,namespace-require函数直接将给定的模块导入当前命名空间。从(namespace-require 'racket/base)将对导入绑定并使后续(eval '(require my-dsl))运行。上面的比较好,不仅仅是因为它更紧凑,还因为它避免引入不属于特定领域语言的绑定。 24 | -------------------------------------------------------------------------------- /15.2.2 共享数据和代码的命名空间: -------------------------------------------------------------------------------- 1 | 15.2.2 共享数据和代码的命名空间 2 | 3 | 如果不需要对新命名空间附加的模块,则将重新加载并实例化它们。例如,racket/base不包括racket/class,加载racket/class又将创造一个不同的类数据类型: 4 | 5 | > (require racket/class) 6 | > (class? object%) 7 | #t 8 | 9 | > (class? 10 | (parameterize ([current-namespace (make-base-empty-namespace)]) 11 | (namespace-require 'racket/class) ; loads again 12 | (eval 'object%))) 13 | #f 14 | 15 | 对于动态加载的代码需要与其上下文共享更多代码和数据的,使用namespace-attach-module函数。 namespace-attach-module的第一个参数是从中提取模块实例的源命名空间;在某些情况下,已知的当前命名空间包含需要共享的模块: 16 | 17 | > (require racket/class) 18 | > (class? 19 | (let ([ns (make-base-empty-namespace)]) 20 | (namespace-attach-module (current-namespace) 21 | 'racket/class 22 | ns) 23 | (parameterize ([current-namespace ns]) 24 | (namespace-require 'racket/class) ; uses attached 25 | (eval 'object%)))) 26 | #t 27 | 28 | 然而,在一个模块中,define-namespace-anchor和namespace-anchor->empty-namespace的组合提供了一种更可靠的获取源命名空间的方法: 29 | 30 | #lang racket/base 31 | 32 | (require racket/class) 33 | 34 | (define-namespace-anchor a) 35 | 36 | (define (load-plug-in file) 37 | (let ([ns (make-base-empty-namespace)]) 38 | (namespace-attach-module (namespace-anchor->empty-namespace a) 39 | 'racket/class 40 | ns) 41 | (parameterize ([current-namespace ns]) 42 | (dynamic-require file 'plug-in%)))) 43 | 44 | 由namespace-attach-module绑定的锚将模块的运行时间与加载模块的命名空间(可能与当前命名空间不同)连接在一起。在上面的示例中,由于封闭模块需要racket/class,由namespace-anchor->empty-namespace生成的名称空间肯定包含了一个racket/class的实例。此外,该实例与一个导入模块的一个相同,因此类数据类型共享。 45 | -------------------------------------------------------------------------------- /15.3 脚本求值和使用load: -------------------------------------------------------------------------------- 1 | 15.3 脚本求值和使用load 2 | 3 | 从历史上看,Lisp实现没有提供模块系统。相反,大的程序是由基本的脚本REPL来求值一个特定的顺序的程序片段。而REPL脚本是结构化程序和库的好办法,它仍然有时是一个有用的性能。 4 | 5 | load函数通过从文件中一个接一个地读取S表达式来运行一个REPL脚本,并把它们传递给eval。如果一个文件"place.rkts"包含以下内容 6 | 7 | (define city "Salt Lake City") 8 | (define state "Utah") 9 | (printf "~a, ~a\n" city state) 10 | 11 | 那么,它可以加载进一个REPL: 12 | 13 | > (load "place.rkts") 14 | Salt Lake City, Utah 15 | 16 | > city 17 | "Salt Lake City" 18 | 19 | 然而,由于load使用eval,像下面的一个模块一般不会运行——基于命名空间中的相同原因描述: 20 | 21 | #lang racket 22 | 23 | (define there "Utopia") 24 | 25 | (load "here.rkts") 26 | 27 | 对求值"here.rkts"的上下文的当前命名空间可能是空的;在任何情况下,你不能从"here.rkts"到那里。同时,在“"here.rkt"里的任何定义对模块里的使用不会变得可见;毕竟,load是动态发生,而在模块标识符引用是从词法上解决,因此是静态的。 28 | 29 | 不像eval,load不接受一个命名空间的参数。为了提供一个用于load的命名空间,设置current-namespace参数。下面的示例求值在"here.rkts"中使用racket/base模块绑定的表达式: 30 | 31 | #lang racket 32 | 33 | (parameterize ([current-namespace (make-base-namespace)]) 34 | (load "here.rkts")) 35 | 36 | 您甚至可以使用namespace-anchor->namespace使封闭模块的绑定可用于动态求值。在下面的例子中,当“"here.rkts"被加载时,它既可以指racket,也可以指racket的绑定: 37 | 38 | #lang racket 39 | 40 | (define there "Utopia") 41 | 42 | (define-namespace-anchor a) 43 | (parameterize ([current-namespace (namespace-anchor->namespace a)]) 44 | (load "here.rkts")) 45 | 46 | 不过,如果"here.rkts"定义任意的标识符,这个定义不能直接(即静态地)在外围模块中引用。 47 | 48 | racket/load模块语言不同于racket或racket/base。一个模块使用racket/load对其所有上下文以动态对待,通过模块主体里的每一个表去eval(使用以racket初始化的命名空间)。作为一个结果,eval和load在模块中的使用看到相同的动态命名空间作为直接主体表。例如,如果"here.rkts"包含以下内容 49 | 50 | (define here "Morporkia") 51 | (define (go!) (set! here there)) 52 | 53 | 那么运行 54 | 55 | #lang racket/load 56 | 57 | (define there "Utopia") 58 | 59 | (load "here.rkts") 60 | 61 | (go!) 62 | (printf "~a\n" here) 63 | 64 | 打印“Utopia”。 65 | 66 | 使用racket/load的缺点包括减少错误检查、工具支持和性能。例如,用程序 67 | 68 | #lang racket/load 69 | 70 | (define good 5) 71 | (printf "running\n") 72 | good 73 | bad 74 | 75 | DrRacket的语法检查(Check Syntax)工具不能告诉第二个good是对第一个的参考,而对bad的非绑定参考仅在运行时报告而不是在语法上拒绝。 76 | -------------------------------------------------------------------------------- /16 宏(Macro): -------------------------------------------------------------------------------- 1 | 16 宏(Macro) 2 | 3 | 宏(macro)是一种语法表,它有一个关联的转换器(transformer),它将原有的表扩展(expand)为现有的表。换句话说,宏是Racket编译器的扩展。racket/base和racket的大部分句法表实际上是宏,扩展成一小部分核心结构。 4 | 5 | 像许多语言一样,Racket提供基于模式的宏,使得简单的转换易于实现和可靠使用。Racket还支持任意的宏转换器,它在Racket中实现,或在Racket中的宏扩展变体中实现。 6 | 7 | (对于自下而上的Racket宏的介绍,你可以参考:《宏的敬畏(Fear of Macros)》) 8 | 9 | 16.1 基于模式的宏 10 | 16.1.1 define-syntax-rule 11 | 16.1.2 词法范围 12 | 16.1.3 define-syntax和syntax-rules 13 | 16.1.4 匹配序列 14 | 16.1.5 标识符宏 15 | 16.1.6 set!转化器 16 | 16.1.7 宏的宏生成 17 | 16.1.8 扩展的例子:函数的参考调用 18 | 16.2 通用宏转化器 19 | 16.2.1 语法对象 20 | 16.2.2 宏转化器程序 21 | 16.2.3 混合模式和表达式:syntax-case 22 | 16.2.4 with-syntax和generate-temporaries 23 | 16.2.5 编译和运行时相 24 | 16.2.6 通用相层级 25 | 16.2.6.1 相绑定 26 | 16.2.6.2 相和模块 27 | 16.2.7 语法污染 28 | -------------------------------------------------------------------------------- /16.1 基于模式的宏: -------------------------------------------------------------------------------- 1 | 16.1 基于模式的宏 2 | 3 | 基于模式的宏(pattern-based macro)将任何与模式匹配的代码替换为使用与模式部分匹配的原始语法的一部分的扩展。 4 | -------------------------------------------------------------------------------- /16.1.1 define-syntax-rule: -------------------------------------------------------------------------------- 1 | 16.1.1 define-syntax-rule 2 | 3 | 创建宏的最简单方法是使用define-syntax-rule: 4 | 5 | (define-syntax-rule pattern template) 6 | 7 | 作为一个运行的例子,考虑交换宏(swap macro),它将交换值存储在两个变量中。可以使用define-syntax-rule实现如下: 8 | 9 | (define-syntax-rule (swap x y) 10 | (let ([tmp x]) 11 | (set! x y) 12 | (set! y tmp))) 13 | 14 | define-syntax-rule表绑定一个与单个模式匹配的宏。模式必须总是以一个开放的括号开头,后面跟着一个标识符,这个标识符在这个例子中是交换的。在初始标识符之后,其它标识符是宏模式变量(macro pattern variable),可以匹配宏使用中的任何内容。因此,这个宏匹配这个表((swap form1 form2)给任何form1和form2。 15 | 16 | 在define-syntax-rule中的模式之后是摸板(template)。模板用于替代与模式匹配的表,但模板中的模式变量的每个实例都替换为宏使用模式变量匹配的部分。例如,在 17 | 18 | (swap first last) 19 | 20 | 模式变量x匹配first及y匹配last,于是扩展是 21 | 22 | (let ([tmp first]) 23 | (set! first last) 24 | (set! last tmp)) 25 | -------------------------------------------------------------------------------- /16.1.2 词法范围: -------------------------------------------------------------------------------- 1 | 16.1.2 词法范围 2 | 3 | 假设我们使用swap宏来交换名为tmp和other的变量: 4 | 5 | (let ([tmp 5] 6 | [other 6]) 7 | (swap tmp other) 8 | (list tmp other)) 9 | 10 | 上述表达式的结果应为(6 5)。然而,这种swap的使用的单纯扩展是 11 | 12 | (let ([tmp 5] 13 | [other 6]) 14 | (let ([tmp tmp]) 15 | (set! tmp other) 16 | (set! other tmp)) 17 | (list tmp other)) 18 | 19 | 其结果是(5 6)。问题在于,这个单纯的扩展混淆了上下文中的tmp,那里swap与宏摸板中的tmp被使用。 20 | 21 | Racket不会为了swap的上述使用生成单纯的扩展。相反,它会生成以下内容 22 | 23 | (let ([tmp 5] 24 | [other 6]) 25 | (let ([tmp_1 tmp]) 26 | (set! tmp other) 27 | (set! other tmp_1)) 28 | (list tmp other)) 29 | 30 | 正确的结果在(6 5)。同样,在示例中 31 | 32 | (let ([set! 5] 33 | [other 6]) 34 | (swap set! other) 35 | (list set! other)) 36 | 37 | 其扩展是 38 | 39 | (let ([set!_1 5] 40 | [other 6]) 41 | (let ([tmp_1 set!_1]) 42 | (set! set!_1 other) 43 | (set! other tmp_1)) 44 | (list set!_1 other)) 45 | 46 | 因此局部set!绑定不会干扰宏模板引入的赋值。 47 | 48 | 换句话说,Racket的基于模式的宏自动维护词法范围,所以宏的实现者可以思考宏中的变量引用以及在同样的途径中作为函数和函数调用的宏使用。 49 | -------------------------------------------------------------------------------- /16.1.3 define-syntax和syntax-rules: -------------------------------------------------------------------------------- 1 | 16.1.3 define-syntax和syntax-rules 2 | 3 | define-syntax-rule表绑定一个与单一模式匹配的宏,但Racket的宏系统支持从同一标识符开始匹配多个模式的转换器。要编写这样的宏,程序员必须使用更通用的define-syntax表以及syntax-rules转换器表: 4 | 5 | (define-syntax id 6 | (syntax-rules (literal-id ...) 7 | [pattern template] 8 | ...)) 9 | 10 | 例如,假设我们希望一个rotate宏将swap概括为两个或三个标识符,因此 11 | 12 | (let ([red 1] [green 2] [blue 3]) 13 | (rotate red green) ; swaps 14 | (rotate red green blue) ; rotates left 15 | (list red green blue)) 16 | 17 | 生成(1 3 2)。我们可以使用syntax-rules实现rotate: 18 | 19 | (define-syntax rotate 20 | (syntax-rules () 21 | [(rotate a b) (swap a b)] 22 | [(rotate a b c) (begin 23 | (swap a b) 24 | (swap b c))])) 25 | 26 | 表达式(rotate red green)与syntax-rules表中的第一个模式相匹配,因此扩展到(swap red green)。表达式(rotate red green blue) 与第二个模式匹配,所以它扩展到(begin (swap red green) (swap green blue))。 27 | -------------------------------------------------------------------------------- /16.1.4 匹配序列: -------------------------------------------------------------------------------- 1 | 16.1.4 匹配序列 2 | 3 | 一个更好的rotate宏将允许任意数量的标识符,而不是只有两个或三个标识符。匹配任何数量的标识符的rotate使用,我们需要一个模式表,它有点像克林闭包(Kleene star)。在一个Racket宏模式中,一个闭包(star)被写成...。 4 | 5 | 为了用...实现rotate,我们需要一个基元(base case)来处理单个标识符,以及一个归纳案例以处理多个标识符: 6 | 7 | (define-syntax rotate 8 | (syntax-rules () 9 | [(rotate a) (void)] 10 | [(rotate a b c ...) (begin 11 | (swap a b) 12 | (rotate b c ...))])) 13 | 14 | 当在一种模式中像c这样的模式变量被…跟着的时候,它在模板中必须也被…跟着。模式变量有效地匹配一个零序列或多个表,并在模板中以相同的顺序被替换。 15 | 16 | 到目前为止,rotate的两种版本都有点效率低下,因为成对交换总是将第一个变量的值移动到序列中的每个变量,直到达到最后一个变量为止。更有效的rotate将第一个值直接移动到最后一个变量。我们可以用...模式使用辅助宏去实现更有效的变体: 17 | 18 | (define-syntax rotate 19 | (syntax-rules () 20 | [(rotate a c ...) 21 | (shift-to (c ... a) (a c ...))])) 22 | 23 | (define-syntax shift-to 24 | (syntax-rules () 25 | [(shift-to (from0 from ...) (to0 to ...)) 26 | (let ([tmp from0]) 27 | (set! to from) ... 28 | (set! to0 tmp))])) 29 | 30 | 在shift-to宏里,在模板里的...后面跟着(set! to from),它导致(set! to from)表达式在in和from序列中与必须使用的每个标识符匹配被复制一样多次。(to和from匹配的数量必须相同,否则宏扩展就会有一个错误的失败。) 31 | -------------------------------------------------------------------------------- /16.1.5 标识符宏: -------------------------------------------------------------------------------- 1 | 16.1.5 标识符宏 2 | 3 | 根据我们的宏定义,swap或rotate标识符必须在开括号之后使用,否则会报告语法错误: 4 | 5 | > (+ swap 3) 6 | eval:2:0: swap: bad syntax 7 | 8 | in: swap 9 | 10 | 标识符宏(identifier macro)是一个模式匹配宏,当它被自己使用时不使用括号。例如,我们可以定义val为一个标识符宏,扩展到(get-val),所以(+ val 3)将扩展到(+ (get-val) 3)。 11 | 12 | > (define-syntax val 13 | (lambda (stx) 14 | (syntax-case stx () 15 | [val (identifier? (syntax val)) (syntax (get-val))]))) 16 | > (define-values (get-val put-val!) 17 | (let ([private-val 0]) 18 | (values (lambda () private-val) 19 | (lambda (v) (set! private-val v))))) 20 | > val 21 | 0 22 | 23 | > (+ val 3) 24 | 3 25 | 26 | val宏使用syntax-case,它可以定义更强大的宏,并在《混合模式和表达式:syntax-case(Mixing Patterns and Expressions: syntax-case)》中讲解。现在,知道定义宏是必要的,在lambda中使用了syntax-case,它的模板必须用显式syntax构造函数包装。最后,syntax-case子句可以指定模式后面的附加保护条件。 27 | 28 | 我们的val宏使用identifier?条件确保在括号中val不能(must not)使用。相反,宏引一个发语法错误: 29 | 30 | > (val) 31 | eval:8:0: val: bad syntax 32 | 33 | in: (val) 34 | -------------------------------------------------------------------------------- /16.1.6 set!转化器: -------------------------------------------------------------------------------- 1 | 16.1.6 set!转化器 2 | 3 | 使用上面的val宏,我们仍然必须调用put-val!更改存储值。然而,直接在val上使用set!会更方便。当val用于set!时借助宏,我们用make-set!-transformer创建一个赋值转换器(assignment transformer)。我们还必须声明set!作为syntax-case文本列表中的文字。 4 | 5 | > (define-syntax val2 6 | (make-set!-transformer 7 | (lambda (stx) 8 | (syntax-case stx (set!) 9 | [val2 (identifier? (syntax val2)) (syntax (get-val))] 10 | [(set! val2 e) (syntax (put-val! e))])))) 11 | > val2 12 | 0 13 | 14 | > (+ val2 3) 15 | 3 16 | 17 | > (set! val2 10) 18 | > val2 19 | 10 20 | -------------------------------------------------------------------------------- /16.1.7 宏的宏生成: -------------------------------------------------------------------------------- 1 | 16.1.7 宏的宏生成 2 | 3 | 假设我们有许多标识符像Val和val2,我们想重定向给访问器和突变函数像get-val和put-val!。我们希望可以只写: 4 | 5 | (define-get/put-id val get-val put-val!) 6 | 7 | 自然地,我们可以实现define-get/put-id为一个宏: 8 | 9 | > (define-syntax-rule (define-get/put-id id get put!) 10 | (define-syntax id 11 | (make-set!-transformer 12 | (lambda (stx) 13 | (syntax-case stx (set!) 14 | [id (identifier? (syntax id)) (syntax (get))] 15 | [(set! id e) (syntax (put! e))]))))) 16 | > (define-get/put-id val3 get-val put-val!) 17 | > (set! val3 11) 18 | > val3 19 | 11 20 | 21 | define-get/put-id宏就是是一个宏生成宏(macro-generating macro)。 22 | -------------------------------------------------------------------------------- /16.2 通用宏转化器: -------------------------------------------------------------------------------- 1 | 16.2 通用宏转化器 2 | 3 | define-syntax表为标识符创建一个转换器绑定(transformer binding),这是一个可以在编译时使用的绑定,同时扩展表达式以在运行时进行求值。与转换器绑定相关联的编译时间值可以是任何东西;如果它是一个参数的过程,则绑定用作宏,而过程是宏转换器(macro transformer)。 4 | 5 | 16.2.1 语法对象 6 | 16.2.2 宏转化器程序 7 | 16.2.3 混合模式和表达式:syntax-case 8 | 16.2.4 with-syntax和generate-temporaries 9 | 16.2.5 编译和运行时相 10 | 16.2.6 通用相层级 11 | 16.2.6.1 相绑定 12 | 16.2.6.2 相和模块 13 | 16.2.7 语法污染 14 | -------------------------------------------------------------------------------- /16.2.1 语法对象: -------------------------------------------------------------------------------- 1 | 16.2.1 语法对象 2 | 3 | 宏转换器(即源和替换表)的输入和输出被表示为语法对象(syntax object)。语法对象包含符号、列表和常量值(如数字),它们基本上与表达式的引用(quote)表相对应。例如,表达式描述为(+ 1 2)包含符号'+和数字1和2,都在列表中。除了引用的内容之外,语法对象还将源位置和词汇绑定信息与表的每个部分关联起来。在报告语法错误时使用源位置信息(例如),词汇绑定信息允许宏系统维护词法范围。为了适应这种额外的信息,表达式描述为(+ 1 2)不仅是'(+ 1 2),但'(+ 1 2)的封装成为了语法对象。 4 | 5 | 要创建文字语法对象,请使用syntax表: 6 | 7 | > (syntax (+ 1 2)) 8 | # 9 | 10 | 在同样的方式,'省略了quote,#'省略了syntax: 11 | 12 | > #'(+ 1 2) 13 | # 14 | 15 | 只包含符号的语法对象是标识符语法对象(identifier syntax object)。它提供了一些特定于标识符语法对象的附加操作,包括identifier?操作以检查操作符。最值得注意的是,free-identifier=?确定两个标识符是否引用相同的绑定: 16 | 17 | > (identifier? #'car) 18 | #t 19 | 20 | > (identifier? #'(+ 1 2)) 21 | #f 22 | 23 | > (free-identifier=? #'car #'cdr) 24 | #f 25 | 26 | > (free-identifier=? #'car #'car) 27 | #t 28 | 29 | > (require (only-in racket/base [car also-car])) 30 | > (free-identifier=? #'car #'also-car) 31 | #t 32 | 33 | 要在语法对象中看到列表、符号、数字等等,请使用syntax->datum: 34 | 35 | > (syntax->datum #'(+ 1 2)) 36 | '(+ 1 2) 37 | 38 | syntax-e函数类似于syntax->datum,但它打开了一个单层的源位置和词汇上下文信息,离开有它们自己的信息作为语法的对象子表: 39 | 40 | > (syntax-e #'(+ 1 2)) 41 | '(# # #) 42 | 43 | syntax-e函数总是放弃语法对象包装器,它包围着子表,子表表示为通过符号、数值,和其它文本值。唯一的一次额外的子表打开时展开一个配对,在这种情况下,配对的cdr可以根据语法构建的对象递归地展开。 44 | 45 | 当然,syntax->datum的相对是datum->syntax。除了像'(+ 1 2)这样的数据外,,datum->syntax还需要一个现有的语法对象来贡献它的词法上下文,并且可以选择另一个语法对象来贡献它的源位置: 46 | 47 | > (datum->syntax #'lex 48 | '(+ 1 2) 49 | #'srcloc) 50 | # 51 | 52 | 在上面的例子中,对#'lex的词法上下文是用于新的语法对象,而#'srcloc源位置被使用。 53 | 54 | 当datum->syntax的第二个(即,“datum”)参数包含语法对象时,这些语法对象将原封不动地保存在结果中。那就是,用syntax-e解构的结果最终产生了给予datum->syntax的这个语法对象。 55 | -------------------------------------------------------------------------------- /16.2.2 宏转化器程序: -------------------------------------------------------------------------------- 1 | 16.2.2 宏转化器程序 2 | 3 | 任何一个参数的过程都可以是一个宏转换器(macro transformer)。事实证明,语法规则(syntax-rules)表是一个扩展为过程表的宏。例如,如果直接求值syntax-rules表(而不是放在define-syntax表的右侧),结果就是一个过程: 4 | 5 | > (syntax-rules () [(nothing) something]) 6 | # 7 | 8 | 可以使用lambda直接编写自己的宏转换器过程,而不是使用syntax-rules。对过程的参数是表示源表的语法对象(syntax object),过程的结果必须是表示替换表的语法对象(syntax object): 9 | 10 | > (define-syntax self-as-string 11 | (lambda (stx) 12 | (datum->syntax stx 13 | (format "~s" (syntax->datum stx))))) 14 | > (self-as-string (+ 1 2)) 15 | "(self-as-string (+ 1 2))" 16 | 17 | 传递给宏转换器的源表表示一个表达式,其中在应用程序位置(即在启动表达式的括号之后)使用其标识符,或者如果它被用作表达式位置而不是应用程序位置,则它本身代表标识符。 18 | 19 | > (self-as-string (+ 1 2)) 20 | "(self-as-string (+ 1 2))" 21 | 22 | > self-as-string 23 | "self-as-string" 24 | 25 | define-syntax表支持与define的函数一样的快捷语法,因此下面的self-as-string定义等同于显式使用lambda的那个定义: 26 | 27 | > (define-syntax (self-as-string stx) 28 | (datum->syntax stx 29 | (format "~s" (syntax->datum stx)))) 30 | > (self-as-string (+ 1 2)) 31 | "(self-as-string (+ 1 2))" 32 | -------------------------------------------------------------------------------- /16.2.3 混合模式和表达式:syntax-case: -------------------------------------------------------------------------------- 1 | 16.2.3 混合模式和表达式:syntax-case 2 | 3 | 通过syntax-rules生成的程序在内部使用syntax-e解构了语法对象,并使用datum->syntax以构造结果。syntax-rules表没有提供一种方法从模式匹配和模板构建模式中跳转到任意的Racket表达式中。 4 | 5 | syntax-case表允许混合模式匹配、模板构造和任意表达式: 6 | 7 | (syntax-case stx-expr (literal-id ...) 8 | [pattern expr] 9 | ...) 10 | 11 | 与syntax-rules不同,syntax-case表不产生过程。相反,它从一个stx-expr表达式决定的语法对象来匹配pattern。另外,每个syntax-case有一个pattern和一个expr,而不是一个pattern和template。在一个expr里,syntax表——通常用#'缩写——进入模板构造方式;如果一个从句的expr以#'开始,那么我们就会获得一些像syntax-rules的表: 12 | 13 | > (syntax->datum 14 | (syntax-case #'(+ 1 2) () 15 | [(op n1 n2) #'(- n1 n2)])) 16 | '(- 1 2) 17 | 18 | 我们可以使用syntax-case来编写swap宏,以代替define-syntax-rule或syntax-rules: 19 | 20 | (define-syntax (swap stx) 21 | (syntax-case stx () 22 | [(swap x y) #'(let ([tmp x]) 23 | (set! x y) 24 | (set! y tmp))])) 25 | 26 | 使用syntax-case的一个优点是,我们可以为swap提供更好的错误报告。例如,使用swap的define-syntax-rule定义,然后(swap x 2)在set!条件中产生语法错误!,因为2不是一个标识符。我们可以改进swap的syntax-case实现来显式检查子表: 27 | 28 | (define-syntax (swap stx) 29 | (syntax-case stx () 30 | [(swap x y) 31 | (if (and (identifier? #'x) 32 | (identifier? #'y)) 33 | #'(let ([tmp x]) 34 | (set! x y) 35 | (set! y tmp)) 36 | (raise-syntax-error #f 37 | "not an identifier" 38 | stx 39 | (if (identifier? #'x) 40 | #'y 41 | #'x)))])) 42 | 43 | 通过这个定义,(swap x 2)提供了一个源自swap而不是set!的语法错误。 44 | 45 | 在互换上述swap的定义里,#'x和#'y是模板,即使它们不是作为宏转换器的结果。这个例子说明了如何使用模板来访问输入语法的片段,在这种情况下可以检查碎片的表。同时,在对raise-syntax-error的调用中对#'x或#'y的匹配被使用,所以语法错误信息可以直接指到非标识符的源位置。 46 | -------------------------------------------------------------------------------- /16.2.4 with-syntax和generate-temporaries: -------------------------------------------------------------------------------- 1 | 16.2.4 with-syntax和generate-temporaries 2 | 3 | 由于syntax-case允许我们用任意的Racket表达式进行计算,我们可以更简单地解决我们在编写define-for-cbr(参见《扩展示例:引用函数调用》(Extended Example: Call-by-Reference Functions))中的一个问题,在这里我们需要根据序列ID...生成一组名称: 4 | 5 | (define-syntax (define-for-cbr stx) 6 | (syntax-case stx () 7 | [(_ do-f (id ...) body) 8 | .... 9 | #'(define (do-f get ... put ...) 10 | (define-get/put-id id get put) ... 11 | body) ....])) 12 | 13 | 代替上面的....,我们需要绑定get ...和put ...到生成标识符的列表。我们不能使用let绑定get和put,因为我们需要绑定那个计数作为模式变量,而不是普通的局部变量。with-syntax表允许我们绑定模式变量: 14 | 15 | (define-syntax (define-for-cbr stx) 16 | (syntax-case stx () 17 | [(_ do-f (id ...) body) 18 | (with-syntax ([(get ...) ....] 19 | [(put ...) ....]) 20 | #'(define (do-f get ... put ...) 21 | (define-get/put-id id get put) ... 22 | body))])) 23 | 24 | 现在我们需要一个表达代替....,它生成与在原始模式中匹配id一样多的标识符。由于这是一个常见的任务,Racket提供了一个辅助函数,generate-temporaries,以一系列的标识符并返回一个序列生成的标识符: 25 | 26 | (define-syntax (define-for-cbr stx) 27 | (syntax-case stx () 28 | [(_ do-f (id ...) body) 29 | (with-syntax ([(get ...) (generate-temporaries #'(id ...))] 30 | [(put ...) (generate-temporaries #'(id ...))]) 31 | #'(define (do-f get ... put ...) 32 | (define-get/put-id id get put) ... 33 | body))])) 34 | 35 | 这种方式产生的标识符通常比欺骗宏扩展产生的纯粹基于模式的宏的名字更容易理解。 36 | 37 | 一般来说,一个with-syntax绑定左边是一个模式,就像在syntax-case中一样。事实上,一个with-syntax表只是一个syntax-case的部分转换。 38 | -------------------------------------------------------------------------------- /16.2.6 通用阶段等级: -------------------------------------------------------------------------------- 1 | 16.2.6 通用阶段等级 2 | 3 | 一个阶段(phase)可以被看作是在一个进程的管道中分离出计算的方法,在这个过程中,一个代码生成下一个程序所使用的代码。(例如,由预处理器进程、编译器和汇编程序组成的管道)。 4 | 5 | 设想为此启动两个Racket过程。如果忽略套接字和文件之类的进程间通信通道,则进程将无法共享任何其它内容,而不是从一个进程的标准输出导入到另一个进程的标准输入中的文本。同样,Racket有效地允许一个模块的多个调用在同一进程中存在但相隔阶段。Racket强制分离这些阶段,不同的阶段不能以任何方式进行通信,除非通过宏扩展协议,其中一个阶段的输出是下一阶段使用的代码。 6 | -------------------------------------------------------------------------------- /16.2.6.1 阶段和绑定: -------------------------------------------------------------------------------- 1 | 16.2.6.1 阶段和绑定 2 | 3 | 一个标识符的每个绑定都存在于特定的阶段中。一个绑定和它的阶段之间的链接用整数阶段等级(phase level)表示。阶段等级0是用于“扁平”(或“运行时”)定义的阶段,因此 4 | 5 | (define age 5) 6 | 7 | 为age添加绑定到阶段等级0中。标识符age可以在较高的阶段等级上用begin-for-syntax定义: 8 | 9 | (begin-for-syntax 10 | (define age 5)) 11 | 12 | 对一个单一的begin-for-syntax包装器,age被定义在阶段等级1。我们可以容易地在同一个模块或顶级命名空间中混合这两个定义,并且在不同的阶段等级上定义的两个age之间没有冲突: 13 | 14 | > (define age 3) 15 | > (begin-for-syntax 16 | (define age 9)) 17 | 18 | 在阶段等级0的age绑定有一个值为3,在阶段等级1的age绑定有一个值为9。 19 | 20 | 语法对象将绑定信息捕获为一级值。因此, 21 | 22 | #'age 23 | 24 | 是一个表示age绑定的语法对象,但因为有两个age(一个在阶段等级0,一个在阶段等级1),哪一个是它捕获的?事实上,Racket用给所有阶段等级的词汇信息充满#'age,所以答案是#'age捕捉两者。 25 | 26 | 当#'age被最终使用时,被#'age捕获的age的相关的绑定被决定。作为一个例子,我们将#'age绑定到模式变量,所以我们可以在一个模板里使用它,并对模板求值eval: 27 | 28 | > (eval (with-syntax ([age #'age]) 29 | #'(displayln age))) 30 | 3 31 | 32 | 结果是3,因为age在第0阶段等级使用。我们可以在begin-for-syntax内再试一次age的使用: 33 | 34 | > (eval (with-syntax ([age #'age]) 35 | #'(begin-for-syntax 36 | (displayln age)))) 37 | 9 38 | 39 | 在这种情况下,答案是9,因为我们使用的age是阶段等级1而不是0(即, begin-for-syntax在阶段等级1求值表达式)。所以,你可以看到,我们用相同的语法对象开始,#'age,并且我们能够使用这两种不同的方法:在阶段等级0和在阶段等级1。 40 | 41 | 语法对象有一个词法上下文,从它第一次存在的时刻起。从模块中提供的语法对象保留其词法上下文,因此它引用源模块上下文中的绑定,而不是其使用上下文中的。下面的示例在第0阶段等级定义了button,并将其绑定到0,同时see-button为在模块a中的button绑定语法对象: 42 | 43 | > (module a racket 44 | (define button 0) 45 | (provide (for-syntax see-button)) 46 |   ; 为什么不是(define see-button #'button)?我们 47 |  后边解释。 48 | (define-for-syntax see-button #'button)) 49 | > (module b racket 50 | (require 'a) 51 | (define button 8) 52 | (define-syntax (m stx) 53 | see-button) 54 | (m)) 55 | > (require 'b) 56 | 0 57 | 58 | 在m宏的结果是see-button的值,它是带有模块a的词汇上下文的#'button。即使是在b中的另一个button,第二个button不会混淆Racket,因为#'button(这个值对see-button予以约束)的词汇上下文是a. 59 | 60 | 注意,see-button是被用define-for-syntax定义它的长处约束在第1阶段等级。由于m是一个宏,所以需要第1阶段等级,所以它的本体在高于它的定义上下文的一个阶段执行。由于m是在第0阶段等级定义的,所以它的本体处于阶段等级1,所以由本体引用的任何绑定都必须在阶段等级1上。 61 | -------------------------------------------------------------------------------- /16.2.7 语法污染: -------------------------------------------------------------------------------- 1 | 16.2.7 语法污染 2 | 3 | 一个宏的一个使用可以扩展到一个标识符的使用,该标识符不会从绑定宏的模块中导出。一般来说,这样的标识符不必从扩展表达式中提取出来,并在不同的上下文中使用,因为使用不同上下文中的标识符可能会中断宏模块的不变量。 4 | 5 | 例如,下面的模块导出一个宏go,它扩展到unchecked-go的使用: 6 | 7 | "m.rkt" 8 | 9 | #lang racket 10 | (provide go) 11 | 12 | (define (unchecked-go n x) 13 | ; to avoid disaster, n must be a number 14 | (+ n 17)) 15 | 16 | (define-syntax (go stx) 17 | (syntax-case stx () 18 | [(_ x) 19 | #'(unchecked-go 8 x)])) 20 | 21 | 如果对unchecked-go的引用从(go 'a)扩展解析,那么它可能会被插入一个新的表达,(unchecked-go #f 'a),导致灾难。datum->syntax程序同样可用于构建一个导出标识符引用,即使没有宏扩展包括一个对标识符的引用。 22 | 23 | 为了防止这种滥用的导出标识符,这个宏go必须用syntax-protect明确保护其扩展: 24 | 25 | (define-syntax (go stx) 26 | (syntax-case stx () 27 | [(_ x) 28 | (syntax-protect #'(unchecked-go 8 x))])) 29 | 30 | syntax-protect函数会导致从go被污染(tainted)的结果中提取的任何语法对象。宏扩展程序拒绝受污染的标识符,因此试图从(go 'a)的扩展中提取unchecked-go产生一个标识符,该标识符不能用于构造一个新表达式(或者,至少,不是宏扩展程序将接受的表达式)。syntax-rules、syntax-id-rule和define-syntax-rule表自动保护其扩展结果。 31 | 32 | 更确切地说,syntax-protect配备了一个带一个染料包(dye pack)的语法对象。当一个语法对象被配备时,那么syntax-e在它的结果污染任何语法对象。同样,它的当第一个参数被配备时,datum->syntax污染其结果。最后,如果引用的语法对象的任何部分被配备,则相应的部分将受到所生成的语法常数的影响。 33 | 34 | 当然,宏扩展本身必须能够解除(disarm)语法对象上的污染,以便它可以进一步扩展表达式或其子表达式。当一个语法对象配备有一个染料包时,染料包装有一个相关的检查者,可以用来解除染料包装。一个(syntax-protect stx)函数调用实际上是一个对(syntax-arm stx #f #t)的简写,这配备stx使用合适的检查程序。在试图扩展或编译它之前,扩展程序使用syntax-disarm并在每个表达式上使用它的检查程序。 35 | 36 | 与宏扩展程序从语法转换器的输入到其输出的属性(参见《语法对象属性》(Syntax Object Properties))相同,扩展程序将从转换器的输入复制染料包到输出。以前面的例子为基础, 37 | 38 | "n.rkt" 39 | 40 | #lang racket 41 | (require "m.rkt") 42 | 43 | (provide go-more) 44 | 45 | (define y 'hello) 46 | 47 | (define-syntax (go-more stx) 48 | (syntax-protect #'(go y))) 49 | 50 | (go-more)的扩展介绍了一个对在(go y)中的非导出y的引用,以及扩展结果被装备,这样y不能从扩展中提取。即使go没有为其结果使用syntax-protect(可能归根到底是因为它不需要保护unchecked-go),(go y)上的染色包被传播给了最终扩展(unchecked-go 8 y)。宏扩展器使用syntax-rearm从转换程序的输入和输出增殖(propagate)染料包。 51 | -------------------------------------------------------------------------------- /16.2.7.1 污染模式: -------------------------------------------------------------------------------- 1 | 16.2.7.1 污染模式 2 | 3 | 在某些情况下,一个宏执行者有意允许有限的解构的宏结果没有污染结果。例如,给出define-like-y宏, 4 | 5 | "q.rkt" 6 | 7 | #lang racket 8 | 9 | (provide define-like-y) 10 | 11 | (define y 'hello) 12 | 13 | (define-syntax (define-like-y stx) 14 | (syntax-case stx () 15 | [(_ id) (syntax-protect #'(define-values (id) y))])) 16 | 17 | 也有人可以在内部定义中使用宏: 18 | 19 | (let () 20 | (define-like-y x) 21 | x) 22 | 23 | “q.rkt”模块的执行器最有可能是希望允许define-like-y这样的使用。以转换一个内部定义为letrec绑定,但是通过define-like-y产生的define表必须解构,这通常会污染x的绑定和对y的引用。 24 | 25 | 相反,对define-like-y的内部使用是允许的,因为syntax-protect特别对待一个以define-values开始的语法列表。在这种情况下,代替装备整个表达式的是,语法列表中的每个元素都被装备,进一步将染料包推到列表的第二个元素中,以便它们被附加到定义的标识符上。因此,在扩展结果(define-values (x) y)中的define-values、x和y分别被装备,同时定义可以被解构以转化为letrec。 26 | 27 | 就像syntax-protect,通过将染料包推入这个列表元素,这个扩展程序重新装备一个以define-values开始的转换程序结果。作为一个结果,define-like-y已经实施产生(define id y),它使用define代替define-values。在这种情况下,整个define表首先装备一个染料包,但是一旦define表扩展到define-values,染料包被移动到各个部分。 28 | 29 | 宏扩展程序以它处理以define-values开始的结果相同的方式处理以define-syntaxes开始的语法列表结果。从begin开始的语法列表结果同样被处理,除了语法列表的第二个元素被当作其它元素一样处理(即,直接元素被装备,而不是它的上下文)。此外,宏扩展程序递归地应用此特殊处理,以防宏生成包含嵌套define-values表的一个begin表。 30 | 31 | 通过将一个'taint-mode属性(见《语法对象属性》(Syntax Object Properties))附加到宏转换程序的结果语法对象中,可以覆盖染料包的默认应用程序。如果属性值是'opaque,那么语法对象被装备而且不是它的部件。如果属性值是'transparent,则语法对象的部件被装备。如果属性值是'transparent-binding,那么语法对象的部件和第二个部件的子部件(如define-values和define-syntaxes)被装备。'transparent和'transparent-binding模式触发递归属性在部件的检测,这样就可以把装备任意深入地推入到转换程序的结果中。 32 | -------------------------------------------------------------------------------- /16.2.7.2 污染和代码检查: -------------------------------------------------------------------------------- 1 | 16.2.7.2 污染和代码检查 2 | 3 | 想要获得特权的工具(例如调试转换器)必须在扩展程序中解除染料包的作用。权限是通过代码检查器(code inspector)授予的。每个染料包的记录一个检查器,同时语法对象可以使用使用一个足够强大的检查器解除。 4 | 5 | 当声明一个模块时,该声明将捕获current-code-inspector参数的当前值。当模块中定义的宏转换器应用syntax-protect时,将使用捕获的检查器。一个工具可以通过提供与一个相同的检查器或模块检查器的超级检查器提供syntax-disarm对结果语法对象予以解除。在将current-code-inspector设置为不太强大的检查器(在加载了受信任的代码,如调试工具,之后),最终会运行不信任代码。 6 | 7 | 有了这种安排,宏生成宏需要小心些,因为正在生成的宏可以在已经生成的宏中嵌入语法对象,这些已经生成的宏需要正在生成的模块的保护等级,而不是包含已经生成的宏的模块的保护等级。为了避免这个问题,使用模块的声明时间检查器,它是可以作为(variable-reference->module-declaration-inspector (#%variable-reference))访问的,并使用它来定义一个syntax-protect的变体。 8 | 9 | 例如,假设go宏是通过宏实现的: 10 | 11 | #lang racket 12 | (provide def-go) 13 | 14 | (define (unchecked-go n x) 15 | (+ n 17)) 16 | 17 | (define-syntax (def-go stx) 18 | (syntax-case stx () 19 | [(_ go) 20 | (protect-syntax 21 | #'(define-syntax (go stx) 22 | (syntax-case stx () 23 | [(_ x) 24 | (protect-syntax #'(unchecked-go 8 x))])))])) 25 | 26 | 当def-go被用于另一个模块定义go时,并且当go定义模块处于与def-go定义模块不同的保护等级时,生成的protect-syntax的宏使用是不正确的。在unchecked-go在def-go定义模块等级应该被保护,而不是go定义模块。 27 | 28 | 解决方案是定义和使用go-syntax-protect,而不是: 29 | 30 | #lang racket 31 | (provide def-go) 32 | 33 | (define (unchecked-go n x) 34 | (+ n 17)) 35 | 36 | (define-for-syntax go-syntax-protect 37 | (let ([insp (variable-reference->module-declaration-inspector 38 | (#%variable-reference))]) 39 | (lambda (stx) (syntax-arm stx insp)))) 40 | 41 | (define-syntax (def-go stx) 42 | (syntax-case stx () 43 | [(_ go) 44 | (protect-syntax 45 | #'(define-syntax (go stx) 46 | (syntax-case stx () 47 | [(_ x) 48 | (go-syntax-protect #'(unchecked-go 8 x))])))])) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RackGuideInChinese 2 | ## Racket指南(中文翻译) 3 | 4 | ## 最新信息: 5 | 由于最新版本的Racket6.12已经出来。故自16章之后的翻译直接在原文档的Scribble文件上翻译,请需要的朋友查看racket6.12里的相应内容。 6 | 7 | **罗马不是一天建成的。** 8 | 9 | 欢迎愿意参与的朋友一起来完成这个项目,给想学习Racket的朋友提供一个学习工具。 10 | 11 | [ 英文的原始网页:https://docs.racket-lang.org/guide/index.html ](https://docs.racket-lang.org/guide/index.html) 12 | --- 13 | ## 文件规则: 14 | 1. 文件名称为:小节编号(如:2.2.1),后边跟小节名称。编号超过10的,一律采用两位数表示(如:03.03)。 15 | 2. 每个文件第一行为文件名称,文件名称应与实际名称相同。 16 | 3. 目录中的每行独立成一个文件编辑。 17 | 4. 每小节内容的带编号的节原则上也划分成一个独立文件编辑(为了减少每个阶段的工作量,便于碎片化工作)。 18 | 5. 在“0 Racket指南”中目录每一项进行标注。完成的标注为“(OK)”,正在进行的标注“(……)”,没有做的没有标注。 19 | 6. 不要在乎翻译的精确性,先完成一个完整的内容,再反过来修订。先求有,再求好,再求精确。 20 | 7. 要参与的朋友请先告知希望做那个部分,这样可以打上标记,提醒该部分已经在进行中,以避免重复。 21 | 8. 翻译内容所涉及到的名词术语统一的翻译名称可参考字典文件([WORDBOOK.md](https://github.com/OnRoadZy/RackGuideInChinese/blob/master/WORDBOOK.md))内定义内容。目录中的名称译法等同于字典。 22 | --- 23 | ## 贡献者名单:(按加入先后) 24 | |github|贡献内容| 25 | |----|------| 26 | |OnRoadZy|整体| 27 | |InvisibleMoon|第8章~8.6节| 28 | |Ghaker|第11章~11.01节| 29 | --------------------------------------------------------------------------------