├── 00. 术语表.md
├── .gitignore
├── README.md
├── 01.概论.md
├── LICENSE
└── 02.简介.md
/00. 术语表.md:
--------------------------------------------------------------------------------
1 | # 术语
2 |
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.ss~
2 | *.ss#*
3 | .#*.ss
4 |
5 | *.scm~
6 | *.scm#*
7 | .#*.scm
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aisi
2 |
3 | Scheme概论及其实现 翻译
4 |
5 | 翻译活动经费由@guenchi出资赞助: 1000字/100元
6 |
7 | 源文档:http://www.cs.rpi.edu/academics/courses/fall00/ai/scheme/reference/schintro-v14/schintro_toc.html
8 |
--------------------------------------------------------------------------------
/01.概论.md:
--------------------------------------------------------------------------------
1 | 概论
2 | ============
3 | 本书为程序员们提供了一份对 Scheme 的介绍 —— 不是给新手,而是给已经知道如何编程(至少一点点)并对学习 Scheme 感兴趣的人。
4 | ### 1.[ Scheme: 小而强大的语言](#1)
5 | ### 2.[本书的对象](#2)
6 | ### 3.[为什么是 Scheme ?](#3)
7 | ### 4.[本书不是...](#4)
8 | ### 5.[本书的结构](#5)
9 |
10 |
11 |
12 | ### 1.Scheme:小而强大的语言
13 |
14 | Scheme 是一种干净,小巧但功能强大的语言,一种通用编程语言,一种脚本语言,一种嵌入应用的扩展语言,或是其他任何东西。
15 |
16 | Scheme 在设计上适用于各种实现方法,而且存在许多实现 —— 大部分都是自由软件。有直接的解释器(如 BASIC 或 Tcl ), 快速机器码的编译器(像是 C 或 Pascal ),以及可移植的解释虚拟机代码的编译器(像是 Java)。
17 |
18 | 存在几种 Scheme 的扩展实现,包括我们自己的 RScheme 系统,它是一种具有集成对象系统和强大可扩展性功能的强可移植 Scheme 实现。
19 |
20 | 这是关于 Scheme,Scheme 实现和 Rscheme 语言及其实现的三个计划文档中的第一个。当它们都被完成后,我可能会把它们合并为一大本书。它们都将采用 Texinfo 格式,以便于打印、硬拷贝、作为在线浏览的信息文档(使用 Info 浏览器, 或 Emacs 编辑器的 Info 系统)或自动转换为 HTML 格式,以便使用 Web 浏览器进行浏览。不管以哪一种方式阅读,欢迎来到 Scheme。
21 |
22 |
23 |
24 | ### 2.本书的对象
25 |
26 | 本书的对象是对 Scheme 工作原理感兴趣的人,就语言设计这一方面对 Scheme 有兴趣的人,或只是对使用 Scheme 感兴趣的人。
27 |
28 | 这些目的并不矛盾,因为学习 Scheme 的最好方法之一 —— 以及程序语言设计的重要原则 —— 就是明白如何实现 Scheme。在 Scheme 中,我会通过展示两个简单的 Scheme 子集的解释器和一个简单的编译器来展示 Scheme 的强大功能。Scheme 的编译器可以是令人惊奇地简单和容易理解。
29 |
30 | 这是一种相当传统的方法,由 Abelson 和 Sussman 在 *Structure and Interpretation of Computer Programs*,一本广泛使用的优秀的入门编程书中首创。这种方法在其他几本 Scheme 编程的入门书籍中或多或少地得到了遵循。不过,这些书大多是给初学者的。虽然我认为 Scheme 是一种优秀的第一语言,但很多人不愿意翻翻入门书来学习 Scheme,不得不忍受学习 C,Pascal 或其他语言的痛苦。
31 |
32 | 我的方法在几个方面和目前大部分的书不同。
33 |
34 | 我会从略介绍基础编程知识 —— 例如,我假设你已经了解了什么是变量,什么是递归。
35 |
36 | 我会采用比其他 Scheme 书籍作者更加具体的方法,因为我发现许多学生觉得这样更容易理解。时不时地我会降到低于编程语言的底层,并告诉你大多数语言的实际实现是怎样的。我发现具体有助于消除许多学生,以及我自己心中的模糊。
37 |
38 | 我不会从函数式编程的角度出发来假装 Scheme 通过重写表达式来执行。(如果这对你没有任何意义,绝对不要担心!)
39 |
40 | 我把 Scheme 视作一种弱面向对象语言的特例。谈到弱面向对象,我并不是指它在继承等意义上是面向对象的 —— 即使一些 Scheme 扩展版本做到了。我只是指,在这种语言中,值是数据对象,它的标识很重要 —— 也就是说,你可以比较指向两个对象的指针,来判断它们是否为完全相同(very same)的对象,不只是判断它们是否有着相同的状态 —— 对象可能具有可变(可更改)状态。(这种观点在 RScheme,一种是 Scheme 的完全面向对象的语言中得到了进一步的发展。但这是另外一本书了,还没被写出来。)
41 |
42 | 一些人可能不喜欢这种方法,因为我在一开头就讲到了状态和赋值。在 Scheme 中随意使用赋值通常被认为是一种不好的风格,主要写"函数式"或"应用式"程序则被认为是好的风格。我同意在 Scheme 主要用函数式编程是正确的做法,但我的目的是在一开始就明确语言的语义,让新的 Schemer 们明白 Scheme 只是一种相当普通的编程语言,即使它异常干净和富有表达力。我在教授 Scheme 时的经验使我确信,许多人从早期接触赋值中获益;它讲清了变量和变量绑定的基础。当其他的东西更清楚的时候,我会在接下来讨论编程样式。
43 |
44 | 如果你曾尝试过学习 Lisp 或 Scheme,但并未深入,这本书可能有助于你。许多人学习 Scheme 或 Lisp,就像鸭子戏水一样简单。然而,有些人不是,我认为这往往是因为学习材料的展现方法的问题 —— 学习 Lisp 或 Scheme 一点都不难。在本书中,我尝试对一些事情给予不同于常规的解释,从而避免有些人从现有书籍中学到的毛病。具体的解释可能有助于克服对这些语言的不熟悉感。Scheme 真的只是一种普通的编程语言,但同时又有着能以特殊方式使用的强大功能。
45 |
46 | 如果你是个程序语言设计者,但不擅长于 Scheme 或 Lisp,这本书对于讲清楚这些语言的本质有所帮助。我确信,在 Lisp 世界和"传统的"命令式编程语言世界间存在着破坏性的裂痕。这在很大程度上是不同社区的使用的不同词汇导致。最近的 Scheme 发展并未得到其他语言设计者的广泛称赞。(这个话题会在本系列的其他文档中进一步阐述。)即便是古老的 Lisp 特性,例如宏,也还没有被大多语言设计者们恰当地理解,他们的问题在 Scheme 中得到了实质性的解决。
47 |
48 | 如果你是个编程语言实现者,或者教授编程语言的实现,这本书也可能有用。(我在有关语言和实现的课程中用到了它。)我会介绍 Scheme 的解释器和编译器。Scheme 是讲授编程语言实现原则的优秀工具,因为它的语法简单,从简单的解释器到复杂的解释器有一个直接的演变,从简单的解释器到编译器又有一个直接的转变。这支持了以最少无关细节教授语言实现的原则。
49 |
50 |
51 |
52 | ### 3.为什么是 Scmeme
53 |
54 | Scheme 是一种非常好的语言,用于实现语言,或用于通用的转换编程 (即编写编写程序的程序),或是用于编写易于扩展或自定义的程序。让实现 Scheme 有吸引力的 Scheme 特性也让它适用于各种事物,包括编写脚本,构建新语言和特定于应用程序的编程环境,等等。
55 |
56 | [随着你学习 Scheme,你很可能意识到,所有有趣的程序实际上最终都是特定于应用程序的编程环境 ...]
57 |
58 | 大多数 Scheme 系统是交互式的,允许你逐步开发和测试程序的各个部分。从这方面讲,它非常像 BASIC 或 Tcl —— 但它更加干净和有表现力。Scheme 也能够被编译,使程序运行迅速。这让它能像 BASIC 或 Tcl 一样开发,但速度仍然像 C 那么快。(Scheme 通常不如 C 快,但如果有一个好的编译器,它通常不会慢很多。)所以,如果你是个寻找不那么灵活/古板的语言的 BASIC 或 Tcl 程序员,Scheme 也许会是你的选择。
59 |
60 | 不同于大多数交互式语言,Scheme 是精心设计的:它并不是被一些考虑应用非常有限的人粗制滥造的,后来又超出合理使用范围的组装机(kludge)。它从一开始就被设计为通用语言,结合了两种早期语言的最好特性。它是对 Lisp 的彻底修订,合并了 Lisp 和 Algol ( C,Pascal 及其他语言的祖先)的最佳特性。
61 |
62 | (这正是为什么 Scheme 已被几个组织作为像 Tcl 和 Perl 一样的蹩脚语言的替代品。自由软件基金会的 Guile 扩展语言就是基于 Scheme 的。UNIX 的脚本 Scheme Shell (scsh)也是如此。
63 | CAD 框架计划已经接受 Scheme 作为控制计算机辅助设计工具。Dylan 语言也是基于 Scheme 的,虽然有着不同的语法和扩展。)
64 |
65 | 如果你想学习 Lisp,Scheme 是个不错的开始。Common Lisp 是个大型的,有点杂乱的语言,从 Scheme 开始学习很可能是最简单的。然后你可以把 Common Lisp 理解为对 Scheme 的一系列扩展。(和显著的混淆)Common Lisp 中一些最好的特性是从 Scheme 中抄过去的。
66 |
67 | 如果你想从函数式编程的风格中得到些什么,你可以在 Scheme 中做到这一点 —— 大多数写得好的 Scheme 程序在基本上是函数式的,因为这就是做许多趣事的最简单的方法。
68 |
69 | 如果你只是想把编程学的更好,Scheme 会带给你看待编程的新的角度。许多人会在 Scheme 中写出程序的原型,因为这是如此简单,即使最后他们不得为了满足雇主而用其他语言来重写代码。
70 |
71 | ### 为什么现在是 Scheme
72 |
73 | Scheme 并不是一种新的语言 —— 它已经存在并缓慢演变了 20 多年。
74 |
75 | Scheme 的演变非常缓慢,因为为 Scheme 的标准制定者们非常保守 —— 只有在人们就特性如何运作达成近乎普遍的共识时,特性才会被标准化。重点被放在质量上,而不是工业可用性。
76 |
77 | 这一政策导致了两个结果。第一,Scheme 是一种美丽的,精心设计的语言。第二,Scheme 一直“落后于形势”,缺少一些在通用语言中有用的特性。然而,渐渐地,Scheme 已从只适用于教学概念的小语言,发展成了一种非常有用的语言。
78 |
79 | Scheme 最重要的新特性(在我看来)是词法范围(“卫生的”)宏,
80 | 它允许以可移植的,高效的方式实现语言特性。这让 Scheme 保持小体型,但又允许将对基本语言有用的扩展编写成库,而不会造成显著的性能损失。
81 |
82 |
83 |
84 | ### 4.本书不是...
85 |
86 | 这本书并不是 Scheme 的语言定义,也不是使用任何 Scheme 实现的手册。有一份免费的 Scheme 语言定义文档,叫做 Revised Scheme Report,(也有 IEEE 标准)可通过互联网轻易获得。我建议获得这份报告并打印出来,或者使用 Web 浏览器浏览 HTML 版本。(http://www.cs.indiana.edu/scheme-repository/doc.standards.html)
87 | 它不是很大,因为 Scheme 是一个相当小的语言,我也推荐看一看你使用的特定实现的文档。
88 |
89 | 另一方面, 这本书在多数情况下是可以充当一本语言手册的。(我设计了在线索引,一旦它更加充实,它就可以更好地达到这个目的。)它清晰地描述了标准 Scheme 的所有重要特性,你能将它们用于大多数目的。因为 Scheme 足够干净且“正交”(orthogonal) —— 大多数特性不会以令人惊讶的方式交互,所以,如果你理解了 Scheme,并做着“Scheme 式”的事,Scheme 会按照你期待的去做。
90 |
91 | 有关
92 | Scheme、特定 Scheme 实现的详细信息,请参阅 usenet 新闻组 comp.lang.scheme 上的 FAQ 列表。它可从 Scheme 仓库通过匿名互联网 ftp 从 ftp.cs.indiana.edu 在目录 pub/scheme-repository 下访问。或者,如果你是万维网(World Wide Web)用户,请在 http://www.cs.indiana.edu/scheme-repository 访问 Scheme 仓库。Scheme 仓库包含几个免费的 Scheme 实现,以及各种有用的程序,库和文章。
93 |
94 |
95 |
96 | ### 5.本书的结构
97 |
98 | 这本书的结构反映了它的教程意图,而不是任何强大的概念分组。在接下来的三章,我会以我认为最容易学习的顺序介绍一些概念。每一章或多或少地介绍了一些相关的概念,带有短小的代码示例,最后还提供了更多的 Scheme 程序实例,来说明为什么这些概念是有用的。之后的章节会介绍相对独立的主题。
99 |
100 | “简介”这一节描述了一些 Scheme 基础特性,包括一些语法,并给出了代码示例,以表明 Scheme 可以像大多数编程语言一样使用 —— 使用 Scheme 时,你不会放弃很多,也不难切换。
101 |
102 | “使用 Scheme (A Tutorial)”这一节 给出了 Scheme 编程的教程,用于运行 Scheme 系统和交互式测试例子。
103 |
104 | “写一个解释器”一节展示了一个 Scheme 子集的解释器。
105 |
106 | “环境和过程”一节描述了 Scheme 的绑定环境和过程,并展示了在一个过程为一等公民,不定范围块结构,(垃圾回收)的语言中,过程抽象如何能变得非常强大。之后它会展示一个上一章中解释器的绑定环境和过程的实现,并演示如何以相当复杂的方式使用 Scheme 的绑定和过程定义构造。
107 |
108 | “Scheme 中的递归”一节讨论递归,尤其是尾递归。
109 |
110 | “准引用和宏”一节展现了准引用,一种构造复杂数据类型和定型数据结构变体的方法,之后介绍宏,一种在 Scheme 中定义你自己的"特殊形式"的工具。宏允许你定义自己的控制结构,像是对象系统的数据结构系统,等等。(如果你已经被 C 或 Lisp 的宏的问题吓倒过,不用担心 —— Scheme 宏解决了旧版宏系统的主要问题。宏是有趣的,因为它们经常被用在 Scheme 本身的实现上。它们允许你在一个层中构造语言实现,其中大部分语言都是用语言本身编写的,方法是从被编译器理解的核心语言启动。
111 |
112 | “其他有用的特性”一节展现了一系列杂乱的 Scheme 特性,它们在编写真正的程序时非常有用。它们不是 Scheme 的概念核心,但任何有用的语言都应该具有它们。
113 |
114 | 章节 “记录和面向对象”...
115 |
116 | 章节 "call-with-current-continuation" 讨论了第一等延续,这是 Scheme 中最强大的控制结构。延续 允许你捕捉活跃堆栈的状态(排序),并返回到该状态以在程序执行的给定点恢复。延续 在概念上是奇怪的,不应被轻易使用,但在回溯,线程等方面表现出色。
117 |
118 | 章节“一个简单的 Scheme 编译器”展示了一个 Scheme 示例程序,它碰巧是个简单的 Scheme 编译器。这是一个玩具编译器,但也算是一个真正的编译器,带有所有 Scheme 编译器的基本特征,但在执行表计划技术,存储管理方面,仅仅提供最少的支持。
119 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/02.简介.md:
--------------------------------------------------------------------------------
1 | 简介
2 | ===
3 |
4 | 在本章,我会对 Scheme 的基础特性进行简要概括,足够开始编写一些程序。
5 |
6 | 本章会过的很快, 简要介绍 Scheme 中大约一半的想法。在之后的章节中,我会更加全面地解释和演示这些特性,并介绍其他高级特性。
7 |
8 | 本章被打算是与下一章的前半部分同时阅读,下一章包含了关于交互式使用 Scheme 的教程。当你应当翻看下一章的部分内容时,我会给出指示。在熟悉 Scheme 后,它将作为基础的参考内容。你可以参考下一章中的基本示例,并在后面的章节中了解高级技巧。
9 |
10 | 如果你精通编程语言的概念,尤其是你已经在用 Lisp 编程,你可以轻松阅读这一章,来了解 Scheme 是关于什么的。如果你精通编程语言的概念,可以直接阅读本章。
11 |
12 | 如果你想要在 Scheme 中实际编程,你必须听从指示并阅读下一章的部分内容,而不是径直读完本章。
13 |
14 | ### 1.[Scheme 是什么](#1):What is Scheme?
15 |
16 | ### 2.[Scheme 基础特性](#2):Basic Features of Scheme (modified last time)
17 |
18 | ### 3.[序对和表](#3):pairs and lists
19 |
20 | ### 4.[数据结构上的递归](#4):Recursion Over Lists and Other Data Structures
21 |
22 | ### 5.[类型和相等判定](#5):Type and Equality Predicates
23 |
24 | ### 6.[引用和字面量](#6):Quoting and Literals
25 |
26 | ### 7.[局部变量和词法作用域](#7):Local Valiables and Lexical Scpoe
27 |
28 | ### 8.[过程](#8):Procedures
29 |
30 | ### 9.[再次绑定变量](#9):Valuables, Bindings, and Values
31 |
32 | ### 10.[尾递归](#10):Tail Recursion
33 |
34 | ### 11.[宏](#11):Extending the Language
35 |
36 | ### 12.[延续](#12):Continuations
37 |
38 | ### 13.[迭代构造](#13):Iteration Constructs
39 |
40 | ### 14.[讨论](#14):Introduction Discussion
41 |
42 |
43 |
44 | ### 1. Scheme 是什么(Hunk A)
45 |
46 | ==================================================================
47 |
48 | Hunk A starts here:
49 |
50 | ==================================================================
51 |
52 | 首先, 一堆行话 —— 不想看就忽略掉:
53 |
54 | Scheme 是一种词法作用域,块结构,动态类型,大体上的函数式语言。它是 Lisp 的变体。它有带有块结构,和不确定范围的第一等过程。它的参数按值传递,但值就是引用。它有第一等延续,允许构建新的控制抽象。它有词法作用域(“卫生”)的宏,允许定义新的语法形式,或重新定义旧的语法形式。
55 |
56 | 如果这些现在对你毫无意义,别担心,继续向下读。
57 |
58 | Scheme 被设计为一种交互性的和安全的语言。普通的 Scheme 系统是一个交互式程序,能让你按你想要的顺序运行 Scheme 程序的某些部分。当一个程序运行时,已有的程序不只是终止,你的数据不会消失 —— Scheme 会询问下一步做什么, 你可以检验数据或告诉 Scheme 去运行程序的另一部分。
59 |
60 | Scheme 是安全的,因为交互式系统一般不会崩溃。如果你犯了个会导致系统崩溃的错误,Scheme 会发现它, 并询问你怎么做。它允许你检查和更改系统的状态,然后继续。这和普通的 编辑-编译-链接-运行-崩溃 周期的“批处理”编程语言相比,比如 C 和 C++,是一种非常不同的编程和调试风格。
61 |
62 |
63 |
64 | ### 2. Scheme 基础特性
65 |
66 | 我会把一些 Scheme 基础特性简单地讲一遍,为清晰起见,只给出少量代码示例。
67 |
68 | #### 2.1.[表达式](#2.1):代码由表达式构成
69 |
70 | #### 2.2.[布尔值](#2.2):布尔值 #t 和 #f
71 |
72 | #### 2.3.[其他控制流结构](#2.3):cond,and,和 or
73 |
74 | #### 2.4.[注释](#2.4):注释从分号开始直到行末
75 |
76 | #### 2.5.[括号和缩进](#2.5):关于括号和缩进的注解
77 |
78 | #### 2.6.[值皆指针](#2.6):所用的值在概念上是指针
79 |
80 | #### 2.7.[自动内存管理](#2.7):Scheme 自动内存回收
81 |
82 | #### 2.8.[动态类型](#2.8):对象具有类型,而变量没有
83 |
84 | #### 2.9.[空表](#2.9):空表对象(),也叫做空指针
85 |
86 |
87 |
88 | #### 2.1.表达式
89 |
90 | 像 Lisp 一样,Scheme 也使用前缀表达式进行编写,使用括号进行分组。前缀意味着运算符的名称排在第一个,在运算对象(操作的内容)的前面。
91 |
92 | 在 Scheme 中,并没有对表达式(比如算术运算)和语句(像是 条件语句 或 循环语句 或 声明 )的区分。它们都是“表达式” —— 从普遍意义上来说。
93 |
94 | - ##### 2.1.1.[前缀表达式](#2.1.1):带括号的前缀表达式
95 |
96 | - ##### 2.1.2.[值和副作用](#2.1.2):表达式会返回 值,但可能有副作用
97 |
98 | - ##### 2.1.3.[定义变量和过程](#2.1.3):定义变量和过程
99 |
100 | - ##### 2.1.4.[定义 vs 赋值](#2.1.4):定义命名存储空间,赋值改变存储的值
101 |
102 | - ##### 2.1.5.[大多数运算符都是过程](#2.1.5):大多数运算符都是过程
103 |
104 | - ##### 2.1.6.[特殊形式](#2.1.6):特殊形式不是过程
105 |
106 | - ##### 2.1.7.[控制结构是表达式](#2.1.7):控制结构是返回 值的表达式
107 | #|注:“返回”这个词和“值”连到一起就成了“返回值”,这是个名词,在下文中,我使用“返回 值”来表示返回了一个或多个值,使用“返回值”来表示返回值:p |#
108 |
109 |
110 |
111 | ##### 2.1.1.前缀表达式
112 |
113 | 在 C 或 Pascal 中,对带有参数 bar 和 baz 的过程 foo 的调用被写成:
114 |
115 | ```C
116 | foo (bar, baz);
117 | ```
118 |
119 | 但在 Scheme 中,它被写成:
120 |
121 | ```scheme
122 | (foo bar baz)
123 | ```
124 |
125 | 请注意,过程名和参数一起在括号里。习惯它。如果你把它想成操作系统的一条 Shell 命令,比如,rm foo 或 dir bar —— 只是带上了括号分隔,它看起来就不会太奇怪了。
126 |
127 | 就像在 C 中一样,表达式可以嵌套。下面是对过程 `foo` 的调用,其中嵌套调用了过程表达式来计算参数。
128 |
129 | ```scheme
130 | (foo (bar x) (baz y))
131 | ```
132 |
133 | 这几乎相当于 C 中的
134 |
135 | ```C
136 | foo (bar(x), baz(y));
137 | ```
138 |
139 | 与 C 或 Pascal 一样,在实际调用过程之前,过程中的参数表达式会被求值;返回值被传递给过程。在 Scheme 术语中,我们说该过程被*应用*于实际参数。
140 |
141 | 你很快就会注意到,Scheme 中的特殊字符非常少,表达式通常用括号和空格分隔。例如,a-variable 是单个标识符,而不是减法表达式。在 Scheme 中,标识符不仅可以包含字母和数字,还可包含其他几种字符,例如 ! , ? , 和 _ 。长标识符通常由短语构成,使用连字符分割单词,来清楚地说明它们的含义;例如,你可以有一个名为 list-of-first-ten-lists 的变量。你能够在一个标识符中使用像 +,-,* ,/ 的字符,就像在 before-tax-total+tax 或 estimate-epsilon 中一样。
142 |
143 | Scheme 对构建标识符的规则很宽松,这样导致的一个后果是空格很重要。除非特殊字符(通常是括号)使标识符之间的划分显得明显,你必须在标识符之间输入一个或多个空格(或是回车)。例如,加法表达式 (+ 1 a) 不能被写成 (+1 a) 或 (+1a) 再或是 (+ 1a) 。(它能被写成 ( + 1 a ),因为在标识符之间额外的空格会被忽略)
144 |
145 |
146 |
147 | ##### 2.1.2.值和副作用
148 |
149 | Scheme 表达式结合了表达式和语句的特性。它们返回 值,但它们也可以有副作用 —— 也就是说,它们能通过赋值改变变量或对象的状态。
150 |
151 | 在 Scheme 中,变量赋值操作是 `set!`,读作“set-bang”。如果我们把 3 赋给变量 foo,我们这样写
152 |
153 | ```scheme
154 | (set! foo 3)
155 | ```
156 |
157 | 这和 C 中的 `foo=3;` 非常像。
158 |
159 | 注意,`(set! foo 3)` 看上去像是函数调用,因为所有东西都使用前缀表达法,但这并不是一个真正的调用,这是另一种表达式。
160 |
161 | 你不应该在 Scheme 程序中使用太多赋值操作。就像我在之后解释的,这通常是不好风格的体现。我将展示怎样以一种不需要过多副作用的风格来编程。不过,如果你需要,它们就在那里。
162 |
163 | 当你编写一个修改参数值,而不仅仅是返回 值的过程时,给它一个以感叹号结尾的名字是一种好的风格。这提醒你和任何读你的代码的人,该过程会改变已经存在的东西,而不仅仅是像返回一个新的数据结构一样返回一个值。大多数改变状态的 Scheme 标准过程都以这种方式命名。
164 |
165 | 但是,大多数 Scheme 过程不会修改任何东西。例如,标准过程 `reverse` 以表作为参数并返回反序的表。也就是说它返回了原始列表的反向副本,而没有修改原始列表。如果你编写了一个返回反序的表的过程,但以修改内容的方式使之反向,你最好称之为 `reverse!` 。这警告人们被传递给 `reverse!` 的表可能会被改变。
166 |
167 | 另一个带副作用的过程例子是 `display`。`display` 接受一个值,并将其以打印的形式写入屏幕或文件。如果你给它一个参数,它会写入“标准输出”;默认情况下,这是终端或其他展示方式。
168 |
169 | 例如,如果你想要给用户展示数字 1022 的打印表示,可以使用这个表达式
170 |
171 | ```scheme
172 | (display 1022)
173 | ```
174 |
175 | 执行此表达式的副作用是将 1022 写在用户的屏幕上。(`display` 自动将数字转化为字符串,以便你阅读。)
176 |
177 | 注意,`display` 并没有以感叹号结尾,因为它并没有对你提供的参数产生副作用。你可以给它一个数据结构,并确信它不会进行修改;`display` 当然具有副作用,但是 —— 它改变的是它将内容写入的显示屏(或文件)的状态。
178 |
179 | `display` 相当灵活,能够写入许多常见的 Scheme 对象的打印表示,甚至是相当复杂的数据结构。
180 |
181 | 除其他外,`display` 还能打印字符串。(字符串是另一种 Scheme 对象。你可以在双引号之间写出字符串字面量,“like this”,Scheme 创建一个字符串对象来存储字符序列。
182 |
183 | 表达式 `(display "hello world")` 带有写入"hello world"到标准输出的副作用,标准输出通常是用户的显示器。
184 |
185 | 这让 `display` 在调试程序、编写小的示例及编写交互式程序中显得非常有用。相似的过程 `write` 被用于在文件中保存数据;数据能通过使用 `read` 被拷贝回内存中。
186 |
187 | 在之后的章节,我会通过传递第二参数给 `display`,告诉它在何处输入,来展示如何写入文件。就现在而言,你只需要使用带一个参数的 `display`。不要尝试传递给 `display` 几个东西,并期望它全都打印出来。
188 |
189 |
190 |
191 | ##### 2.1.3.定义变量和过程
192 |
193 | 可以使用 `define` 在 Scheme 中定义变量:
194 |
195 | ```scheme
196 | (define my-variable 5)
197 | ```
198 |
199 | 这告诉 Scheme 为 my-variable 分配空间,并将存储空间的值初始化为 5。
200 |
201 | 在 Scheme 中,你总是给一个变量赋初值,这样一来,就没有像是未初始化的变量或未初始化变量错误之类的东西了。
202 |
203 | Scheme 的值始终是指向对象的指针,所以当我们使用字面量 5 时,Scheme 将其解释为指向对象为 5 的指针。数字是可以指向的对象,就像其他任何类型的数据结构一样。(实际上,大多数 Scheme 实现都使用了一些技巧来避免数字上的指针开销,但这并未在语言水平上显示。你不必注意它。)
204 |
205 | 在上述定义之后,我们可以像这样画出结果的情况:
206 |
207 | ```
208 | +-------+
209 | foo | *---|--->5
210 | +-------+
211 | ```
212 |
213 | `define` 表达式做了三件事:
214 |
215 | - 它向 Scheme 声明,我们将在当前作用域中得到一个名为 foo 的变量。(我会在稍后讨论作用域。)
216 |
217 | - 它告诉 Scheme 为变量分配存储空间。存储空间被称作绑定 —— 我们把变量 foo 与特定的一块内存进行“绑定”,以便我们能通过变量名 foo 来引用存储空间。
218 |
219 | - 它告诉 Scheme 被放进存储空间的初始值。
220 |
221 |
222 | 当你在其他语言中定义变量时,这三件事也会发生。在 Scheme 中,我们为三者都起了名字。
223 |
224 | 在图中,盒子体现了 Scheme 为变量分配存储空间的事实。在盒子旁的名字 foo 表示我们将存储空间命名为 foo。箭头表示盒子中的值是指向整数对象 5 的指针。(不要关心整数对象的实际表示方式,这一点都不重要。)
225 |
226 | 你也可以使用 define 来定义新过程:
227 |
228 | ```scheme
229 | (define (two-times x)
230 | (+ x x))
231 | ```
232 |
233 | 这里我们定义了名为 `two-times` 的新过程, 它接受一个参数 x。之后它调用加法过程 + 来使参数值与自己相加,并返回相加后的结果。
234 |
235 | 注意变量定义和过程定义之间的语法差异:对一个过程定义,名字两边有括号,参数跟在名字后面。
236 |
237 | 这和过程调用的方式很相似。考虑过程调用表达式 `(two-times 5)`,它返回 10;它和定义 `(two-times x)` 看起来很像,只是我们在形式参数 x 的地方使用了实际参数 5。
238 |
239 | 下面是一些你应该知道的编程语言术语:你传递给过程的参数有时被称为实参。位于过程之内的参数被称为形参 —— 它们代表任何在运行时实际传递给过程的内容。“实”意为你实际上传递给过程的内容,“形”意为你在过程内部调用的东西。通常我只说“参数”,但这和“实参”是同一个东西。有时我会讲到“参数变量”,这和“形参”是一样的。
240 |
241 | 你可以定义不带参数的过程,但你仍然需要在过程名两旁加上括号,以表明你在定义一个过程。当你调用它时,也在名字两旁加上括号,来表明这是一个过程调用。
242 |
243 | 例如,这是一个变量定义,它的值是 15:
244 |
245 | ```scheme
246 | (define foo 15)
247 | ```
248 |
249 | 但下面这个是过程定义,它在被调用时返回 15:
250 |
251 | ```scheme
252 | (define (foo) 15)
253 | ```
254 |
255 | ```
256 | +-------+
257 | foo | *---|--->#
258 | +-------+
259 | ```
260 |
261 | 这张图展示了,当你定义过程时,你实际上定义了一个变量,它的值恰好是一个(指针指向的)过程。现在而言,你不必关心这个问题。需要知道的主要事情是,现在你可以通过名字 foo 来调用过程。例如,过程调用表达式 `(foo)` 会返回 15,因为该过程体做的就是返回 15 这个值。
262 |
263 | 通常,我们像这样缩进过程定义,过程内容在一个新的行,并缩进几个字符。
264 |
265 | ```
266 | (define (foo)
267 | 15)
268 | ```
269 |
270 | 这样可以更清楚地表明这是个过程定义。
271 |
272 |
273 |
274 | ##### 2.1.4.定义 vs 赋值
275 |
276 | 请注意,我们可以用两种方式赋给变量一个值:我们能在定义它时给它一个初始值,或者使用 `set!` 来改变它的值。
277 |
278 | 两者的区别在于 `define` 为变量分配存储空间,并为其命名。 `set!` 没有这样做。你必须在使用 `set!` 之前先定义一个供它作用的变量。
279 |
280 | 例如,如果没有定义变量 quux,表达式 `(set! quux 15)` 就是个错的,Scheme 就会抱怨。你请求 Scheme 将 15 (指针指向的)放到名为 quux 的存储空间中 —— 但 quux 并没有命名任何存储空间,所以这样做没有意义。
281 |
282 | 这就像是,我对你说:“把这个给 Philboyd”。然后给你一些东西,(比如,一支铅笔)。如果你不知道一个叫 Philboyd 的人,你可能会开始抱怨。`set!` 就是这样。我们必须在要求你为 Philboyd 做些什么之前,就“ Philboyd”是谁达成一致。`define` 是为标识符赋予意义的方式 —— 使它引用一块内存 —— 同时也给出存放的值。
283 |
284 |
285 |
286 | ##### 2.1.5.大多数运算符都是过程
287 |
288 | 在传统的的编程语言中,比如 C 和 Pascal ,过程调用和其他种类的表达式中有着尴尬的区分。例如,在 C 中,`(a + b)` 是一个表达式,但是 `foo (a, b)` 是一个过程调用。在 C 中,处理过程时,你不能用像 + 之类的运算符做同样的事。
289 |
290 | 在 Scheme 中,事情在词义和语法上要统一的多。大多数基础运算,比如加法,都是过程,并且有着书写表达式的统一语法 —— 被括起来的前缀表示法。因此,你在 Scheme 中写 `(+ a b)`,而不是 `(a + b)`。你会写 `(foo a b)`,而不是 `foo(a, b)`。不管是哪一个,它们都是带括号的,都是运算符后面跟着运算对象的。
291 |
292 | 对任何过程调用表达式(也叫组合),所有要传递的值会在实际调用过程之前计算。(这和 C 或 Pascal 没什么不同。)
293 |
294 |
295 |
296 | ##### 2.1.6.特殊形式
297 |
298 | 虽然大多数操作都是过程调用,但你还需要了解其他几种表达式,它们的行为是不同的。它们被称作特殊形式。
299 |
300 | 过程调用和特殊形式在语法上很相似 —— 它们都是括号中的语法单元,如 `(foo bar baz)`。然而,它们在语义上非常不同。这就是为什么你需要了解特殊形式,不把它们和过程搞混的原因。
301 |
302 | 如果左括号旁的第一个东西是表明特殊形式的关键字,如 `define` 或 `set!`, Scheme 会为这种表达式执行一些特殊的操作。如果不是,Scheme 会将括号中的表达式视为过程调用,并以通常的过程调用的方式对其进行求值。
303 |
304 | (这也是为什么特殊形式被称作“特殊形式” —— Scheme 认为一些组合表达式需要做特殊处理,而不仅仅是进行过程调用。
305 |
306 | 你已经看到了 5 个或 6 个重要的特殊形式中的两个,`define` 和赋值运算符 `set!`。
307 |
308 | 注意,`set!` 并不是一个过程,因为它的第一个参数并不是一个以通常方式求值、并作为参数传递的值的表达式。它是存放值的地址。(例如,如果我们写 `(set! a b)` ,我们得到 b 的值,并将它放入名为 a 的存储空间中。)
309 |
310 | 同样,`define` 特别对待它的第一个参数 —— 变量名或过程名不是一个被求值并传递给 `define` 的表达式 —— 它只是个名称,你告诉 `define` 分配一些存储空间并使用这个名称。
311 |
312 | 我们将要看到的其他特殊形式包括:
313 |
314 | - 控制结构:`if`,`cond`,`case` 与排序电路逻辑运算符(sort-circuiting logical operators)`and` 和 `or`
315 |
316 | - 定义局部变量用到的形式:`let` 和它的变体 `letrec` 和 `let*`
317 |
318 | - 循环结构:命名`let` 和 `go`
319 |
320 | - `quote` 和 `quasiquote`,它们让你在代码中将复杂的数据结构作为文本字面量(textual literals)来编写
321 |
322 | - `lambda`,用于以相当好用的方式创建新过程
323 |
324 | 也有一些非常特别的特殊形式,如 `define-syntax`,它允许你用“宏”定义你自己的特殊形式。
325 |
326 |
327 |
328 | ##### 2.1.7.控制结构是表达式
329 |
330 | Scheme 的控制结构是表达式,并返回 值。if 表达式和 C 的 if-then 语句很像,但“then"分支和“else”分支也都是带返回值的表达式;if 表达式返回它求值的子表达式中的任何一个的值。
331 |
332 | 比如:
333 |
334 | ```scheme
335 | (if (< a b)
336 | a
337 | b)
338 | ```
339 |
340 | 会返回变量 a 或变量 b 的值,这取决于谁小(如果相等,则返回 b 的值)。如果你熟悉 C 中的条件表达式,这就像是 (a < b) ? a : b。在 Scheme 中,不需要 if 语句和类似 if 的三元运算符表达式, 因为 if“语句”是表达式。
341 |
342 | 注意,即使每一个表达式都有值,也并不需要用到所有的值 —— 你可以忽略掉 if 表达式的值。`if` 特殊形式因此可被用于控制将要执行的内容,或用于返回一个值,或同时使用两者。这取决于你。
343 |
344 | 返回值设定的一致性意味着我们从来都不必显式使用返回语句,因此 Scheme 没有它们。假设我们想要写一个返回两个数中的最小值的函数 min,在 C 中,我们可能会这样写:
345 |
346 | ```C
347 | int min(int a, int b)
348 | {
349 | if (a < b)
350 | return a;
351 | else
352 | return b;
353 | }
354 | ```
355 |
356 | 在 Scheme 中,我们可以这样做:
357 |
358 | ```scheme
359 | (define (min a b)
360 | (if (< a b)
361 | a
362 | b))
363 | ```
364 |
365 | 不论取哪条分支,相应的变量(a 或 b)的值会被作为 if 分支的值返回,同时也是作为整个 if 表达式的值返回,这也作为过程调用的返回值。
366 |
367 | 当然,你也可以写不带 else 子句的单分支 if 。
368 |
369 | ```scheme
370 | (if (some-test)
371 | (some-ation))
372 | ```
373 |
374 | 如果条件为假,单分支 if 的返回值是不确定的,如果你对返回值感兴趣,你应该使用两个分支的 if ,并显式确定两者应返回的东西。
375 |
376 | 注意到控制流是从上往下的,通过表达式的嵌套,if 控制着哪条子表达式被求值,这就像大多数语言中的控制语句的嵌套一样。值从表达式返回到调用方,就像大多数语言中表达式的嵌套。
377 |
378 | 你可以使用 `begin` 写出表达式的有序序列,例如:
379 |
380 | ```scheme
381 | (begin (foo)
382 | (bar))
383 | ```
384 |
385 | 先调用 foo 再调用 bar。就控制流而言,(begin ... ) 表达式非常像 Pascal 中的 begin ... end 块,或 C 中的 { ... } 块。(我们不需要 end 关键字,因为括回做了相同的工作。)
386 |
387 | 然而,Scheme 的 `begin` 表达式并不仅仅是代码块,因为它们是带返回值的表达式。`begin` 返回序列中的最后一个表达式的值。例如,上面的 begin 表达式返回调用 bar 而返回的值。
388 |
389 | 过程也像 `begin` 一样运作。如果过程中包含几个表达式,它们会被顺序求值,而且最后一个表达式的值会作为过程调用的返回值。
390 |
391 | 下面是一个过程 baz,它调用 foo,,然后调用 bar 并返回对 bar 的调用的结果。
392 |
393 | ```scheme
394 | (define (baz)
395 | (foo)
396 | (bar))
397 | ```
398 |
399 |
400 |
401 | #### 2.2布尔值
402 |
403 | Scheme 提供了一个特殊的唯一对象,写作 #f,读作假。如果它是条件表达式 if(或 cond)的结果
404 | ,它会被算作假值。在大多数 Scheme 中,这是唯一会被作为假的值,所有其他的值都会被算作真。
405 |
406 | 假 这个对象和整数零不是同一个东西(在 C 中是这么认为的:0就是假),它和空指针也不一样(在 Lisp 中如此)。假 对象是个唯一的对象。
407 |
408 | 为了方便和清晰,Scheme 也提供了另一个布尔值,写作 #t,可以作为真值,注意,普遍意义上,值非假即真,但特殊的布尔对象 #t 在你想判断某事为真时很好用 —— 返回的布尔真值清楚地表明你返回了一个真值,而不是带有更多信息的其它值。
409 |
410 | 像其他对象一样,在概念上,布尔值是在堆上的,当你写下 #t 或 #f 时,它表示“一个指向规范真值的指针”和“一个指向假值对象的指针”。
411 |
412 | Scheme 提供了一些过程和特殊形式来操作布尔值。过程 `not` 作为 非 运算符,总是返回真与假(#t 或 #f),如果作用于 #f,它就返回 #t。因为所有其他值都算作 真,将 not 作用于其他任何东西都会返回 #f。
413 |
414 |
415 |
416 | #### 2.3.其他控制流结构
417 |
418 | 我们已经看到,特殊形式 `if` 是一种表达式,它在影响控制流的同时返回值。Scheme 还有使用更广泛的条件结构 `cond`,和扩展逻辑运算符 `and` 和 `or`。它们都是带返回值的表达式;它们也都是特殊形式,而不是过程:它们依据其他表达式的返回值,来判断表达式是否被求值。
419 |
420 | - [cond](#2.3.1):`cond` 就像 if...else if....else....
421 |
422 | - [and 和 or](#2.3.2) :`and` 和 `or` 是“短路的”(short-circuiting)
423 |
424 |
425 |
426 |
427 | ##### cond
428 |
429 | 在大多数过程式语言中,你可以使用 if 的扩展版本写出一列 if 测试,就像这样:
430 |
431 | ```
432 | if test then
433 | action();
434 | else if test2 then
435 | action2();
436 | else if test3 then
437 | action3();
438 | else
439 | action4();
440 | ```
441 |
442 | Scheme 有一个相似的结构,一个叫做 cond 的特殊形式。上面的例子在 Scheme 中可以被写成这样:
443 |
444 | ```scheme
445 | (cond (test1
446 | (action1))
447 | (test2
448 | (action2))
449 | (test3
450 | (action3))
451 | (else
452 | (action4)))
453 | ```
454 |
455 | 注意,每一对 test-action 都在同一对括号中。在这个例子中,test1 只是个变量引用,而不是过程调用,我们测试变量 test1 ,来判断它的值是否为 #f;如果不是,我们执行 (action1),也就是调用过程 action1。如果它是,则控制“下落”到下一个测试,并继续这样的操作,直到某一个测试值为真值(除 #f 之外的值)。
456 |
457 | 我们将相应于测试部分的执行部分缩进了一个字符。这样,操作会直接放在测试的下面,而不是放在将它们组合在一起的开括号下。
458 |
459 | 我们不需要 else 子句,因为我们可以使用求值总为真值的测试表达式来达到相同的效果。这样做的一种方法是使用字面量 #t,布尔真值,因为它总为真。
460 |
461 | ```scheme
462 | (cond (test1
463 | (action1))
464 | (test2
465 | (action2))
466 | (test3
467 | (action3))
468 | (#t ;字面量 #t 总为真,所以
469 | (action4))) ;如果到了这里,这个分支会被执行
470 | ```
471 |
472 | 上面的代码和下面的 if 嵌套表达式等价:
473 |
474 | ```
475 | (if test1
476 | (action1)
477 | (if test2
478 | (action2)
479 | (if test3
480 | (action3)
481 | (if #t
482 | (action4)))))
483 | ```
484 |
485 | 像 `if` 一样,`cond` 返回被求值的分支的值。例如,如果 test1 为真,上面的 `cond` 表达式会返回过程调用(action1)的返回值。
486 |
487 | 记住,每一条 `if` 分支都是单个表达式;如果你想要在一条分支中执行多个表达式,你必须将表达式包在 `begin` 中。有了 `cond`,你就不必这么做了。你可以在测试表达式后面附加多条执行表达式,Scheme 会对所有表达式按顺序求值,并返回最后一个表达式的值。就像个 `begin` 表达式或过程。
488 |
489 | 假设我们想要调整上面的 cond 例子以便它打印出它选择的分支,同时对表达式求值并返回表达式的值,我们可以这样做:
490 |
491 | ```scheme
492 | (cond (test1
493 | (display "taking first branch")
494 | (action1))
495 | (test2
496 | (display "taking second branch")
497 | (action2))
498 | (test3
499 | (display "taking third branch")
500 | (action3))
501 | (else
502 | (display "taking fourth (default) branch")
503 | (action4)))
504 | ```
505 |
506 | 这个 `cond` 表达式会返回和之前的 `cond` 表达式相同的值,因为它总会返回分支的最后表达式的值。然而,它在执行时,它会显示它执行的操作。我们可以在以求值和显示效果为目的时使用 `cond`。
507 |
508 | 要特别小心 `cond` 的括号。你必须把一条分支的测试表达式和相应的一系列执行表达式放入同一个括号中。如果你想在这些表达式中进行过程调用,你必须将过程调用放入括号中。在上面的例子中,如果我们想要将第一个测试写成对过程 test1 的调用,而不是获得变量 test1 的值,我们会这样写:
509 |
510 | ```scheme
511 | (cond ((test1)
512 | (display "taking first branch")
513 | (action1))
514 | ...)
515 | ```
516 |
517 | 而不是
518 |
519 | ```scheme
520 | (cond (test1
521 | (display "taking first branch")
522 | (action1))
523 | ...)
524 | ```
525 |
526 | (注意到这儿的缩进。我们通常在垂直方向上排列测试部分和相应的执行序列,不论表达式是否以括号开头。也就是说,我们在包含它们的括号的开括号的下面缩进一个字符。)
527 |
528 | “额外的”括号是必须的,以便 `cond` 能分辨执行序列是和哪个测试组合在一起的。
529 |
530 | 不要怕在只有一或两条分时使用 `cond`,`cond` 通常用起来比 `if` 更舒服,因为它能够执行一系列表达式,而不只是一条。看见像下面的样式是很寻常的:
531 |
532 | ```scheme
533 | ...
534 | (cond ((foo)
535 | (bar)
536 | (baz)))
537 | ...
538 | ```
539 |
540 | 不要被它搞糊涂了 —— 它是只有一条分支的 cond 表达式,就像单分支 if 一样。我们可以这样写:
541 |
542 | ```scheme
543 | ...
544 | (if (foo)
545 | (begin (bar)
546 | (baz)))
547 | ...
548 | ```
549 |
550 | 使用 `cond` 更方便,这样我们就可以在调用 baz 并返回其值前先调用 bar,而不用显示使用 `begin` 表达式来使其顺序求值。
551 |
552 | 我们说 `cond` 是分支带有 `begin` 的 `if` 的语法糖。没有我们只能用 `cond` 做,而不能直接用 `if` 和 `begin` 做的事 —— `cond` 只是给了我们“加糖的”语法,也就是用起来更舒服。
553 |
554 | 大部分 Scheme 特殊形式就像这样 —— 它们只是写起来更加舒服的方式罢了,你能用更基础的特殊形式将它们表示出来。(在特殊形式中,只有五个“核心”特殊形式是必须的,其他的都和这些核心特殊形式的组合等价。)
555 |
556 |
557 |
558 | ##### 2.3.2. and 和 or
559 |
560 | 特殊形式 `and` 和 `or` 能被作为逻辑运算符使用,但它们也可被作为控制结构,这也是为什么它们是特殊形式。
561 |
562 | `and` 接受任意数量的表达式,并对它们进行顺序求值,直到其中之一返回 #f 或它们全被求值为止。当某个表达式返回 #f,`and` 将 #f 作为 `and` 表达式的返回值返回。如果没有表达式返回 #f,它会返回最后一个子表达式的值。
563 |
564 | 这确实是一个控制结构,而不仅仅是一个逻辑运算符,因为子表达式是否被求值取决于之前的子表达式的返回值。
565 |
566 | `and` 通常既用于控制流,又用于求返回值,就像一个 `if` 测试序列。你可以写出这样的东西
567 |
568 | ```
569 | (and (try-first-thing)
570 | (try-second-thing)
571 | (try-third-thing))
572 | ```
573 |
574 | 如果三者都返回真值,`and` 会返回最后一个表达式的值。然而,如果其中之一返回 #f ,剩余的表达式不会被求值,#f 会作为全部表达式的返回值。
575 |
576 | 同样,`or` 接受任意数量的参数,返回第一个返回值为真值(不是 #f 的任何值)的表达式的值。当它得到一个真值时,它会停止对剩余表达式的求值,并返回那个真值。
577 |
578 | ```
579 | (or (try-first-thing)
580 | (try-second-thing)
581 | (try-third-thing))
582 | ```
583 |
584 | `or` 在它所求值的子表达式为真之前会持续对子表达式求值;如果遇到了真值,`or` 会返回这个值。如果全部表达式都返回 #f,`or` 会返回 #f。
585 |
586 | ##### not 只是个过程
587 |
588 | `not` 是接受单个参数的过程,并返回 #t 或 #f,这个参数可以是任意类型的 Scheme 值。如果参数值为 #f(唯一的假值对象),它会返回 #t,否则返回 #f。也就是说,所有的值,除了假值对象,都被算作真值 —— 就像在条件表达式中一样。例如,(not 0)返回 #f。
589 |
590 | 考虑到 `and` 和 `or` 都是 特殊形式,你可能会认为 `not` 逻辑运算符也是个特殊形式。它不是。它只是个过程 —— 特别地,一个谓词。
591 |
592 | 这是有道理的,因为 `not` 总是对它的(一个)参数求值并返回一个值。它并未特殊对待任何参数 —— 它就是个普通的一等对象,在过程调用之前,参数以普通的方式被求值。
593 |
594 | 普遍意义上说,能作为过程的运算符都是过程。Scheme 只认为真正特殊的东西是特殊形式,在过程调用中,需要对它们的参数进行特殊处理。(即使是 Scheme 中最为强大的控制结构,`call-with-current-coutinuation`,也只是个一等过程。)
595 |
596 | ==========================================================================
597 |
598 | 这是 Hunk A 的结尾
599 |
600 | 是时候去试一试了
601 |
602 | 这时,你应该去看下一章的 Hunk B 并使用 Scheme 系统来试一试例子。然后回来,继续这一章。
603 |
604 | ==========================================================================
605 |
606 |
607 |
608 | #### 2.4.注释
609 |
610 | ==========================================================================
611 |
612 | Hunk C 从这里开始:
613 |
614 | ==========================================================================
615 |
616 | [ 我是不是应该早点讲这个,并把它用在例子中呢?]
617 |
618 | 你可以,也应该把注释放到你的 Scheme 程序中。注释从一个分号开始。Scheme 会忽略这一行分号之后的所有内容(这就像是 C++ 中的 // 注释。)
619 |
620 | 例如,下面是一个带有注释的变量定义:
621 |
622 | ```scheme
623 | (define foo 22) ; 定义一个初始值为 22 的变量)
624 | ```
625 |
626 | 当然,大多数注释应该要能告诉你一些代码中不明显的事情。
627 |
628 | 标准 Scheme 没有像 C 一样的 /* ... */ 块注释。
629 |
630 | 用两到三个分号,而不是一个来开始一条注释是很寻常的。这让注释的开始比单个分号显得更加突出。额外的分号会和所有在同一行的其他字符一起被忽略。
631 |
632 | 对大多数注释使用两个分号是寻常的形式,对占据一整行,或描述文件内容的注释,使用三个分号。
633 |
634 |
635 |
636 | #### 2.5.括号和缩进
637 |
638 | 学习 Scheme 最大的两个障碍可能就是括号和缩进了。在 Scheme 中,括号的使用方式和大多数编程语言中略有不同。缩进也很重要,因为语言的表面语法是如此规则。当阅读 Scheme 代码时,有经验的程序员阅读缩进结构和记号一样多。如果你没有正确地使用括号,你的程序不会正确地运行。如果你没有正确地使用缩进,它们会很难理解。
639 |
640 | Scheme 的语法,与看第一眼时的(奇怪)印象相比,其实和 C 或 Pascal 的语法更相似。毕竟,几乎所有的语言都是以嵌套的(语句或)表达式为基础。像 C 或 Pacsal 一样,Scheme 形式自由,你可以按你的任意想法进行缩进。
641 |
642 | 有些人编写 Scheme 代码时使用与 C 相似的缩进,将闭括号放在开括号的正下方,来显示嵌套。(就像我在后面解释的,这样做的人通常是不会恰当使用编辑器的初学者。)他们可能会这样写
643 |
644 | ```scheme
645 | ;;糟糕的 if 表达式缩进
646 | (if a
647 | (if b
648 | c
649 | d
650 | )
651 | e
652 | )
653 | ```
654 |
655 | 而不是像这样
656 |
657 | ```scheme
658 | ;;好的 if 表达式嵌套
659 | (if a
660 | (if b
661 | c
662 | d)
663 | e)
664 | ```
665 |
666 | 第一个版本看起来有点像 C,但它并不真正易读。第二个例子清晰地向你展现了它的结构,如果你知道如何阅读 Scheme 的话。实际上它更易读,因为它并不都是拉长的。第二个例子使用了更少的页面或电脑屏幕的空间。(在一个窗口编辑代码,同时在其他窗口做其他的事情时,这是很重要的 —— 你在一个时间可以看见更多程序。)
667 |
668 | 有两件与 Scheme 括号有关的事情需要牢记。第一,括号是重要的。在 C 或 Pascal中,你通常可以省略括号,因为运算符优先级分析,编辑器会弄清分组。更重要的是,你可以在表达式两旁加上额外的括号而不影响表达式的意思。
669 |
670 | 在 Scheme 中,这样可不行!在 Scheme 中,括号的作用不只是让运算符之间的关系清晰。在 Scheme 中,括号不是个可选项,在一个东西的两旁放上额外的括号会改变它的意思。例如,表达式 foo 是一个变量引用,它的作用是取得变量 foo 的值,另一方面,表达式 (foo) 是对无参数过程 foo 的调用。
671 |
672 | (注意到,即使是在 C 中,使用太少或太多括号来编写过程调用通常是不可接受的:调用 foo(a,b) 不能被写成 foo a,b 或 foo((a,b))。)
673 |
674 | 大体上,你必须知道什么时候需要用括号,什么时候不用,这需要对于 Scheme 规则的理解。有些括号表明过程调用,而有些仅仅是作为特殊形式的间隔符。幸运的是,规则很简单;它们应该会在下面一到两个章节中变得非常清楚。
675 |
676 | 你需要知道的另外一件事是它们必须配对。对每一个开括号,必须有一个闭括号,当然,闭括号必须在正确的位置。
677 |
678 | ##### 2.5.1.[让你的编辑器帮你](#2.5.1):编辑器使括号配对变得简单
679 |
680 | ##### 2.5.2.[缩进简单的东西](#2.5.2):过程调用和简单的控制结构
681 |
682 | ##### 2.5.3.[cond 的缩进](#2.5.3):没用恰当的缩进,cond 是不可读的
683 |
684 | ##### 2.5.4.[缩进过程定义](#2.5.4)
685 |
686 |
687 |
688 | ##### 2.5.1.让你的编辑器帮你
689 |
690 | 如果你有个像样的编辑器,括号配对是很简单的。例如,在 vi 中,你可以把光标放在一个括号上,按下 %,它会向前或向后扫描(分别根据是开括号还是闭括号)来寻找配对的括号并使其发光,它会跳过任何配对的括号;如果没有找到配对的括号,它会警告你。
691 |
692 | 大多数编辑器都有像这样的功能。学习使用它。通常很容易把闭括号弄对,如果你心存疑虑,可以使用编辑器来确保你在正确的地方用到闭括号。
693 |
694 | 一些编辑器,像 Emacs,有专门编辑 Lisp 和 Scheme 的 Mode。这当然很有用,但只有配对括号的功能是一个编辑 Scheme 的编辑器所必须的。Emacs Scheme 的一个好处是,如果你愿意,它会自动对你的代码进行缩进,这会告诉你,你的表达式是否是按照它所期望的方式嵌套的 —— 如果你的括号不对,文本看起来会很滑稽,并提示你改掉错误。
695 |
696 | (cnuscheme 是 Emacs 对 Scheme 的一个Mode,可以从通常的 Emacs 模组代码中得到。它只是个 Emacs Lisp 程序,通过定制来使 Emacs“理解”Scheme 的语法并帮助你格式化你的代码。使用 Emacs Lisp 包 cmscheme.el,它会提供给你一个方便的 Scheme 编辑模组。它可以从 Scheme 仓库中获得。)
697 |
698 | 即便没有一个特殊的包,一个编辑器也可以帮你不少。例如,大多数 Emace 模组自动配对括号,在你输入相应的闭括号时闪一下开括号。用一点时间弄清你的编辑器的括号配对功能会节省你很多时间。
699 |
700 | #|注:emacs 自带 scheme-mode,会自动缩进代码。可以安装 paredit-mode 来提高体验,emacs才是 scheme 的 IDE,—— 身为Emacs党的我自豪的断言道 :p |#
701 |
702 |
703 |
704 | ##### 2.5.2.缩进简单的东西
705 |
706 | #| ~the original website is empty......~ |#
707 |
708 |
709 |
710 | ##### 2.5.3. cond 的缩进
711 |
712 | 小心对待和 `cond` 有关的括号和缩进。注意,在 test-action 子句中,只缩进一个字符,但这是很重要的。没有这个缩进,`cond` 读起来会很困难。
713 |
714 | 假设我们用 `cond` 替换下面尴尬的 `if` 表达式。
715 |
716 | ```scheme
717 | ;;需要 begin 来顺序执行分支的尴尬的 if 表达式
718 | (if (a)
719 | (begin (b)
720 | (c))
721 | (begin (e)
722 | (f)))
723 | ```
724 |
725 | 我们可以写成这样:
726 |
727 | ```scheme
728 | (cond ((a)
729 | (b)
730 | (c))
731 | (else
732 | (e)
733 | (f)))
734 | ```
735 |
736 | 有时,当一个 `cond` 的子句很小时,一个完整的子句会被水平地写出来。上面的实例很可能会被写成这样:
737 |
738 | ```scheme
739 | (cond ((a) (b) (c))
740 | (else (d) (e)))
741 | ```
742 |
743 | 同样也要小心对待条件表达式两旁的括号。注意,a 的两边之所以有括号,是因为它是个对无参数过程的调用,而不是因为你总是在条件表达式的两边加括号。(注意到,#t 的两边没有括号,如果我们只是想测试变量 a 的值,而不是调用它并测试它的结果,a 的两边不会有括号。)
744 |
745 |
746 |
747 | ##### 2.5.4.缩进过程定义
748 |
749 | 就像我在前面提到的,过程定义的缩进有着特殊的规则。你一般要将过程的内容缩进一些字符长度(我缩进 3 个),但是,不要把过程内容的表达式直接放在变量名列表下。
750 |
751 | 不要这样做:
752 |
753 | ```scheme
754 | (define (double x)
755 | (+ x x))
756 | ```
757 |
758 | 如果你这样做,一个过程定义看起来就像是个过程调用,或是个普通的变量定义。要让定义一个过程显得明显,这样做:
759 |
760 | ```scheme
761 | (define (double x)
762 | (+ x x))
763 | ```
764 |
765 | 这样就清楚表明 (double x) 和 (+ x x) 不是一类东西。前者声明了怎样调用过程,后者说明了过程会做什么。
766 |
767 |
768 |
769 | #### 2.6.值皆指针
770 |
771 | 就像我前面说的,所有的值在概念上都是指针指向的,堆上的对象,并且你不能显式释放内存。
772 |
773 | 说到“对象”,我不一定是指面向对象意义上的对象。我只是指像是 Pascal 的记录或是 C 的结构一样的数据对象,它们能够通过指针引用(也许不能),能够承载状态信息。
774 |
775 | 一些版本的 Scheme 有面向对象的对象系统。(这也包括我们的 RScheme 系统,其中标准 Scheme 类型都是统一对象系统中的所有类。)然而,在这本书中,我们会以更宽广的含义使用“对象”这个词,意思是一个可以指向的实体。
776 |
777 | ##### 2.6.1.[所有的值都是指针](#2.6.1):All Value are Pointers
778 |
779 | ##### 2.6.2.[堆上的对象](#2.6.2):Object on the heap
780 |
781 |
782 |
783 | ##### 2.6.1.所有的值都是指针
784 |
785 | 概念上,所有的 Scheme 对象都在堆上分配,并通过指针引用。这确实让事情变得简单,因为你不必担心在当你需要使用值的时候,需要对指针解引用 —— 你总是这样做。因为指针的解引用是普遍的,过程总是在真正需要用到值时,对指向值的指针解引用,你不必显示解引用。
786 |
787 | 例如,前置定义的 Scheme 过程 `+` 接受两个指向数字的指针,自动在做加法之前对两个指针解引用。它返回一个指向相加值的指针。
788 |
789 | 所以,当我们对表达式 `(+ 2 3)` 求值来计算二加三,我们会接受一个指向 2 的指针和一个指向 3 的指针,并将这些作为参数,传递给过程 `+`。`+` 会返回一个指向 5 的指针。我们可以嵌套表达式,例如,(* (+ 2 3 ) 6),这样指向 5 的指针转而被传递给过程 `*` ,因为这些函数都接受指针作为参数,并将指针作为返回值,你可以忽略指针,像在其他任何语言中一样写下算术表达式。
790 |
791 | 当你思考它的时候,在数学意义上,改变整数的值是没有任何意义的。例如,把整数 6 的值变成 7 有什么意义?这并不合理,当然如此。6 是唯一的,抽象的数学对象并没有任何能被改变的状态 —— 6 就是 6,它的举止永远是 6。
792 |
793 | 在传统语言中,并没有真正改变一个整数值 —— 它将一个整数值(的拷贝)替换为另一个整数值(的拷贝)。这是因为,大多数编程语言既有指针语义(对指针变量)又有值语义(对非指针变量,如整数)。你创建多个值的拷贝,在之后的赋值中将拷贝值破坏。
794 |
795 | 在 Scheme 中,我们不必破坏整数的值,因为我们通过用一个指针代替另一个达到了效果。一个整数在 Scheme 中是个唯一的实体(概念上),就像它在数学中一样。我们并没有多次拷贝一个特定的数,只是多次引用。(实际上,Scheme 处理数字并不真的这么简单和美妙,由于我将在不久之后解释的效率问题。)
796 |
797 | 我们将在之后看到,如果没有影响到程序员对事物的看法,一个实现可以自由地对这些指针进行优化,但当你尝试理解一个程序,你总是需要将值视为指向对象的指针。
798 |
799 | 指针的普遍使用使很多事情变得简单。在 C 或 Pascal 中,无论你是处理一个原始值或一个指针,你必须小心。如果你有一个指针并且你需要知道实际的值,你必须显式对指针解引用(例如,C中的前缀运算符 `*` ,或 Pascal 中的前缀运算符 `^`)。如果你有一个值而且你需要用指针指向它,你必须得到它的地址(例如,C 中的前缀运算符 `&`,或 Pascal 中的前缀运算符 `^`)
800 |
801 | 在 Scheme 中,这些麻烦都是不必要的。用户定义的程序一致地传递指针,当他们深入到预定义的程序时(如内置的 `+` 过程或 `set!` 特殊形式),这些底层内置操作会执行任何必要的解引用。
802 |
803 | (当然,当做比如遍历链表的事情时,程序员必须要求对指针解引用,但从程序员的角度来看,这只是意味着从已指向的对象的内容中获得另一个指针值。)
804 |
805 | 有时,据说,像 Scheme 这样的的语言(Lisp,smalltalk,Eiffel 和 Java)“没有指针”。至少可以说,相反的观点是真实的 —— 一切皆指针。它们所没有的,是你所关心的对指针和非指针的区分。
806 |
807 | ##### 多数实现优化了许多指针
808 |
809 | 你可能认为,对每一个对象都用一个指针来指向的话,开销会很大,因为你不得不为指针和指针指向的对象都分配空间,同时你也不得不使用额外的指令来通过指针访问值。
810 |
811 | “一切都是指针”是在语言层面 —— 例如,从程序员的角度。但一个 Scheme 系统实际上并不必像语言层面上那样来表示事物。
812 |
813 | 许多 Scheme 实现都优化了许多指针。例如,将整数值表示为用指针指向的堆上的对象的做法是低效的。Scheme 实现因此使用一些技巧,不通过真正使用指针来表示整数。(再次提醒,记住,这只是对程序员隐藏的实现技巧,整数值具有指针的语义,即使它们的表示方式与其他东西不同。)
814 |
815 | 相比于把整数值放在堆中,然后传递指针给它们,大多数实现把实际的整数的位组合(bit pattern)直接放进变量中 —— 毕竟,合理大小的整数适合机器字长。
816 |
817 | 存储在变量中的短值(short value)(如普通整数)称为直接值,不是用于间接引用对象的指针。
818 |
819 | 将整数和短值放到变量中的问题是 Scheme 不得不对它们进行区分,并将可能具有相位组合的指针和它们区分开。
820 |
821 | 解决方案是标记(tagging)。每个变量中的值实际上有一些位被用作类型标记,这样就说明了变量的类型 —— 例如,是否为指针。使用一些位作为标记略微减少了可用于实际值的存储空间,但就像我们将要看到的,这通常不是个问题。
822 |
823 | 似乎直接存储整数的位组合会产生打破 Scheme 所要展现的抽象 —— 幻想所有值都是指向堆上对象的指针。然而,并非如此,因为语言的强制限制,使程序员看不见其间的差异。
824 |
825 | 就数字(数学上的)和一些其他类型而言,你不能改变对象本身的状态。没有通过对整数使用副作用,使它表现得不同于它本身的性质的方法。我们说,整数是不可变的,也就是说,你不能使它变异(mutate)(改变)。
826 |
827 | 如果整数实际上分配在堆上并通过指针引用,而且如果你能够改变整数的值,那么该更改对于其他指向整数的指针是可见的。
828 |
829 | (这并不是说变量的值不能一会儿是一个整数,一会儿又是另一个 —— 变量的值实际上是指向整数的指针,不是整数本身,而且你真的只是将指向一个数的指针,替换成了指向另一个数的指针。)
830 |
831 |
832 |
833 | ##### 2.6.2.堆上的对象
834 |
835 | 大多数 Scheme 对象只有通用的值单元格字段 —— 任何字段都能储存任何 Scheme 值,无论是带标记的直接值还是带标记的指向堆分配对象的指针。(当然,概念上它们都是指针,所以字段的类型只是(“可以指向任何东西的指针”。)
836 |
837 | 例如,一个序对(pair)(在 Lisp 术语中被称为“ cons 单元格”)是堆上的带两个字段的对象。每个字段都能存储任意种类的值,比如数字,文本字符,布尔值,或是指向其他堆上对象的指针。
838 |
839 | 序对的前一个字段被称为 car 字段,第二个字段被称为 cdr 字段。这些名词是所有计算机科学名词中最蠢的。(它们只是第一个 Lisp 实现及其运行的计算机上的历史产物。)
840 |
841 | 序对可以通过调用过程 `cons` 来创建。例如,要创造一个 car 字段值为 22,cdr 字段值为 15 的序对。你可以写下过程调用 (cons 22 15)。
842 |
843 | 序对的字段就像变量绑定,这样它们可以存储任何类型的 Scheme 值。绑定和字段都被称为值单元格 —— 也就是说,它们是可以存放任何类型值的位置。
844 |
845 | 在大多数实现中,每个堆上分配的对象都有一个隐藏的“头”字段,那是作为 Scheme 程序员的你所不必知道的。额外的字段存储了类型信息,清楚地表明堆上分配的对象的种类。所以,放在内存中,序对看起来就像这样:
846 |
847 | ```
848 | +-----------+
849 | header | |
850 | +===========+
851 | car | +-----+----->22
852 | +-----------+
853 | cdr | +-----|----->15
854 | +-----------+
855 | ```
856 |
857 | 在这张图中,对(cons 单元格)中的 car 字段存储整数 22,cdr 字段存储整数 15。
858 |
859 | 存储在序对中的字段的值画成了箭头,因为它们是指向数字 22 和 15 的指针。
860 |
861 | (实际表示这些值的方式可能是 2 位用作区分整数和真正的指针的标记,30 位用作存储数字二进制数,但你不必担心这个。)
862 |
863 | Scheme 提供了内置过程 `car` 来取得一个序对的 car 字段的值,还有 `set-car!` 来设置这个字段的值。同样还有 `cdr` 和 `set-car!` 用来取得和设置 cdr 字段的值。
864 |
865 | 假设我们有个对变量 foo 的顶级(top-level)变量绑定,它的值是指向序对的指针。我们可以像这样把这种情况画出来:
866 |
867 | ```scheme
868 | +=========+
869 | +---------+ header| |
870 | foo | *----|------------->+=========+
871 | +---------+ car| *----+---->22
872 | +---------+
873 | cdr| *----+---->15
874 | +---------+
875 | ```
876 |
877 | 大多数 Scheme 对象都是以相似的方式表示的。例如,一个向量(一维数组)通常表示为线性的值单元的数组,它可以存储任何类型的值。
878 |
879 | 即便实际上不是这样表示的对象也可以用这种方式思考,因为在概念上,堆上的所有东西都能通过指针引用。
880 |
881 |
882 |
883 | #### 2.7.自动内存管理
884 |
885 | 在像 C 或 Pascal 的语言中,数据对象可以通过几种方式分配。(回忆一下,谈到“对象”我只是指像记录(records)的数据对象。)它们可以被静态地分配(像是全局变量的情况),或使用像是 malloc 或 new 的分配函数,在程序运行时进行堆上的动态分配。
886 |
887 | Scheme 更简单 —— 所有的对象都在堆上分配,通过指针引用。Scheme 的堆是垃圾回收的,这意味着 Scheme 系统会在你之后自动清理内存。时不时地,系统会找出不再使用的对象,并回收它们的存储空间。(这种方式是非常保守和安全的 —— 收集器不会回收你的程序中指针指向的对象,也不会通过任何可能的指针遍历来接触到对象。不要害怕收集器会在你没有注意时,吃掉你还在关注的对象!)
888 |
889 | 垃圾回收的使用支持了不定程度(indefinite extent)的抽象。这意味这对象在概念上是永久存活的,或至少是在它们和程序有关时 —— 没有重用内存的概念(在语言层面)。以这个视角看待运行的程序,内存就是无限的 —— 它能够无限地分配内存,而不需要重用它们的空间。
890 |
891 | 当然,这个抽象,当没有足够的内存供你做你尝试做的事情时,就失效了。如果你尝试创建一个比可用内存更大的数据结构,你的内存会用光。垃圾回收并不能给你提供你没有的内存。
892 |
893 | 一些人认为垃圾回收在时间或空间上的开销很大。即便垃圾回收不是毫无代价的,它的代价也比通常认为的要小很多。一些人也有着在关键时刻因为垃圾回收而使得系统停止的糟糕经历,但现代的 GC 技术能够解决这个问题。(如果你对高效而无中断的垃圾回收实现感兴趣的话,我的 GC 研究论文是个不错的起点,可以访问我的研究小组网站( http://www.cs.utexas.edu/users/oops 。)
894 |
895 |
896 |
897 | #### 2.8.动态类型
898 |
899 | ##### 对象具有类型,但变量没有
900 |
901 | 如果我把手指当作指针,我可以用它指向任何东西 —— 一台电脑,一幅画,一辆摩托车,或是任意数量的东西。Scheme 中的变量就像是这样。
902 |
903 | 在 Scheme 中,所有的变量都具有相同的类型:“可以指向任何东西的指针”
904 |
905 | Scheme 是动态类型的,这意味着变量没有固定的类型,但对象具有。一个对象自带类型 —— 整数永远都是整数,但一个变量有时可能引用一个整数,有时又是个字符串(或其他东西)。语言提供运行时的类型检查,来确保你不会对对象执行错误的操作 —— 例如,如果你尝试将两个字符串相加,系统会发现这个错误并提示你。
906 |
907 | 有时,人们认为像 Scheme(还有 Lisp,Smalltalk)这样的语言是无类型的。这非常具有误导性,在真正的无类型语言(比如 FORTH 和大部分汇编语言)中,你可以按照你想要的方式来解释一个值 —— 作为一个整数,一个指针,或任何其他东西。(你在 C 中也可以这么做,使用不安全的强制转换,这是许多耗时的 bug 的来源。)
908 |
909 | 在动态类型系统中,类型在运行时确定。例如,如果你尝试使用作用于数字的过程 `+` 来把两个表相加,系统会发现这个错误并从容地停止 —— 它不会傻乎乎地认为你知道你在做什么。你也不能将非指针值错误解释为一个指针,产生让你的程序崩溃的,致命的分割违规。
910 |
911 | 你可能认为动态类型成本很高,它确实如此。但好的 Scheme 编译器能够通过编译时的类型推断来消除大部分开销,大部分高级实现还允许在性能关键位置进行类型声明,以便编译器生成类似于 C 或 Pascal 的代码。
912 |
913 | ==========================================================================
914 |
915 | 这是 Hunk C 的结尾
916 |
917 | 是时候试一试了
918 |
919 | 在这个时候,你应该去读一读下一章的 Hunk D 并使用 Scheme 系统来试一试例子。然后回来,继续这一章。
920 |
921 | ==========================================================================
922 |
923 |
924 |
925 | #### 2.9.空表
926 |
927 | ==========================================================================
928 |
929 | Hunk E 从这儿开始:
930 |
931 | ==========================================================================
932 |
933 | 在 Scheme 中,只有一个空指针值,叫做“空表”,打印为 ()。(之后,我们会看到它为什么会这样写,还有为什么它会被称为“空表”。)
934 |
935 | 概念上,空表是个特殊对象,一个“空”指针是一个指向表的结尾对象的指针。你可以忽略这个事实并仅仅将它看作一个空指针,因为你不能对它所指向的对象做什么有趣的事。
936 |
937 | (在有些实现中,空表对象 '() 是个真正的可用指针引用的对象,而且空指针真的的是指向它的指针。在其他实现中,一个空表是个不可更改的值,是空指针的特殊标识。在 Scheme 语言的水平上,在特定 Scheme 系统中使用哪种方式来实现并不重要。你所真正能对空指针做的是将它与其他的指针比较,来判断它们是否也是空指针。)
938 |
939 | 空表对象扮演着用作任何目的的空指针。Scheme 只有一种指针(可以指向任意内容的指针),所以只有一种空指针(指向空内容的指针)。
940 |
941 | Scheme 提供了一个过程,null? 来检查某个值(指针指向的)是不是空表,也就是说,是否为一个空指针。例如,如果变量 foo 是个空表,(null? foo) 会返回 #t,否则返回 #f。
942 |
943 | 你可以将空表在你的程序中写作字面量 '()。也就是说,表达式 '() 返回空表(空指针)。之后我会解释,当你写下空表作为字面量时,为什么你必须在一对空的括号之前使用单引号标记。
944 |
945 |
946 |
947 | ### 3. 序对和表
948 |
949 | 像 Lisp 一样,Scheme 有着处理一种特别灵活的表——由序对组成的表,的内置过程。它的 cdr 字段存储着将它们连在一起的指针,它的 car 字段存储值。
950 |
951 | #### 3.1. [cdr 连接的表](#3.1):空表作为结尾的,由 cdr 连起来的,由序对组成的表,car 字段存储对物体的引用。
952 |
953 | #### 3.2.[表和引用](#3.2):字面量表
954 |
955 | #### 3.3.[空表这个名字哪来的](#3.3):为什么它会被这么叫,会被打印为 (),并会被作为字面量而写作 '()。
956 |
957 | #### 3.4.[一些方便的表处理过程](#3.4):length,list,append 和 reverse
958 |
959 |
960 |
961 | #### 3.1.cdr 连接的表
962 |
963 | 在 Lisp 和 Scheme 中,你不必像传统方式一样,通过指向下一个对象的“next”指针来将对象们串成表。你可以创建一个由序对组成的表,其中,序对的 cdr 字段存储指向对象的指针,并将序对连接成一串。
964 |
965 | 这并不是 Scheme 中真正的表数据类型。一个表时实际上是由以空指针结尾的序对序列组成的。一个空指针也是一个表 —— 它是没有序对的以空指针结尾的表。我们有时会谈到“表的 car 部分”和“表的 cdr 部分”,但它们真正的意思是“表的第一个序对的 cdr 部分”和“表的第一个序对的 cdr 部分”。
966 |
967 | 假设我们有着指向一个含有整数 22,15 和 6 的表的变量 foo。下面是画出这种情况的一种方法。
968 |
969 | ```scheme
970 | +---------+ +---------+ +---------+
971 | +---------+ | | | | | |
972 | foo | *----+--->+=========+ +-->+=========+ +-->+=========+
973 | +---------+ | 22| / | 15| / | 6|
974 | +---------+ / +---------+ / +---------+
975 | | *----+---+ | *----+--> | * |
976 | +---------+ +---------+ +---------+
977 | ```
978 |
979 | 这样展现了与实际上的内存真正代表的很相近的东西。但还有更好的画出表的方法,这种方法强调了数字的值在概念上是指针指向的数,并与我们通常对表的看法相对应:
980 |
981 | ```scheme
982 | +---+ +---+---+ +---+---+ +---+---+
983 | bar | *-+--->| * | *-+----->| * | *-|----->| * | * |
984 | +---+ +-+-+---+ +-+-+---+ +-+-+---+
985 | | | |
986 | \|/ \|/ \|/
987 | 22 15 6
988 | ```
989 |
990 | 我省略掉了对 Scheme 程序员不可见的,对象的头字段。
991 |
992 | 我使用了特殊方法来绘制序对,让 car 字段和 cdr 字段边靠边。将字段靠在一起使我能够从左到右地画出表,让 cdr 字段在合适的位置来表现出它作为“next”指针的用途。我把整数画在序对的外面,用 car 字段中的指针指向它们,因为这就是语言层面的看法。
993 |
994 | 这样强调了表和在表“里面的”东西实际上是分开的。
995 |
996 | 我们省略了序对头,因为它们只是底层细节,因为它们可能是因系统而异的隐藏实现细节,也因为 Scheme 程序员能认出这种“两盒式”的点对描绘。
997 |
998 | Scheme 表结构的一个主要优点是,你不必修改一个对象来将它放入表中 —— 一个对象能同时在多个表中,因为一个表是一串带有指向表中内容的指针的序对。这与人们在大多入门编程课堂上,被以传统方式教导创建单链表相比,要清晰的多。(在语言中,所有的值都是指针这一点是非常自然的 —— 当然,由对象组成的表真的只是由指向对象的指针组成的表。)
999 |
1000 | 例如,你可以有含有相同元素的两个表,或是其中一些元素相同,但很可能顺序不同的表。
1001 |
1002 | ```scheme
1003 | +---+ +---+---+ +---+---+ +---+---+
1004 | bar | *-+--->| * | *-+----->| * | * +----->| * | * |
1005 | +---+ +-+-+---+ +-+-+---+ +-+-+---+
1006 | | | |
1007 | \|/ \|/ \|/
1008 | 22 15 6
1009 | /|\ /|\
1010 | | |
1011 | +---+ +-|-+---+ +-+-+---+
1012 | baz | *-+--->| * | *-+-------------------->| * | * |
1013 | +---+ +---+---+ +---+---+
1014 | ```
1015 |
1016 | 这儿我画了两个表,bar 和 baz ——也就是说,两个表是变量 bar 和 baz 的值。bar 带有元素 22,15,和 6,baz 只带有元素 22 和 6。
1017 |
1018 | 由于这两个表都只是由序对构成的,所以它们是不同的序对,我们可以在不修改其中一个的情况下修改另一个,而且也不需要修改在表“里面”的对象。例如,我们可以让一个表反序而不影响另一个。
1019 |
1020 | (我们也不必创建一个特殊的,带有两个 next 字段的表节点,以便一个东西同时能在两个表中。我们可以使用两个,三个或四个分开的序对表。)
1021 |
1022 | Scheme 有书写表的文本表达的标准方式。考虑上图所示情况,对表达式 `(display bar)` 求值会打印 (22 15 6)。对表达式 `(display baz)` 求值会打印 (22 6)。注意到,Scheme 只是在括号中写出了表中的物体 —— 它不代表单个的序对,而只是 car 部分的值。
1023 |
1024 | 动态类型也有助于表的有用性。序对表能存储任意类型的对象,甚至是混有不同类型对象的包。因此,例如,序对表可以是整数表,可以是由表组成的表,或是任何我们还没碰到过的类型的对象组成的表。它也能是个混有整数、其他的表、或类似东西的表。因此,一些表处理函数在一系列情况中很有用 —— 例如,单链表查找函数能够在任意种类的表中查找靶向对象。
1025 |
1026 | 下面这张图展示了对变量 bar 和 foo 的变量绑定。bar 的绑定存有表 (10 15 6),foo 的绑定存有表 (22 15 6)。我们说这些表共享了结构,也就是说,一个表的部分也是另一个表的一部分。
1027 |
1028 | ```scheme
1029 | +---------+
1030 | +---------+ | |
1031 | bar | *----+--->+=========+
1032 | +---------+ | 10|
1033 | +---------+
1034 | | *----+-+
1035 | +---------+ \
1036 | \
1037 | +---------+ \ +---------+ +---------+
1038 | +---------+ | | \ | | | |
1039 | foo | *----+ +=========+ +-->+=========+ +-->+---------+
1040 | +---------+ | 22| / | 15| / | 6|
1041 | +---------+ / +---------+ / +---------+
1042 | | *----+---+ | *----|-+ | * |
1043 | +---------+ +---------+ +---------+
1044 | ```
1045 |
1046 | 这张图可能会很好的与内存中的真实表示相对应,但还是有点令人困惑。
1047 |
1048 | 更加普遍的绘制方式是这样的
1049 |
1050 | ```scheme
1051 | +---+ +---+---+
1052 | bar | *-+--->| * | *-+------+
1053 | +---+ +-+-+---+ |
1054 | | |
1055 | \|/ |
1056 | 10 |
1057 | \|/
1058 | +---+ +---+---+ +---+---+ +---+---+
1059 | foo | *-+--->| * | *-+----->| * | *-+----->| * | * |
1060 | +---+ +-+-+---+ +-+-+---+ +-+-+---+
1061 | | | |
1062 | \|/ \|/ \|/
1063 | 22 15 6
1064 | ```
1065 |
1066 | 再次强调,概念上,所有东西都是指针,序对带有指向整数的指针。
1067 |
1068 | 在上面的图中,我们可以讲“foo 的 car 部分”,它表示被存储在 foo (的绑定)中的值所指向的点对中的 car 字段。它是(指针指向的)10。我们常常管这个叫“表 foo 的 car 部分”。
1069 |
1070 | 注意,foo 的 cdr 部分也是一个表,它也是与 bar 的 cdr 部分相同的表 —— 第一个序对的 cdr 部分指向同一个表。
1071 |
1072 | 我们可以说,对过程 eq? 而言,foo 的 cdr 和 bar 的 cdr 是相同的,因为表达式 (eq? (cdr foo) (cdr bar)) 返回真值。也就是说, (cdr bar) 和 (cdr foo) 返回(指针指向的)完全相同的对象。
1073 |
1074 |
1075 |
1076 | #### 3.2.表和引用
1077 |
1078 | Scheme 允许你使用引用来书写字面量表。就像你能在程序中写字面量布尔值和数字一样,如果你是用特殊形式 `quote` 的话,你可以写出字面量表。
1079 |
1080 | 引用是个特殊形式,而不是个过程,因为它并不以通常的方式对它的参数求值。(它的参数只是数据结构的字面量表达,这看起来像一个 Scheme 表达式,但并不是。)
1081 |
1082 | 例如,表达式 `(quote (1 2 3))` 返回一个指向表 (1 2 3) 的指针,也就是说,一个各个 car 的值为(指针指向的)1,2,3 的,由 cdr 连接起来的序对组成的表。
1083 |
1084 | 你可以把 `quote` 表达式用作其他表达式的子表达式,因为它们仅仅返回和像其他任何东西一样的指针值。
1085 |
1086 | 例如,表达式 `(define foo (quote (1 2 3)))` 定义了(也绑定了)一个变量 foo,并将它的绑定初始化为一个(指针指向的)三元素表。
1087 |
1088 | 我们可以用这种方式画出结果情况:
1089 |
1090 | ```scheme
1091 | +---+ +---+---+ +---+---+ +---+---+
1092 | foo | *-+--->| * | *-|----->| * | *-+----->| * | * |
1093 | +---+ +-+-+---+ +-+-+---+ +-+-+---+
1094 | | | |
1095 | \|/ \|/ \|/
1096 | 1 2 3
1097 | ```
1098 |
1099 | `quote` 只接受一个参数,并返回一个和你键入的,作为 quote 参数的数据结构的打印表示相同的数据结构。Scheme 不会对 quote 的参数以表达式的方式求值 —— 它只是给你一个指向数据结构的指针。
1100 |
1101 | 注意到,quote 并不构建一个(字面意义的)字符串 —— 它会构建一个可能是表,或是树,或甚至是数组的数据结构。这是非常普适的引用能力,比构建字符串对象的,在字符串两旁的双引号要强大的多。
1102 |
1103 | Scheme 提供了一种更清爽的书写 qoute 表达式的方式,使用特殊的单引符 '。相比写下 (quote some-expression),你可以使用单引号来写引用表达式。例如,我们可以用 (define foo '(1 2 3)) 写出相同的对 foo 的定义。你不需要一个回引号,这是因为 Scheme 的前缀带括号语法 —— 它能弄清引用数据结构何时结尾。
1104 |
1105 | qoute 表达式便利的一点是,qoute 表达式并不在每次它被调用时创造一个数据结构 —— 对同样的表达式多次求值可能会返回许多指向同一结构的指针。
1106 |
1107 | 考虑过程定义
1108 |
1109 | ```scheme
1110 | (define (foo)
1111 | '(1 2 3))
1112 | ```
1113 |
1114 | 表 (1 2 3) 可能会在我们定义过程 foo 时被创建,每次我们调用 foo 时,它可能会返回指向相同表的指针。(会发生什么取决于具体的 Scheme 实现,但因为效率原因,大部分都是这样做的。对 quote 表达式求值只是取得指向之前创建的数据结构的指针。)
1115 |
1116 | 因为这个原因,修改从 qoute 形式返回的数据结构就是个错误。不幸的是,许多 Scheme 系统没有发现这个错误,还就让你这么干。如果你每次都需要一个新的数据结构,你应该使用像是 list 的过程,它每次总会创建一个新的数据结构。(list 是个接受任意数量参数,创造由参数组成的表的 Scheme 标准过程,我们会在之后讨论这个。)
1117 |
1118 | 例如,如果我们想要过程 foo 在每次调用时返回一个新的表 (1 2 3),我们可以写成这样
1119 |
1120 | ```scheme
1121 | (define (foo)
1122 | (list 1 2 3))
1123 | ```
1124 |
1125 |
1126 |
1127 | #### 3.3.空表这个名字哪来的
1128 |
1129 | 既然你已经理解了 Scheme 的表和单引用,我就能解释为什么空指针被叫做“空表”,并被写作 '()。
1130 |
1131 | 考虑三元素表:
1132 |
1133 | ```scheme
1134 | '(1 2 3)
1135 | ```
1136 |
1137 | 这个表的 cdr 部分是表 (2 3)。我们可以把这个字面量写成 '(2 3)。
1138 |
1139 | (2 3) 的 cdr 部分是单元素表 (3)。我们可以把这个字面量写作 '()。
1140 |
1141 | (3) 的 cdr 部分是零元素表,(),也就是说,这是个空表。我们可以把这个字面量写作 '()。
1142 |
1143 | 考虑到 Scheme 表的工作方式,没有元素的表和空指针是同一个东西,将它打印成带有零个元素的表 (),将它写作带有单引号的字面量 '(),这样做是很自然的。
1144 |
1145 |
1146 |
1147 | #### 3.4.一些便利的表处理过程
1148 |
1149 | Scheme 提供了多种多样的表处理过程,以便你不必考虑序对 —— 你可以考虑整个表。我会在之后更加详细地讨论这些过程,但这儿只给出简要介绍。
1150 |
1151 | 这些过程不会修改它们的参数 —— 它们以表作为参数,但它们会返回新的表,而不对旧的表进行修改。
1152 |
1153 | ##### 3.4.1.[length](#3.4.1)
1154 |
1155 | ##### 3.4.2.[list](#3.4.2)
1156 |
1157 | ##### 3.4.3.[append](#3.4.3)
1158 |
1159 | ##### 3.4.4.[recerse](#3.4.4)
1160 |
1161 | ##### 3.4.5.[member](#3.4.5)
1162 |
1163 |
1164 |
1165 | ##### 3.4.1.length
1166 |
1167 | `length` 接受一个作为参数的表,并返回等于表长度的整数。例如,(length '(0 #t #f)) 返回 3。
1168 |
1169 |
1170 |
1171 | ##### 3.4.2.list
1172 |
1173 | `list` 接受一个或更多参数并创建一个由参数组成的表。也就是说,一个 cdr 连接的,空表结尾的序对序列会被创建,每个序对的 car 部分会存储一个作为参数传递给 list 的值。
1174 |
1175 | 注意,这和 `cons` 不同, 在 `cons` 中,参数通常不是表 —— 它们只是能被放进表的物体。
1176 |
1177 | 直观地,我们经常使用 cons 将一个物体放入已经存在的表,但我们使用 list 来创建。
1178 |
1179 | 注意到,如果我们只给 list 一个参数,例如,(list 1),这样会创建一个序对,它的 cdr 部分为空表,它的 car 部分是给定的参数。与之相较,如果我们使用 cons 来创建单元素表,我们必须用这个元素和一个空表来作为 cdr 部分的值:(cons 1 '())。
1180 |
1181 |
1182 |
1183 | ##### 3.4.3.append
1184 |
1185 | `append` 接受两个或更多表作为参数,并创建一个带有所有参数的表。例如,
1186 |
1187 | ```scheme
1188 | (append '(1 2) '(3 4))
1189 | ```
1190 |
1191 | 会返回表 (1 2 3 4)。
1192 |
1193 | 注意到,这和 `list` 做的不一样:
1194 |
1195 | ```scheme
1196 | (list '(1 2) '(3 4))
1197 | ```
1198 |
1199 | 会返回 ((1 2) (3 4)),一个以给定表为元素的双元素表。list 使它的参数组成新的表,这与参数使表或其他东西无关。
1200 |
1201 | `append` 要求它的参数是表,并创建一个以参数表的元素为元素的表 —— 在上面这种情况下,为一个四元素表。直观上,它将给定的表连接起来。然而,它之连接第一级的结构 —— 它不会“压扁”嵌套的结构。例如
1202 |
1203 | ```scheme
1204 | (append '((1 2) '(3 4))
1205 | '((5 6) '(7 8)))
1206 | ```
1207 |
1208 | 会返回 ((1 2) (3 4) (5 6) (7 8))。
1209 |
1210 | `append` 不会修改它的任何参数,但是 `append` 的结果一般会共享 `append` 得到的参数的最后一个。(它会高效地 `cons` 其他表到最后一个表来创建结果表。)因此,使用 `append` 新建一个表,之后对“旧”表进行修改的行为是危险的。这是为什么副作用在 Scheme 中很危险的原因之一。
1211 |
1212 |
1213 |
1214 | ##### 3.4.4.reverse
1215 |
1216 | `reverse` 接受一个表作为参数,并返回一个反序的新表。
1217 |
1218 | 例如,
1219 |
1220 | ```scheme
1221 | (reverse '(1 2 3 4))
1222 | ```
1223 |
1224 | 会返回表 (4 3 2 1)。像 `append` 一样,`reverse` 只会使一级结构反向。
1225 |
1226 | ```scheme
1227 | (reverse '(1 2) '(3 4))
1228 | ```
1229 |
1230 | 会返回 ((3 4) (1 2)),而不是 ((4 3) (2 1))。
1231 |
1232 |
1233 |
1234 | ##### 3.4.5.member
1235 |
1236 | `member` 接受一个值和一个表,并在表中搜索这个值。如果它找到了,它会返回一个指向 car 部分为该值的序对的头的指针。也就是从被搜索的对象被找到的地方开始的,“剩余的”表。如果没有找到,会返回 #f。(因此,返回值总是要么为序对,要么为假值对象。)
1237 |
1238 | ```scheme
1239 | (member 22 '(18 22 #f 300))
1240 | ```
1241 |
1242 | 会返回 (22 #f 300)。
1243 |
1244 | 注意,`member` 既可被用于在表中找出一个值的位置,又可当作一个断言来检查这个东西在不在表中。因为序对是真值,你可以在条件表达式中使用 `member` 的结果,如果目标被找到了,结果会被算作真。
1245 |
1246 |
1247 |
1248 | ### 4.数据结构之上的递归
1249 |
1250 | [这节放在这里有点不合时宜 —— 需要先介绍类型和相等判定!] [那些东西在课堂上已经展示过了,所以这应该是可以理解的]
1251 |
1252 | 在本节中,我会演示最常见的数据结构 —— 表和数的最寻常的递归方法。
1253 |
1254 | 其中的一些例子将会成为像是 `length`,`list`,`append` 和 `reverse` 一样的 Scheme 标准过程实现。Schene 已经内建了这些过程,但你应该理解如何使用像是 cdr 和 cons 的简单过程来实现它们。你将不可避免地需要编写略有不同但代码类似的特殊过程。(下一章中,我会展示一些更高级的编程技巧,来让你实现像这些例子一样的,更加普遍和/或高效的过程。)
1255 |
1256 | 我也会展示一些其他方便的表处理过程,例如,一个表复制函数。
1257 |
1258 | 之后我会展示在简单二叉树上的递归。在 Scheme 中对树使用递归的普遍形式和其他你可能习惯使用的语言(如 C 或 Pascal)有些不同 —— 也更加简单。
1259 |
1260 | #### 4.1.[length](#4.2)
1261 |
1262 | #### 4.2.[Copying Lists](#4.2)
1263 |
1264 | #### 4.3.[append 和 reverse](#4.3)
1265 |
1266 |
1267 |
1268 | #### 4.1.length
1269 |
1270 | `length` 是 Scheme 标准过程,它返回一个表的长度。它只算表节点上的元素(沿着 cdr)
1271 |
1272 | 使用递归时,这样做很容易。如果表是空的,它的长度就是 0;如果不是,则为 1 加上剩余表的长度。下面是定义 `length` 最简单的方法:
1273 |
1274 | ```scheme
1275 | (define (length lis)
1276 | (cond ((null? lis)
1277 | 0)
1278 | (else
1279 | (+ 1 (length (cdr lis))))))
1280 | ```
1281 |
1282 | 主要需要注意的事情是,这个例子是个递归结构。该过程可接受一个指向序对或空表的指针。该过程的结构直接与(恰当的)表的递归定义相对应。两条分支的 `cond` ,与两条规则描述了表的特征这个事实相对应;它弄清了我们要处理的情况。
1283 |
1284 | 我们显式地检查表结尾的情况,但我们隐含了假设,即处理的对象是个序对。这看起来是个不好的风格,但实际上它是好的,因为它确保了,如果 length 的参数不是空表或一个序对,将会产生错误信号 —— cond 的第二分支会被选择,但对 (cdr lis) 的求值会报错。
1285 |
1286 | 我们可以通过三分支 cond 来使其更清楚,带有两种合理情况和一种错误情况的分离分支:
1287 |
1288 | ```scheme
1289 | (define (length lis)
1290 | (cond ((null? lis)
1291 | 0)
1292 | ((pair? lis)
1293 | (+ 1 (length (cdr lis))))
1294 | (else
1295 | (error "invalid argument to length")))
1296 | ```
1297 |
1298 | 上面我使用了错误信号过程 `error`,它会终止程序运行并显示错误。(在多数系统中,错误信息 ”invalid argument to length“ 会被打印,用户会被展示调试这个问题的中断提示。)不幸的是,`error` 并没有被所有 Scheme 系统支持。(之后我会展示一个在所有 Scheme 系统中都能工作地相当好的实现)
1299 |
1300 | 在这个例子中要注意到,我使用了 lis 作为参数名,而不是 list。这是因为有一个名为 list 的 Scheme 标准过程,它会被任意同名的本地变量遮蔽。(这是因为 Scheme 的统一命名空间 —— 你不能拥有相同名字的变量和过程,原因会在之后解释。list 似乎是唯一的标识符,这通常是个问题。)
1301 |
1302 | 上面的 `length` 定义不是尾递归 —— 在调用自身后,必须有一个返回以便 1 能够被加给长度值并返回。稍后我会展示一个更高效的尾递归版本的 `length` 和一个名为 `reduce` 的通用过程,`reduce` 能被用来构建一系列基础算法相似的过程。
1303 |
1304 |
1305 |
1306 | #### 4.2.Copying Lists
1307 |
1308 | 拷贝的普通意思有两种,(shallow copying)浅拷贝和(deep copying)深拷贝。浅拷贝得到一个对象的副本,这个副本有着和原指针指向相同的指针。
1309 |
1310 | 深拷贝不仅仅拷贝数据结构中的一级对象,还会拷贝一级对象里面的东西,如此递归地进行下去,这样一来,一整个新的对象被创建了。
1311 |
1312 | 对于由多于一个对象组成的表,把这一串表都拷贝是很有用的,也就是说沿着 cdr 进行深拷贝。我们通常将表看作一个特殊种类的对象,即便它是一个序对对象的序列。因此,拷贝”just the list“是很自然的。
1313 |
1314 | 如果我们只是想进行浅拷贝,我们可以定义 pair-copy 来拷贝序对,不需要拷贝其他任何东西。
1315 |
1316 | 在这些例子中,我会假设我们只想拷贝表结构 —— 也就是由序对连接的集合。不论何时,当我们遇到了不是序对的东西,我们会停止拷贝,得到的副本会共享原结构。(这些都不是 Scheme 标准过程。)
1317 |
1318 | 下面是一个真正的浅拷贝,只是拷贝了一个简单序对:
1319 |
1320 | ```scheme
1321 | (define (pair-copy pr)
1322 | (cons (car pr) (cdr pr)))
1323 | ```
1324 |
1325 | 如果我们想要进行深拷贝,我们可以使用递归来拷贝 car 或 cdr 的值,它们也是序对。下面的 pair-tree-deep-copy 的代码假设将被拷贝的结构是个由序对组成的树。 (如果有任何共享的结构,每次程序遇见时,它会被拷贝,这个副本不会含相同的结构。它总是树。在拷贝时保护共享结构更困难,但能够做到。如果有一个定向的环,pair-tree-deep-copy 会无限循环。)
1326 |
1327 | ```scheme
1328 | (define (pair-tree-deep-copy thing)
1329 | (if (not (pair? thing))
1330 | thing
1331 | (cons (pair-tree-deep-copy (car thing))
1332 | (pair-tree-deep-copy (cdr thing)))))
1333 | ```
1334 |
1335 | 注意到,pair-tree-deep-copy 作用于不合适和合适的表,但只会拷贝序对。当它取得非序对的值时,它会终止并在副本中使用相同的值,副本会与原结构共享结构。
1336 |
1337 | pair-tree-deep-copy 的代码直接反映了它拷贝的数据结构的种类。它能操纵非序对,非序对被假设为正在拷贝的,由序对组成的图的叶子,它也能操纵序对,序对被假设为树的内部节点。它们的 car 和 cde 值可能是树的叶子,或其他序对。
1338 |
1339 | 所以,对序对树的递归定义为:
1340 |
1341 | - 一个非序对(叶子),或
1342 |
1343 | - 一个序对,它的 car 和 cdr 是序对树
1344 |
1345 |
1346 | 第一条规则是基础情况,也就是说,它是不需要递归的简单事情。第二条规则是递归规则,它表达了一个事实,内部节点的 car 和 cdr 字段能够指向任何序对树:一个叶子,或另一个内部节点,该节点的子树可能是叶子或其他内部节点...
1347 |
1348 | 这是编写针对数据结构的递归函数的简单方法 —— 弄清精确描述期望数据结构的递归表述,之后使用递归表述来写出所需结构的递归描述。你可以直接地编写遍历结构和计算结果的代码。
1349 |
1350 | 一般地,我们先写出基础情况,来表明递归何时结束 —— 以便我们不会忘记写出它而意外地写出无限递归或不可操纵的情况。如果你一致地这样做,你的代码可读性会更强而且你会更少犯错。
1351 |
1352 | 要拷贝一串真正的表,我们可以使用我们需要的结果描述:
1353 |
1354 | 表的副本是
1355 |
1356 | - 空表(如果原表是空的话),或
1357 |
1358 | - (如果原表非空)一个序对,它的 car 值和原表的 car 值相同,它的 cdr 值为对原表剩余部分的拷贝。
1359 |
1360 |
1361 | 下面是代码:
1362 |
1363 | ```scheme
1364 | (define (list-copy lis)
1365 | (cond ((null? lis)
1366 | '())
1367 | (else
1368 | (cons (car lis)
1369 | (list-copy (cdr lis))))
1370 | ```
1371 |
1372 | 通常,我们会检查参数来判断我们是否位于表的结尾,否则就假设参数是个序对。因为在后一种情况下,我们使用了序对的 car 和 cdr,如果参数不是真正的表,我们会得到一个错误。这通常是我们想要的,所以当 Scheme 到达具有意外结构的表的部分时,它会发出错误信号。
1373 |
1374 | 名字 list-copy 被选择来表明它作用于表,在 Scheme 中,术语”list”默认为“真正的表”。如果我们想要一个拷贝非真正表的函数,我们可以管它叫别的东西,并注释下它所作用的东西。
1375 |
1376 | 实际上,表在 Scheme 中是如此的常见,以至于我们直接叫它 copy。大多数过程名以它们操作的结构种类名开头,但表和数字例外。
1377 |
1378 |
1379 |
1380 | #### 4.3.append 和 reverse
1381 |
1382 | `append` 和 `reverse` 是两种方便的表操作;它们都是 Scheme 标准过程。
1383 |
1384 | `append`接受任意数量的表作为参数,并返回一个带有所有参数的表。`reverse` 接受一个表并返回一个反序的新表。
1385 |
1386 | 注意到,和多数 Scheme 过程一样,它们都不带破坏性 —— 创建一个新表而不对参数有副作用(修改)。
1387 |
1388 | ##### 4.3.1.[append](#4.3.1)
1389 |
1390 | ##### 4.3.2.[reverse](#4.3.2)
1391 |
1392 |
1393 |
1394 | ##### 4.3.1.append
1395 |
1396 | 除了处理复数(多于一个)个数的表这一点外,`append` 和 list-copy 的工作方式很像。
1397 |
1398 | 使之正确的技巧是保持 list-copy 的基本结构,并带有正确的细微差别。
1399 |
1400 | 现在,我们让问题简单一点,只写出双参数的 `append` 版本,名为 `append2`。
1401 |
1402 | 我们的策略是对第一个表进行递归,像 list-copy 一样,在每一步中拷贝表中的一个元素。然而,当我们到达结尾时,基础情况是不同的 —— 相比于以空表将表结尾,我们使用第二个表,来作为副本的“剩余”。
1403 |
1404 | 注意到,当第一个表为空时,基础情况会触发 —— `append` 作用于空表和另一个表的结果就是另一个表,概念上,我们 `cons` 零个项在这个表的前面。具体而言,我们可以返回这个表。
1405 |
1406 | 下面是我们想要的结果的递归特征:
1407 |
1408 | - 如果第一个表为空,结果就是第二个表
1409 |
1410 | - 如果第一个表非空,结果为一个序对,它的 `car` 是第一个表的 `car`,它的 `cdr` 是`append` 作用于第一个表的剩余部分和(全部)第二个表的结果。
1411 |
1412 |
1413 | 下面是一个简单的双参数版本的 `append`:
1414 |
1415 | ```scheme
1416 | (define (append2 lis1 lis2)
1417 | (cond ((null? lis1)
1418 | lis2)
1419 | (else
1420 | (cons (car lis1)
1421 | (append2 (cdr lis1) lis2)))))
1422 | ```
1423 |
1424 | 注意,`append2` 拷贝它的第一个参数,但结果仅仅与后一个表参数共享一个指针 —— 后一个表未被拷贝,因此结果与这个表共享结构。这个结论对于 Scheme 标准过程 `append` 也是正确的,`append` 能接受任意数量的表作为参数。前面的 n-1 个表被拷贝,最后一个被共享。
1425 |
1426 | 确保你理解了算法之上的具体操作。在递归过程中向下进行时,我们拆开第一个表,在每一步中拿出一个表元素。当到达第一个表的结尾时,递归停止然后我们返回第二个表。在回去的路上,我们 `cons` 这些项得到一个新表,回到前面。
1427 |
1428 | 假设我们定义了两个表, foo 和 bar,就像这样:
1429 |
1430 | ```scheme
1431 | (define foo '(x y z))
1432 | (define bar '(a b))
1433 | (define baz (append bar foo))
1434 | ```
1435 |
1436 | 结果会得到与 foo 共享结构,但不与 bar 共享的 baz。通过 foo 对表的更改也可通过 baz 可见。
1437 |
1438 | ```
1439 | +----------------------------------------+
1440 | | |
1441 | \|/ |
1442 | +---+ +---+---+ +---+---+ +---+---+ |
1443 | foo | +-+--->| * | *-+---->| * | *-+----->| * | * | |
1444 | +---+ +-+-+---+ +-+-+---+ +-+-+---+ |
1445 | | | | |
1446 | \|/ \|/ \|/ |
1447 | x y z |
1448 | |
1449 | |
1450 | +---+ +---+---+ +---+---+ |
1451 | bar | *-+--->| * | *-+---->| * | * | |
1452 | +---+ +-+-+---+ +-+-+---+ |
1453 | | | |
1454 | \|/ \|/ |
1455 | a b |
1456 | /|\ /|\ |
1457 | | | |
1458 | +---+ +---+---+ +---+---+ |
1459 | baz | *-+--->| * | *-+---->| * | *-+--------------------+
1460 | +---+ +---+---+ +---+---+
1461 | ```
1462 |
1463 | 普遍上,`append` 的结果共享传递给 `append` 的最后一个参数的结构。如果你想避免这一点,你可以将空表作为传递给 `append` 的最后一个参数。例如 (append '(1 2 3) '()) 会拷贝表 (1 2 3)。
1464 |
1465 | 如果你担心效率问题,你要意识到,花费的时间与必须复制的表,即除最后一个表的所有表的长度成比例。这通常并不重要,但如果是对程序性能关键的部分,尤其是你要 `append` 长表,这值得考虑。
1466 |
1467 | (使用 `append` 时,将短表在长表的前面,之后如果必要使用 `reverse` 是很寻常的。)
1468 |
1469 |
1470 |
1471 | ##### 4.3.2.reverse
1472 |
1473 | `reverse` 返回一个表的反向拷贝。
1474 |
1475 | 使用 `append` 来定义 `reverse` 是个容易(但效率低下)的方法。我们只需取出表的第一个元素,对表的剩余部分取反,并将第一个元素添加再表尾。我们以递归的形式来完成这件事,以便每次对表的剩余部分取反时,我们是再对更短的表做相同的事。当我们到达表尾时,取反不执行:空表的反向还是个空表。
1476 |
1477 | ```scheme
1478 | (define (reverse lis)
1479 | (if (null? lis)
1480 | '()
1481 | (append (reverse (cdr lis))
1482 | (list (car lis)))))
1483 | ```
1484 |
1485 | 想想这到底是怎么运作的。`reverse` 沿着表向下递归,在每一个递归步骤中调用自己作用于表的 `cdr` 部分,直到递归在表尾停止。(最后一次调用返回空表,也就是空表的反向。)在每一步中,我们使用 `car` 来从表中取出一个元素,然后保存它,直到递归调用返回时。
1486 |
1487 | 通过返回,反向的表片段被传回,在每一个返回步骤中表的 `car` 被放到表的后面。(要想使用 `append` 在表尾添加单个项目,我们必须使用 `list` 将它放入单元素表中。)
1488 |
1489 | 最终,我们使用递归的方法从后往前构建了一个新表。在递归过程中每一步拆开一个项目,并在每一步中在新表的尾部添加一个元素。
1490 |
1491 | 这是个便于理解的例子,既具体又抽象。在把表拆开并在之后重新合并成一张表的过程中,你需要理解其中的具体步骤。另一方面,你也应该认识到,即便你没有注意到这点,算法仍然起作用。
1492 |
1493 | 一旦你掌握了递归的窍门,在不考虑其中细微步骤的情况下写出算法,或是仔细考虑其中的步骤顺序通常是很容易的。这样一来,对于是否能够使剩余的表反向,并添加第一个项目到表尾的判断是很容易的。我们不需要过多考虑操作顺序,因为操作会以我们传递给函数参数的方式向下运行。我们可以宣称“the `reverse` of a non-empty list *is* the `append` of the `reverse` of the rest of the list and (a list containing) the first item in the list”(太难翻了......),然后根据这个宣称来写出代码,并作为 (pure)纯函数 —— 只依赖于它的参数值,而没有副作用。
1494 |
1495 | 通过以递归形式书写,我们将对所有的表应用相同的技巧。想的更具体一点 —— 但又不是那么具体 —— 我们能够看到,在每一次我们对剩余的表进行反向操作时,问题中的表会变得更短。在某个地方,我们会碰到表尾,因此我们必须处理这个基础情况。弄明白对基础情况的正确做法通常很简单。这样一来,我们可以宣称”空表的 `reverse`就是空表“,并对 `append` 加上合适的分支。
1496 |
1497 | 这是个教你如何通过组合函数来得到新的函数,并在不使用副作用的情况下实现算法的绝佳例子。(注意到如果我们使用副作用,我们就必须及其慎重地考虑步骤的顺序,以确保我们在明确的变化之后使用数据结构,并且在其他(副作用)之前。)
1498 |
1499 | (接下来对于效率的注解相对高深 —— 你不需要担心这些东西是否会挡在学习如何编程的道路之上。你可以跳过或忽略它们,在你领会了 Scheme 的窍门,并想要调整你的程序的速度核心部分使之达到最大的效率时回来。另一方面,你可能会发现,考虑具体细节会加深基本认识。)
1500 |
1501 | 然而,以如此简单的方法书写 `reverse` 会有两个问题 —— `reverse` 变成了”简单“表操作中效率最低的一个。之后我会展示更加聪明,但只是稍增复杂的更佳版本(它还会是递归的,不会使用循环和赋值操作。)
1502 |
1503 | 第一个问题是每次调用 `append` 所需的时间与给定表的长度成比例。(回忆一下,`append` 实际上会拷贝它的第一个参数表。)我们必须使用 `append` 来拷贝”剩余“的表,从表中的每个序对开始。平均情况下,在递归步骤中拷贝表的一般,因为我们对表中的每一个序对都进行了拷贝,我们得到了一个O($n^{2}$)的算法。
1504 |
1505 | 另一个问题是,我们是在递归返回的过程中进行 `append` 操作,相比于在递归过程中进行操作开销更大。就像我在之后的章节中解释的, 如果所有事情都以顺序进行,Scheme 就能够高效地进行递归 —— Scheme 能够优化除了最后一个的所有步骤和在调用前的状态保存(幸运的是,这做起来很容易。)
1506 |
1507 | 正是因为 Scheme 提供了内置的 `reverse`,你不必想太多。一个优秀的 Scheme 系统会提供一个高度优化的 `reverse` 实现,所用时间与表的长度成线性关系。
1508 |
1509 | `reverse` 非常方便,内置 `reverse` 的效率非常重要,因为通常以简单而高效的顺序构建表,如果需要再将它反向的做法是最佳的。传统上,你每一次 `cons` 一个项目到一个表中,或是一次性 `append` 一些项目,以最简单的方式来创造表。这就允许你以线性时间构造表;有着一个线性时间的 `reverse`,整体过程仍是线性的。
1510 |
1511 | ==================================================================
1512 |
1513 | 这是 Hunk E 的结尾。
1514 |
1515 | 是时候试一试了。
1516 |
1517 | 到了这个时候,你应该阅读下一章的 Hunk F 并使用 Scheme 系统来试试例子。之后回到这里,读完这一章。
1518 |
1519 | ==================================================================
1520 |
1521 | [Lists(Hunk F)]
1522 |
1523 |
1524 |
1525 |
1526 | ### 5.类型和相等判定
1527 |
1528 | ==================================================================
1529 |
1530 | Hunk G starts here:
1531 |
1532 | ==================================================================
1533 |
1534 | 因为一个指针可以指向任意类型的东西,知道它所指向东西的类型通常是一件好事。例如,你可能有一个混合着不同种类东西的表,并想要遍历这个表,对所遇到的不同种对象执行不同的操作。对于这个,Scheme 提供了类型判定,它们是检测被指针指向的对象是否为某种特定类型的过程。
1535 |
1536 | 你也会时常先要知道两个值是否指向相同的变量,或指向相同结构的数据结构。对于这个,Scheme 提供了相等判定。
1537 |
1538 | - 5.1.[类型判定](#5.1):不同种类对象的判定
1539 |
1540 | - 5.2.[相等判定](#5.2):判定对象是否相同
1541 |
1542 | - 5.3.[选择相等判定](#5.3):测试不同程度的相等
1543 |
1544 |
1545 | 这些过程被称作“判定”(predicate),是因为它们测试某个值是否具有某种性质,并返回是或否 —— 也就是说,布尔值 #t 或布尔值 #f。(这就像形式逻辑中的“判定”,它是一种真值取决于参数的语句。)
1546 |
1547 | 判定的名字一般以问号结尾,来标记它们返回一个布尔值。当你写你自己的程序时,以问号作为布尔返回值的函数名结尾是种良好风格。
1548 |
1549 | (该规则的例外是像是 >,< 和 = 的标准数字比较判定。依照这个规则,它们呢应该以问号作为名称结尾,但是它们的使用非常频繁,而且人们普遍认它们作判定。我们不要应为名字中的问号所烦恼,因为它会弄乱算法表达式。)
1550 |
1551 |
1552 |
1553 | #### 5.1.类型判定
1554 |
1555 | Scheme 提供了内置的过程来测试指针指向的对象是否为特定类型。如果你想要知道 x 的值是否为(一个指针指向的)序对,你可以使用判定 `pair?`,就像这样:(pair? x)。
1556 |
1557 | 相同的,如果你想要知道某个东西是否为一个数字,你可以使用判定 `number?` 。如果你想要知道某个值是否为整数,而不是其他类型的数,你可以使用 `integer?`。
1558 |
1559 | 其他的一些类型判定也被提供给一些我们将要讨论的数据类型,包括 `string?`, `character?`,`vector?` 和 `port?`。
1560 |
1561 |
1562 |
1563 | #### 5.2.相等判定
1564 |
1565 | 相等判定用来区分一个值是否和另一个“相等”,这儿实际上有几种意义上的“相等”,因此 Scheme 提供了 4 种相等判定。
1566 |
1567 | 有时你想知道两个数据结构是否从结构上是相等的,是否有相同的值和相同的地址。例如,你可能想知道一个表是否与另一个表有着相同的结构和元素。对于这一点,你可以使用 `equal?`,它会进行一个深度的,元素级别的结构比较。
1568 |
1569 | 例如 (equal? '(1 2 3) '(1 2 3)) 返回 #t,因为参数都是包含 1,2,3 并以这个顺序构成的表。`equal?` 会对数据结构进行深度的遍历,因此你可以在嵌套的表或其他相当复杂的数据结构上使用它。(然而,不要将它用在带有环指针的结构上,这样可能会进入无法结束的死循环。
1570 | #|注:就像 C 中的环链表 |#
1571 |
1572 | `equal?` 对简单的东西同样有用。例如,(equal? 22 22) 返回 #t,(equal? #t 5) 返回 #f。(注意到 equal? 可被用于比较类型不相同的东西,但如果类型是不同的,结果总是 #f。不同类型的对象永远不会 equal。)
1573 |
1574 | 通常你不会想要比较两个完全的数据结构 —— 你只是想要知道它们是否是精确相同的对象。(#| 原文:exact same object,也就是比较指针的值是否相等|#)例如,给定两个指向表的指针,你可能想要知道指针们是否指向同一个表,而不是分别指向两个元素相同的表。
1575 |
1576 | 对于这一点,你可以使用 `eq?`。`eq?` 比较两个值来判断它们是否指向相同的对象。因为 Scheme 中所有的值(观念上)都是指针,这就是个指针比较,所以 `eq?` 很快。
1577 |
1578 | (你可能会想带标记的直接值表示会使 eq? 比简单的指针比较更慢,因为它不得不检查比较的东西是不是真的指针。实际上并不是这样 —— eq? 只是比较了位组合而不关心比较对象是代表指针还是直接值。)
1579 |
1580 | 对数字的相等判定得到了特殊对待。当比较两个数值时,= 是合适的判定。使用 = 有着这样的优点:当 = 被用于非数字值时会出错,Scheme 会在这时进行抱怨。如果你犯了个错误,在本应是数字的地方放了个非数字,Scheme 通常会提示你。(你也可以用 equal?,但是在应用于非数字时,它不会报错,而且 equal? 可能会慢一点。)
1581 |
1582 | 还有另外一个相等判定,eqv?,eqv? 进行数字比较(就像 =),和相等判定(像 eq?)
1583 |
1584 | ==================================================================
1585 |
1586 | 这是 Hunk G 的结尾
1587 |
1588 | 是时候试一试了
1589 |
1590 | 这个时候,你应该去看下一章的 Hunk H,使用Scheme 系统来过一遍例子。之后回来,继续这一章。
1591 |
1592 | ==================================================================
1593 |
1594 |
1595 |
1596 | #### 5.3.选择相等判定
1597 |
1598 | ==================================================================
1599 |
1600 | Hunk I starts here:
1601 |
1602 | ==================================================================
1603 |
1604 | `=` 和 `eqv?` 都被需要的原因是 Scheme 的数字系统,因为效率的原因,不像想象的那么清晰。
1605 |
1606 | 理想情况下,对任何数都有一个副本,所有出现的数字都应该是指向相同且唯一的对象。接下来你就可以使用 `eq?` 进行相等判断,就像你可以将 `eq?` 用于其他类型的值一样。(例如,这样只会有一个浮点数表示 2.36529,任何返回这个浮点数的计算都会返回指向这个唯一对象的指针。((eq? 2.36529 2.36529) 会返回 #t)
1607 |
1608 | 不幸的是,对数字来说,这样做的代价太过昂贵 —— 它需要在系统中一直保持一个包含所有数字的表,并利用这张表来消除对相同值的复制拷贝。作为对效率的妥协,Scheme 允许同一数字存在多个副本,`=` 和 `eqv?` 判定掩盖了这一赘余 —— 处理数字时,它们进行数值比较,这样一来,你不必单型两个有相同数值的数值是否是相同的对象。
1609 |
1610 | `eqv?` 因此检测两个值是否是“相等的”,当两个对象有相同数值时,它们被当作“相等”,像 = 一样,但对所有其他对象,都被以对象相等进行区分,就像 `eq?` 一样。
1611 |
1612 | 一般而言:
1613 |
1614 | - `eq?` 在对非数值进行快速相等判定时很有用,
1615 |
1616 | - `=` 对数字进行数值比较,
1617 |
1618 | - `eqv?` 像 `eq?` 一样,除了认为相同数字的副本是相同对象这一点,
1619 |
1620 | - `equal?` 对数据结构进行“深度”的比较(它使用 eqv? 对结构中的数字进行判定)
1621 |
--------------------------------------------------------------------------------