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-type
、type-case
和test
。【注释】下面是它们的使用实例。创建新的数据类型:
它还提供了其他一些有用的命令,比如控制测试输出的命令等。请参考该语言的文档了解。在DrRacket版本5.3中,打开“帮助/Help”菜单,选择“帮助台/Help Desk”菜单项,然后在帮助台的搜索栏中输入“plai-typed”。
(define-type MisspelledAnimal
[caml (humps : number)]
[yacc (height : number)])
它做的事情类似于在Java中:创建抽象类MisspelledAnimal
,它有两个实体子类:caml
和yacc
,它们的构造参数分别为humps
和height
。
该语言中,我们通过下面方式创建实例:
(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?
函数一个数?