Skip to content

1 引言

1.1 我们的哲学

请参见Youtube 视频

1.2 本书的结构

与某些教科书不同,本书并没有采取自上而下的叙述方式,而是采用了对话发展的方式,有 时也会回头描述讲过的话题。如同现实中的程序员,我们通常一步一步来构造程序。有时候 我们的程序也会包括错误,这并不是因为我不知道该怎么写出正确的程序,而是因为这是帮 助你学习的最好方式。错误会迫使你没法被动的学习,而是必须钻研:你永远也没法确信读 到的材料就是真实的。

最终,你会得到正确的答案。短期来说,这种方式使人挫折,而且读者也没法将本书当做参 考书来使用(你没法打开书,翻到随便一页,就认为其中的内容是正确的)。但是,挫败感 是学习的一个部分。我不觉得有好方法绕开它。

在书中你会遇到

练习

这是练习。请做题。

这和传统教材中的练习题一样,需要你独立完成。如果你确实在某个课程中使用本教材,有 可能这就是课后作业。但是本书也包含这种:

思考题

这是思考题,你看到了吗?

当你看到思考题的时候,请停下来。阅读、思考,形成答案之后再继续。这是因为思考 题本质上就是练习题,唯一的区别是后文会给出其答案,或者你可以通过运行程序自行得到 答案。如果你不加思考的继续阅读,那么你就会读到答案(或者,如果答案是可以通过运行 程序获得的情况下,完全忽略答案)。这样做既没有测试你的知识水平,也无法锻炼你的思 维能力。换一种说法,思考题是鼓励你积极学习的一部分。

1.3 本书使用的语言

本书使用的主要语言是Racket。然而,跟很多操作系统 一样,Racket 支持很多编程语言,所以你必须显式的告诉 Racket 你在使用什么语言进行 编程。在 Unix 系统的 shell 脚本中你需要在脚本开头添加如下一行来指明语言:

#!/bin/sh

在脚本的头部,你可能会类似的指定:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" ...>

类似的,Racket 需要你声明所使用的语言。Racket 语言可能使用和 Racket 一样的括号语 法,但是有不同的语义;或语义相同语法不同;或者有不同的语法和语义。因此每个 Racket 程序以#lang <语言名字> 开头。默认的语言为 Racket(名字为 racket)。【注 释】这本书中我们几乎总是使用语言:

plai-typed

在 DrRacket 版本 5.3 中,打开“语言/Language”菜单,“选择语言/Choose Language”菜 单项,然后选择“使用代码中指定的语言/Use the language declared in the source”。

使用该语言时,除非特别指明,请在程序的第一行添加(本书后面例子代码中请假定我们添 加了该行):

#lang plai-typed

Typed PLAI语言和传统 Racket 最主要的不同是它是静态类型的。它还给你提供了些有用 的的东西(construct):define-typetype-casetest。【注释】下面是它们的 使用实例。创建新的数据类型:

它还提供了其他一些有用的命令,比如控制测试输出的命令等。请参考该语言的文档了解 。在 DrRacket 版本 5.3 中,打开“帮助 /Help”菜单,选择“帮助台/Help Desk”菜单项 ,然后在帮助台的搜索栏中输入“plai-typed”。

(define-type MisspelledAnimal
  [caml (humps : number)]
  [yacc (height : number)])

它做的事情类似于在 Java 中:创建抽象类MisspelledAnimal,它有两个实体子类 :camlyacc,它们的构造参数分别为humpsheight

该语言中,我们通过下面方式创建实例:

(caml 2)
(yacc 1.9)

如同其名字暗示的,define-type会创建给定名字的数据类型。当我们把该数据类型的值 绑定到变量时就需要用到其类型:

(define ma1 : MisspelledAnimal (caml 2))
(define ma2 : MisspelledAnimal (yacc 1.9))

事实上这里你并不需要显式的声明类型,因为 Typed PLAI 在很多情况下(包括这里)都能 够推断出正确的数据类型。因此上面的代码可以写成:

(define ma1 (caml 2))
(define ma2 (yacc 1.9))

不过我们倾向于对类型进行显式的声明。这么做一方面是尊崇规则,另一方面当我们日后阅 读代码时有助于理解。

类型的名字可以递归的使用,本书会经常使用这种方式(例 如2.4 节中)。

该语言为我们提供了模式匹配功能,例如这个函数体:

(define (good? [ma : MisspelledAnimal]) : boolean
  (type-case MisspelledAnimal ma
    [caml (humps) (>= humps 2)]
    [yacc (height) (> height 2.1)]))

在表达式(>= humps 2)中,humps被绑定为caml实例的构造时所用到的参数。

最后,你应该编写测试案例,理想情况下,应该在开始定义函数之前写。当然在定义函数之 后也需要写,以防代码被意外修改。

(test (good? ma1) #t)
(test (good? ma2) #f)

当你运行上面的代码时,语言会告诉你两个测试都通过了。要了解更多请参阅文档。

这里有一点可能比较费解。在模式匹配中,匹配数据字段时我们使用了和数据定义时相同的 名字,humps(和 height)。这是完全没有必要的,模式匹配是基于位置的而不是名字 。因此我们完全可以使用其它名字:

(define (good? [ma : MisspelledAnimal]) : boolean
  (type-case MisspelledAnimal ma
             [caml (h) (>= h 2)]
             [yacc (h) (> h 2.1)]))

因为每个 h 仅在其被引入的匹配分支中可见,所以上面的代码没有重名的问题。命名是请 尊崇传统和可读性。通常来说,定义数据类型时可以使用长而描述性的名字;而定义类型子 句时请使用简短的名字,因为日后这些名字会不断被用到。

我觉得很少有需要你会用到类型判断函数(如 caml?),不过你可以用。数据类型定义时还 会生成字段提取函数,例如caml-humps。有时候,直接使用字段提取函数会比使用模式匹 配更简单。当然一般来说还是模式匹配更好用,就如刚才的good?所示。不过为了完整, 我们实现如下:

(define (good? [ma : MisspelledAnimal]) : boolean
  (cond
    [(caml? ma) (>= (caml-humps ma) 2)]
    [(yacc? ma) (> (yacc-height ma) 2.1)]))

思考题

如果给函数传入了错误的数据类型会发生什么?比如传给 caml 构造器一个字符串?或者 传给前述两个版本的good?函数一个数?

Comments