学习目标
- 能够编写对基本数据类型(包括数字、字符串、图像和布尔值)进行操作的表达式
- 能够编写常量和函数定义
- 能够逐步写出简单表达式(包括函数调用)的求值过程
- 能够使用步进器自动逐步执行表达式的求值过程
- 能够使用 DrRacket Help Desk 来发现新的基本操作
Expressions
首先,在打开 DrRacket 并确保顶部工具栏Language > Choose Language打开后,对话框内选择的是Teaching Languages > Beginning Student,点击OK保存。
DrRacket 上方编写代码的部分被称为定义区 (Definitions Area),下方的输出部分则是交互区 (Interaction Area)。
我们可以在定义区编写一个简单的
1 | (+ 3 4) |
之后点击右上角的Run按钮,就可以在交互区看到其7。
从这个例子可以看出,Racket 是通过计算
Expressions
表达式 (Expression) 是程序中被运算 (Evaluate) 以产生值 (Value) 的元素,语法为(<primitive> <expression>)。例如 (+ 3 4) -> 7、(+ 3 (* 2 3)) -> 9、(/ 12 (* 2 3)) -> 2
上述表达式中的<primitive>有+ * /等,它们被称为基本操作符 (Primitive Operator)
关键的是,数字本身也是表达式。
ps: 以防有人不知道,在大多数编程语言中,/是除法
之后,我们可以选中目前已经写好的表达式们,点击Racket > Comment Out with ";",将你选中的表达式
Comment
在 Racket 中,一行代码前的分号;后的所有内容都是注释 (Comment),注释旨在向人传达关于程序的重要信息。Racket 在运行时会忽略这些注释。
ps: 将所有表达式注释掉后,运行将不会有任何值输出
在加减乘除之外,本节还会涉及两个基本操作符,第一个是sqr,即平方;以及sqrt,即开平方。后面的表达式可以传递参数 (Argument) 给这两个基本操作符以将其平方/开平方。
1 | (sqr 3) |
之后会学到 Parameter 的概念,极容易与 Argument 混淆,这里打个预防针
接下来是课后小练习,我们可以先下载来自edX的 pythag-starter.rkt 文件。将这个文件拖动到 DrRacket 中,或者在上方工具栏File > Open...中选择该文件打开。之后就能看到一个带有题目的代码文件。

