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?函数一个数?

results matching ""

    No results matching ""