在上一章,我们体验了 Racket 的语言设计,也简单介绍并使用了
本章会详细讲解 Racket 语言中的函数,以及如何将自己的大脑改造成函数。
学习目标
- 能够运用《如何设计函数》(HtDF)方法设计处理原始数据的函数
- 能够阅读完整的函数设计并识别其不同组成部分
- 能够评估各元素在清晰度、简洁性及彼此一致性方面的表现
- 能够评估整个设计对给定问题的解决效果
HtDF Recipe
在本节开始之前,下载来自edX的 double-starter.rkt 文件,将其放在 DrRacket 内:

HtDF (How to Design Functions) Recipe 告诉我们如何设计出一个好的函数,如何将大事化小,逐个解决。也就是说,在学习函数的开始,我们会将 Recipe 用作学步车,之后熟练运用的时候可以不逐步遵循。
- 如果你正处于 edX - How to Code: Simple Data 的课程中,可以访问edX 内的 HtDF 指南
- 如果没有,可以访问Racket 官方的 HtDF 文档
先让我们看看刚刚的问题:遵循要求设计一个函数,接受一个数字,得到该数字的两倍。
第一步,写出它的
1 | ;; Number -> Number ; 这是函数的签名,即传入什么类型,得到什么类型 |
多个参数的签名
如果签名的参数涉及多个,请写;; Image Image -> Boolean,用空格,而不要用,一类东西隔开。
桩是指函数的定义,其包含一个可用的函数名、符合要求的参数数量以及一个是正确返回类型的值。
1 | (define (double n) 0) |
ps: 如果你学过其他语言,需要注意桩这个奇怪概念,以及 double 是函数名而非类型
格式规范
函数的签名和目的均一行表述完毕,且开头为;; 的注释,以便与后面的代码区分。
1 | ;; Number -> Number |
对于桩,前面只有; ,是因为之后会把注释删掉以便使用。
1 | ;(define (double n) 0) |
即 ;; 用于表述一些帮助文本,在之后不会被删掉;; 是临时注释。
截至目前,我们接触到的基本类型有:Number、String、Image、Boolean。
第二步,我们引入check-expect表达式,这个表达式可以传入两个别的表达式,用于判断它们是否相等。相比于我们学过的=运算符来说,它不仅仅是简单的布尔或if判断,更属于
当我们写出如下代码并运行:
1 | (define (double n) 0) |
会有一个窗口出现,告诉你这两个测试(即刚刚我们写的两行check-expect)都失败了。这一步被称为
第三步,我们开始设计这个函数了,先写出它的
1 | (define (double n) ; 这就是模板,虽然和之前的桩一样达不到目的 |
第四步,开始写函数的check-expect成立呢?
(check-expect (double 3) 6)就是(check-expect (double 3) (* 2 3))(check-expect (double 4.2) 8.4)就是(check-expect (double 4.2) (* 2 4.2))
之后,我们知晓这个函数应当让n变成它的两倍,即(* 2 n),就可以把它作为函数体填进去了。
1 | (define (double n) |
重复声明
当定义了一个新的函数或是常量之类,赋予其名时,被称为声明 (Declaration)。在整个程序运行中,我们需要注意这一点,如果你的程序出现了两个相同名字的函数、常量,有可能会出现错误。比如以下代码:
1 | (define (double n) 0) |
运行代码之后,交互区出现Both tests passed!,就说明我们通过测试了。
总之,写一个函数的步骤大概是:
- 定下函数的签名和目的,为此写个桩,给函数占个位置,在测试的时候至少可以运行
- 写几个
check-expect表达式用来当这个函数的样例 - 完善函数的模板
- 写函数体
- 通过测试
Simple Practice & When Tests are Incorrect
ps: 由于 edX 的这门课需要付费才能查看 Graded Assignments,本人只能根据题目要求来写了
这一节我们会面临第一个 HtDF 问题,即写一个真正有意义的函数:设计一个函数,传入一个单词,得到它的复数形式,假设给任何单词末尾加个s就够了。
根据上一节提到的步骤,我们可以先
1 | (define (pluralize word) "s") |
之后为这个桩函数写一些check-expect表达式,代入我们预期的结果:
1 | (check-expect (pluralize "apple") "apples") |
这三个测试肯定是不会通过的,因为我们还没有word做一些运算的:
1 | (define (pluralize word) |
然后就是最重要的部分 —— 实现,我们需要知道怎么给单词后面加s:单词是个"s"当然也是个string-append表达式:
1 | (define (pluralize word) |
最后如果能通过测试,这个函数就完美地写完了。
再试试
实现一个函数:传入一个单词(是问候语,如"hello", "Bye"一类),得到它后面加了!的单词。(如”"hello!")
注意
我们在编写函数的时候,有时候会出错,无论是语法还是逻辑,最终导致测试不通过。这里我们不仅仅需要思考是不是函数写错了,同时也要确保你的测试本身是没有问题的。比如,在验证一个求正方形面积的函数:
1 | (check-expect (area 3) 3) ; ??? 为什么边长为3的正方形面积为3,不是9吗 |
Varying Recipe Order
在初学设计函数的时候,困惑是常有的:
- 前面的部分看起来既麻烦又啰嗦,但实际上,熟练后的函数设计过程是近乎无感的。
- 函数体是一个函数最重要的部分,决定了函数本身的功能。
- 不确定函数签名时候,思考这个函数需要什么、得到什么,先把测试样例写出来,再回头写签名。
下载来自edX的 image-area-starter.rkt 文件,将其放在 DrRacket 内:

这道题要求我们设计一个函数:接受一个图像并得到图像的面积,也就是说我们需要将这个图像的宽高相乘得到一个值。
至此,我们已经得到一个;; Image -> Number,且函数的;; produce image's width * height (area)。
这俩明确后,就可以写(define (image-area img) 0)。
然后就是函数的
1 | (check-expect (image-area (rectangle 2 3 "solid" "red")) (* 2 3)) |
之后运行测试,显然是不通过的。
ps: 如果遇到了 rectangle: this function is not defined,请在程序开头加上 (require 2htdp/image) 以引入图像库
经过最初的思考,我们需要围绕传入的图像做运算,即这个函数是围绕img在做事情的,所以这是它的
1 | (define (image-area img) |
函数的所有前期构思都结束了,接下来就是最重要的函数体编写,在此之前可以把上面写过的桩函数和模板都注释掉,写一个完整可用的函数:
1 | (define (image-area img) |
最后,运行测试通过,完整代码如下:
1 | ;; Image -> Natural |
类型
在本节之前,所有提到的Number代指,但到了后期,这一表述将会变得不再准确,比如
Racket 语言的数字类型其实分为很多种,刚刚写的函数签名,更确切地来说,应当是:;; Image -> Natural,即返回值类型是个
当然,可以仔细翻阅 Racket 文档,也会发现在 Racket 中,Natural和Exact-Nonnegative-Integer是同义词。
Poorly Formed Problems
在这一节,我们会遇到表述没有那么清楚的题目。在之后的函数设计中,明确知道函数的需求本身已经将函数设计完成大半了,但真正的难点是需求自身的不清晰。
先下载来自edX的 tall-starter.rkt 文件,将其放在 DrRacket 内:

在这道题中,我们需要先确认函数的签名:传入一个图像,得到这个图像高不高。一开始乍一眼可能觉得它的返回值类型是Number一类的,但实际上是个Boolean,即;; Image -> Boolean
同时明确函数的目的,判断图片高不高也是不够细节的,应当是:;; produce true if the image is tall。在明确函数的目的时,需要细节到函数的返回值类型。
确保引入(require 2htdp/image),接下来为它写个桩函数和测试样例:
1 | (check-expect (tall? (rectangle 2 3 "solid" "red")) true) |
问号结尾的函数名
如果题目涉及到诸如判断…,得到一个Boolean之类的,很有可能需要在函数名末尾加一个?来表示它是需要做出判断并直接给出一个 Yes or No 的答案。
考虑样例多样性
在写测试时,有时需要考虑在尽可能多的方面去检测函数的健壮性和安全性。
比如这个判断图像高不高的函数,如果传入一个圆或三角形什么的,是否需要特殊处理?
假设这个函数只需要这一个测试样例,完成它的模板:
1 | (define (tall? img) |
在实现函数体的时候,我们需要先思考如何判断图像高不高 —— 图像的高大于它的宽 —— 需要一个if表达式判断:
1 | (define (tall? img) |
最后运行,测试通过。
测试覆盖率
取决于你的 DrRacket 个性化设置,你会发现代码运行后,false变成了黑底黄字。
这是因为 Racket 发现:运行了程序中所有的check-expect表达式后,有一部分程序始终没有跑过,这部分在 DrRacket 中会被特别标注。
测试覆盖率在大型项目中尤为重要,它决定了整个项目的安全、健壮和可维护性。
在这套课程中,请务必按照最高覆盖率去写,比如:一个函数需要传2个图像,判断第一个图像是否比第二个图像大,这俩图像需要用它们的宽高,总共就会有3*3=9个check-expect
所以我们可以为这个函数再写一个测试,使其可以覆盖到false部分:
1 | (check-expect (tall? (rectangle 3 2 "solid" "red")) false) |
之后,或许还有一个点我们忽略了,由于题目表述模糊,我们并不知道
1 | (check-expect (tall? (rectangle 3 3 "solid" "red")) false) |
再次运行,测试通过,同时我们也实现了很不错的测试覆盖率。
明确所有模糊
题目当中的模糊请使用注释来明确。
Criteria
最后,本节会给出函数的评分标准,主要从四个方面:
- Commit Ready / 提交前检查
- 代码简洁
- 所有测试代码不应被注释,桩和模板应被注释
- 在编辑器里做得草稿最后应被删除
- Design Completeness / 设计步骤完整
- 所有 HtDF recipe 提到的步骤应该完整地呈现
- Internal Quality / 高质量
- 代码里的每一个设计部分需要即整洁又正确,顺序对应
- 函数名望文生义
- 所有测试应通过
- 测试能做到对程序的全覆盖,不漏分支
- Problem Satisfied / 满足题目要求
- 函数设计应遵循题目
- 如果题目有部分模糊,请准确识别出并在设计过程中将其确定
Practice Problems
这一章的 Recommended Problems:
- HtDF P2 - Less Than 5
- HtDF P3 - Boxify
- HtDF P6 - Double Error
HtDF P2 - Less Than 5 题解
预计耗时:10 min / 简单
这道题需要我们设计一个函数:接受一个字符串,判断它的长度是否小于5,根据 HtDF recipe 作答,需要完整注释。
按照步骤,我们先思考函数的
- 签名:
String -> Boolean,这里的坑点是返回值类型应为Boolean。 - 目的:
produce true if length of s is less than 5,时刻注意需要将目的确切到要返回什么 上。
之后再写一些样例,需要覆盖到函数尽可能多的返回可能性,比如:
1 | (check-expect (less-than-5? "helloWorld") false) |
写一个桩函数:(define (less-than-5? s) true),再将其变为模板:
1 | (define (less-than-5? s) |
最后就可以构思函数体了,由于涉及string-length表达式;同时又需要<:
1 | (define (less-than-5? s) |
通过测试,完成题目,答案如下:
1 | ;; String -> Boolean |
HtDF P3 - Boxify 题解
预计耗时:15 min / 中等
这道题让我们设计一个函数:传入一个图像,得到一个被矩形包住的图像。更细节地说:通过创建一个outline rectangle,让它的尺寸刚刚好能够overlay住原图像,比原图宽高各大2个像素。
由题目描述可知函数名定为boxify
- 签名:
Image -> Image - 目的:
puts a box around given image. Box is 2 pixels wider and taller than given image.
ps: 目的描述很难遵循标准答案,此处直接贴上来了
之后为他写样例,这部分在这道题挺难的,算是快把函数体都写出来了:
1 | (check-expect (boxify (ellipse 60 30 "solid" "red") |
写个桩函数:(define (boxify i) (circle 2 "solid" "green"))
桩函数的返回值该怎么写
写任何只要符合返回值类型的值/表达式就行,比如:
- 如果是
Boolean,写true或false都是可以的。 - 如果是
Number,随便写个数字都行。 - 如果是
Image,捏一个图像就可以。
然后将其变为模板:
1 | (define (boxify i) |
从刚刚写的样例就能想到函数体该怎么写了:
1 | (define (boxify i) |
完整代码如下:
1 | ;; Image -> Image |
HtDF P6 - Double Error 题解
预计耗时:7 min / 简单
这道题是道找 Bug 题,也就是说我们需要找出题目里这部分代码的问题,并以最小幅度将其改对。观察代码:
1 | ;; Number -> Number |
经过十分细致的逐行检查后,也许会发现最后一行(* (2 n))这里,里面的2 n不应当被套一层括号。