你可以选中你想要将其变成注释一部分的表达式们,然后点击Racket > Comment Out with a Box,将它们变成注释块。
Solution
已知直角三角形的两条直角边长,求斜边长度,可用勾股定理解决。
(sqrt (+ (sqr 3) (sqr 4))) -> 5
之后我们可以尝试一下运行(sqrt 2)这个表达式,结果的前面会有#i。这是因为2的平方根是个无理数,Racket 语言会通过附加#i来表示这是一个不精确 (Inexact) 的结果。
Evaluation
上一节提到的都是很简单的表达式,但当我们遇到很复杂的程序代码时,我们需要尝试去理解、分析并计算它们。
对于一个表达式(+ 2 (* 3 4) (- (+ 1 2) 3)),我们知道它的值是14。
分析该表达式可以得到它是由以下元素组成的:
+: 加法运算符 (Operator)2,(* 3 4),(- (+ 1 2) 3): 都是参与运算的操作数 (Operand)(+ 2 (* 3 4) (- (+ 1 2) 3)): 整个表达式是一次对基本操作 (Primitive) 的调用 (Call)
ps: 操作数里有两个还能深入分析的表达式,可以同理得到它们的基本操作符、操作数和调用
Primitive Call
Primitive 在这里代指的是诸如+ - * /一类的基本操作,而 Call,即调用则意味着该操作被执行了,所以这个词组在这里更像是描述一个过程。
在计算一个 Primitive Call 时,我们需要随着括号将操作数都算出来,整体运算顺序为从左至右,从里至外,具体运算顺序如下:
(+ 2 (* 3 4) (- (+ 1 2) 3))(+ 2 12 (- (+ 1 2) 3))(+ 2 12 (- 3 3))(+ 2 12 0)14
Strings and Images
这一节会带来两类新的值 (Primitive Value),即
字符串形如生活中的句子、单词等,在描述一串字符的时候,我们通常用"some words"来表示。出现在字符串两端的应当是双引号,而其内部则是真正要表达的一串字符。
可以尝试在 DrRacket 中执行一行"Apple",它的输出结果也仅仅是一行"Apple"。
我们可以围绕字符串做一些操作,比如string-append,这个操作可以将所有字符串拼接在一起。
1 | (string-append "Hello" "World") |
从字符串的定义要求我们知道,它需要两个双引号来包裹自己,所以可以以此清晰的发现123和"123"的区别(一个是数字,一个是字符串)。
数字的一些操作,比如(+ 1 123),我们知道其值是124。但当你尝试(+ 1 "123")的时候,会发现下方的交互区出现了一个
+: expects a number, given “123”
第二个有关字符串的操作就是获取其长度,即string-length,可以试一下诸如(string-length "hello")看看其值是什么。
第三个操作是substring,它的意思是从字符串中
1 | (substring "Caribou" 2 4) |
如果是第一次接触编程,可能会疑惑其值为什么不是"ar",这是因为在绝大多数的编程语言中,
1 | "Caribou" |
以上就是字符串的相关内容,Racket 也对作图做了支持,本节会涉及一种。
为了能方便地操作多个代码文件,我们可以在 DrRacket顶部工具栏中的File > New Tab来创建新标签页。
在编辑区中,先输入(require 2htdp/image),这一行意为从2htdp这个地方image相关的
然后我们在第二行输入(circle 10 "solid" "red"),点击运行,就可以在交互区发现一个红色的实心圆,以此可以得知这一行的意思是创建一个半径为10的红色实心圆。
还可以输入(rectangle 30 60 "outline" "blue"),得到一个高60宽30的蓝色空心矩形。
除了图形之外,也可以渲染文字作为图形:(text "hello" 24 "orange"),得到一个字号为24的橙色Hello。
接下来我们围绕图形做一些操作,尝试如下代码:
1 | (above (circle 10 "solid" "red") |

运行后,就能在交互区发现三个不同颜色、不同大小的圆
1 | (beside (circle 10 "solid" "red") |

或是将它们
1 | (overlay (circle 10 "solid" "red") |

Constant Definitions
在这一节,我们会涉及到
或许在其他编程语言的教程中,初学者会先接触到变量的概念,然后再与常量对比。但在这里,我们会先接触常量,一个或许更接近现实生活的概念。
ps: 其他语言中的常量很可能和这里不太一样,这里的常量更像是 只读的变量
比如Π (≈3.1415) 这个众所周知的常量,它自定义以来就不会变。在程序中也是一样,我们也可以定义一个不变量,并赋予一个名字。
接上一节绘图部分,我们可以定义如下:
1 | (require 2htdp/image) |
正如我们在写数学公式时,涉及有关圆、球一类的运算,可能会将Π写上一样,我们也可以在代码中用它们的(* WIDTH HEIGHT) -> 240000
常量定义的语法是:(define <name> <expression>)
或许会发现一些有意思的地方,由于定义常量时最后的参数是<expression>,我们在定义时并不一定需要填一个确定的数、字符串进去,诸如1/2一类的表达式也是可以的。
所有的值都是表达式。

之后我们就可以对这只猫进行一些绘图相关的操作了,比如让它旋转-10°: (rotate -10 CAT),点击运行后,就可以在交互区看到一只斜着的猫。
再让它旋转10°: (rotate 10 CAT),就能再看到另一只斜着的猫。
我们也可以各自赋予他们一个名字,比如向右倾斜的猫是RCAT,向左倾斜的猫是LCAT:
1 | (define RCAT (rotate -10 CAT)) |
这两只猫就被存在了两个常量中,它们不再是直接的
这一节非常非常重要,不亚于小时候第一次学到 用字母表示数 对数学思想的改变 ——
很多时候我们编写的代码并不能像前几节一样一句推出结果,而是好几步。中间的运算结果需要我们通过define来存储并使用。
Function Definitions
这一节会引入一个新的概念 ——
再开始之前,先下载edX 的 function-definitions-starter.rkt 文件,并在 DrRacket 中打开。代码如下:
1 | (require 2htdp/image) |
该代码文件执行后,应当是形同红绿灯一样的三个圆出现在交互区。
我们观察三行绘图代码,会发现有一些地方是重复出现的,而只有最后的字符串是会变的。这里就能用到函数的第一个功能,
该如何做到这一点呢?回顾曾经学过数学意义上的函数:f(x) = 2*x,如果x=2,结果是4;如果x=6,结果就是12。
编程里的函数和这一过程很像:
- 可以重复使用
- 可以更改传入的值以得到不同的结果
在 Racket 语言中,函数定义语法如下:
1 | (define (<function-name> <parameter> <body>)) |
我们在代码后添加如下函数:
1 | (define (bulb c) ; bulb 是函数名,c 是参数 |
在这个函数里,c就代表着自变量。之后我们就可以再写一行(bulb "purple")来
之后,一开始的那三行代码都可以被这个函数简化掉,变成如下最终效果相同的代码:
1 | (above (bulb "red") (bulb "yellow") (bulb "green")) |
以后使用函数就和用其他学过的基本操作符 (如string-append) 一样了。举个例子,string-append 的作用是拼接字符串,如(string-append "re" "d")的值是字符串"red"。我们就可以将(bulb "red")变成等效的(string-append "re" "d"),层层嵌套。
Booleans and if Expressions
生活中有许多答案是对或错的问题,回答它们或许很简单,但也很具有决定性。对于 Racket 和其他编程语言也一样,对或错的答案会影响程序后续运行的走向。
这一节会引入
在 Racket 中,这两个布尔值的表述为true和false,true和false,交互区就会出现一行true和一行false。
没有问题只有答案十分无趣。让我们在编辑器中定义WIDTH为100、HEIGHT为100:
1 | (define WIDTH 100) |
接下来提问:WIDTH比HEIGHT大吗?
如何让 Racket 回答问题呢?在数学中我们知道比较两个数的大小可以用包括但不限于> < =等符号,在 Racket 中也一样:
1 | (> WIDTH HEIGHT) ; 使用大于号判断 |
当然,也可以试试其他的基本操作符,比如(= WIDTH HEIGHT)和(>= WIDTH HEIGHT)之类的值都是true。
让这种基本操作符或者函数运算得到true或false的行为被称为
字符串的逻辑运算也可以使用,比如(string=? "foo" "bar") -> false,string=?可以比较两个字符串是否相同。
同样的,图像的一些属性也可以拿来比较,比如两个图像的宽度(使用image-width,故image-height同理):
1 | (require 2htdp/image) |
布尔促成了程序if表达式让程序运行出现分支,其语法如下:
1 | (if <expression> ; 一个值为布尔的条件 |
保留之前的代码,让我们来尝试一下if表达式:
1 | (require 2htdp/image) |
ps: 如果第一个表达式,即条件,值不是布尔类型的,会报错,可以试试
当然,(if true "true" "false") -> true,(if false "true" "false") -> false。
本节的最后一个概念是
- 当你的银行账户密码输入正确
且 你的账户内资金足额,那么你就可以取钱。 - 如果你考到托福 110 分
或 你在英语国家生活4年以上,那么你就可以免除英语要求 - 世上有两种人,一类是懂二进制的人,一类是
不懂 二进制的人。
回顾刚刚我们留下的两行图像定义,如果我们需要同时比较I1和I2的高与I1和I2的宽:
1 | (> (image-height I1) (image-height I2)) |
将它们用and的逻辑关系运算,得到最终的值,我们可以写:
1 | (and (> (image-height I1) (image-height I2)) |
and接受两个表达式,且两个表达式的值只能是布尔类型,其运算过程如下:
1 | ; Step 1 |
短路机制
观察代码的第二、三步,你会发现and似乎没有同时将两个表达式运算出来,而是有先后的。
这个现象在各编程语言都存在 —— 考虑到性能问题,它们会先看前面的表达式的值是不是false,and来说,它需要两个表达式同时为true,所以只要有一个不是true,and运算就可以立即终止了。
这一现象在or和not这类需要将所有表达式都得运算一遍的逻辑运算符身上不存在。
Using the Stepper
在介绍
1 | (require 2htdp/image) |
这一大段代码看着很困惑,可以先运行下看看会输出什么:7与20。
大多数时候,我们可以逐句分析代码的过程并在脑海中构思,但对于一些复杂的情况,我们需要借助 Racket 自己的工具来帮我们还原代码执行过程。
我们点开Run按钮左边的Step,会有一个新窗口弹出:

在窗口的左边是
比如第一步,左边的绿色部分会运算成右边的紫色部分。点击上方的Next就可以让 Racket 执行下一步运算,显然,这一行表达式的最终值是7。
我们直接到了最后一行,即(max-dim (rectangle 10 20 "solid" "blue")),里面的rectangle在右边会被运算成一个小蓝色矩形。
再下一步,神奇的事情出现了,上面对max-dim的定义内容全部都复制到了刚刚的蓝色矩形上 —— max-dim的img都被替换成了蓝色矩形,参与运算。
之后,Racket 开始处理if表达式的image-width和image-height变成了10和20。然后(> 10 20)的值显然是false,最后就走向了第二个表达式 —— 算蓝色矩形的高度,值为20,并输出到交互区。
程序结束了,如果对于上面的步骤不太清楚,可以点击Previous来返回上一步。
Stepper 让我们能更清晰的知道 Racket 是如何一步步处理我们的程序的,这在学习该语言中帮助很大。
ps: 在其他语言当中,类似的功能是 Debugger,即调试器
Discovering Primitives
在前面的小节中,许多基本操作符是渐进地出现的:
- 在学习数字时,同时认识到
+ - * / - 在学习字符串时,我们知道用
string-append等来操作它们 - 在学习图像时,
image-width和image-height可以获取它们的宽高 - 在学习布尔运算时,我们接触了
< > =和and or not等
但教学内容是有限的,Racket 语言自带的基本操作符还有很多很多,我们该如何接触那些从未认识过的基本操作符呢?
其中一个方法是:rectangle和circle来绘制,比如:
1 | (require 2htdp/image) |
那么我们就可以合理猜测:triangle是否存在?假设它存在,那么对于一个三角形来说,它也许需要尺寸、是否实心、颜色什么的:(triangle 40 "solid" "purple")
将这一行代码放进编辑器中运行发现,交互区竟然真的出现一个紫色三角形。但疑惑的事情出现了,
接下来让我们把鼠标指针放在triangle上:
- 如果你是 Windows 系统,请右键
- 如果你是 MacOS 系统,请摁住Ctrl的同时按下鼠标
在弹出的框中,点击Search in Help Desk for "triangle",之后会出现一个网页。

从之前的图像绘制中我们知道这一切都是因为我们导入了2htdp/image这个库,让我们点击上图中的第一行:triangle provided from 2htdp/image,进入到详情页:

我们观察文档上描述triangle的语法,会发现第一个参数的意思是side-length,即边长。这下破案了,我们刚刚填的40实际上是指三角形的边长。
接下来是第二种探究方法,我们先开个新标签页,填入:(/ 3 4),根据之前的学习,当然知道它的值是0.75。
在一些场景中,我们需要知道一次除法运算结果
我们再次以同样方式,点击/的Search in Help Desk for "/",选中网页内的/ provided from lang/htdp-beginner。
里面有大量的基本操作符,我们需要极具耐心地在这一页寻找四舍五入的写法。可以借助浏览器的全局搜索(一般来说可以按下 Ctrl + F ),然后输入round。之后我们就会被定位到round部分的所在位置。
四舍五入的写法:(round <real number>),可以直接复制下来到 DrRacket 尝试,(round (/ 3 4)) -> 1。
借助文档是学习并深入了解一门编程语言几乎最重要的方式。
Practice Problems
这一章的 Recommended Problems:
- BSL P1 - More Arithmetic Expressions
- BSL P3 - Tile
- BSL P5 - Compare Images
- BSL P6 - More Foo Evaluation
- BSL P15 - Function Writing
- BSL P16 - Foo Evaluation
BSL P1 - More Arithmetic Expressions 题解
预计耗时:5 min / 简单
这道题让我们用两个表达式算出3, 5, 7的乘积。
第一个表达式应当为简单的三个数相乘,我们需要知道*是可以接受不止2个参数的:
1 | (* 3 5 7) |
第二个表达式即限制了*只接受两个参数,让我们嵌套着写,先算3*5,再算15*7:
1 | (* 7 (* 3 5)) |
BSL P3 - Tile 题解
预计耗时:5 min / 简单
这道题是让我们还原一个蓝黄矩形,我们可以考虑使用above和beside拼凑。为了方便,我们先定义一个黄色正方形和蓝色正方形:
1 | (define BLUE (rectangle 20 20 "solid" "blue")) |
之后会发现这个矩形的拼凑逻辑可以是:

实现代码如下:
1 | (require 2htdp/image) |
BSL P5 - Compare Images 题解
预计耗时:7 min / 简单
这道题让我们对图像进行三次带有逻辑运算的比较。
第一个:判断IMAGE1是否比IMAGE2高?
1 | (> (image-height IMAGE1) (image-height IMAGE2)) |
第二个:判断IMAGE1是否比IMAGE2窄 (宽度小)?
1 | (< (image-width IMAGE1) (image-width IMAGE2)) |
第三个:判断IMAGE1和IMAGE2的宽高是否都相同?
1 | (and (= (image-height IMAGE1) (image-height IMAGE2)) |
BSL P6 - More Foo Evaluation 题解
预计耗时:7 min / 简单
比较讨厌的逐次人脑运算,从(foo (+ 3 4))开始:
- 从运算顺序来讲,
(+ 3 4)先被算出为7,得到(foo 7) - 之后它调用了
foo函数,那么我们就把函数的内容(即(* n n))复制到下面,并把7填进去,得到(* 7 7) - 最后计算
(* 7 7)得到49
结果为:
1 | (foo (+ 3 4)) |
BSL P15 - Function Writing 题解
预计耗时:5 min / 简单
还是讨厌的逐次人脑运算,从(foo (substring "abcde" 0 3))开始:
- 从运算顺序来讲,
(substring "abcde" 0 3)先被算出为"abc",得到(foo "abc") - 之后它调用了
foo函数,那么我们就把函数的内容复制到下面,并把"abc"填进去,得到:
1 | (if (string=? (substring "abc" 0 1) "a") |
- 这是个
if表达式,我们先计算它的条件,即(string=? (substring "abc" 0 1) "a"),得到:
1 | (if (string=? "a" "a") |
- 这个条件的值为
true,故:
1 | (if true |
- 那么我们就可以提取出
if表达式条件为true时所运行的表达式了,即(string-append "abc" "a") - 它的值是
"abca"
结果为:
1 | (foo (substring "abcde" 0 3)) |
BSL P16 - Foo Evaluation 题解
预计耗时:15 min / 中等
这道题需要自定义一个函数:接受两个数,返回较大的数。也就是说这道题需要用到if表达式,那么我们可以先构思这个条件判断:
1 | (if (> a b) ; 判断 a 是否大于 b |
之后我们将这个if表达式放进一个define里面,将该函数命名为foo (其他什么的也可以):
1 | (define (foo a b) |