作业 0.1
CS 61AS 第一次作业:Racket 入门练习与自我介绍
作业 0.1 导言
在这次作业中,你将运用目前学到的知识来解决一些问题,还要完成一些阅读,并向大家介绍一下你自己。
模板文件
模板文件为每次作业提供了基本的骨架。
如果你在实验室的计算机上,可以在终端中输入下面的命令,把模板复制到当前目录(注意末尾的那个句点):
cp ~cs61as/autograder/templates/hw0-1.rkt .
你也可以在这里下载模板。
语言声明
你可能注意到,模板文件的第一行写着:
#lang racket
这是在告诉 Racket 解释器:这个文件的内容是 Racket 代码。这看起来有些多余,但 Racket 解释器其实也能理解其他 Lisp 语系的语言,包括你自己定义的语言。
无论如何,你编写的每个 Racket 文件都必须在顶部加上这一行。否则,你会看到这样的错误消息:
default-load-handler: expected a `module' declaration
自动评分器
自动评分器(autograder)是一个程序,用来检查你的代码是否满足某次作业的要求。
如果你在实验室的计算机上工作,grader 命令会运行自动评分器,详见下文。如果你在自己的个人电脑上工作,需要下载 grader.rkt 和 HW 0-1 测试文件。
练习 0
首先,向课程团队介绍一下你自己!
在作业文件中回答以下问题:
- 你的名字是什么?
- 你的专业是什么?
- 你是重修的学生吗?(也就是说,你上学期修过 61AS 吗?)
- 是什么促使你选修 61AS?
- 讲一些关于你自己的有趣的事情。
然后,试着在 Piazza 上找到一个名为”Hello World!”的帖子,在该帖下跟帖介绍你自己。务必包含:
- 姓名
- 专业和年级
- 一个关于你自己的有趣事实
- 你为什么选修这门课
练习 1
定义过程的语法如下:
(define ([name of procedure] [variables]) [body of procedure])
例如,你已经见过如何定义 square 过程:
(define (square x) (* x x))
定义好之后,你就可以用 square 过程来求任意数的平方:
-> (square 3)
9
请借助 square,定义一个名为 sum-of-squares 的过程,它接受两个实参,返回这两个实参的平方之和:
-> (sum-of-squares 3 4)
25
一定要测试你的代码!
写好过程后,运行本次练习的自动评分器,检查你定义的过程是否正确。如果你在实验室的计算机上,在终端输入:
grader hw0-1 hw0-1.rkt sum-of-squares
如果你在自己的电脑上,在终端输入:
racket -tm grader.rkt -- hw0-1-tests.rkt hw0-1.rkt sum-of-squares
中场休息
在介绍下一个练习之前,我们需要先补充一些 Racket 的特性。正在学习第 0 单元的同学可以把这部分当作预习——这些特性我们会在第 0.2 课中深入探讨。
词与句子
我们已经见过一些有趣的、能对词和句子进行操作的过程:
'用来构造一个词(如'pi)或一个句子(如'(good morning))。first接受一个词,返回这个词的首字母;或者接受一个句子,返回该句子的第一个词。butfirst(或bf)接受一个词或句子,返回除第一个字母/第一个词之外的全部内容。
请把这些过程和概念记在心里。它们会在后面的练习和课程中再次出现。
特殊形式
Racket 有一些控制特性,让你可以根据某个测试的结果来选择接下来做什么。这些特性属于特殊形式(special forms)的例子——即具有特殊求值规则的过程。特殊形式我们会在课程后面详细讨论。
if
在 Racket 中,if 是一个接受三个实参的特殊形式。if 总是先求值它的第一个实参。如果该实参的值为 true,if 就求值第二个实参并返回其值;如果第一个实参的值为 false,if 就求值第三个实参并返回其值。
下面是 if 的正确语法示例:
(if (= 5 (+ 2 3))
'yay!
(/ 1 0))
这个表达式的求值结果是词 'yay!。因为第一个表达式为真,if 的最后一个实参不会被求值,所以我们不会遇到除零错误。
cond
cond 是一个特殊形式,行为与 if 类似,但提供了多个选项。各个条件会逐个测试,直到某个条件的求值结果为 true。通常在末尾使用 else 子句,用于处理前面所有条件都为 false 的情况。
示例:
(cond ((= 3 1) 'wrong!)
((= 3 2) 'still-wrong!)
(else 'yay))
在这个例子中,前两个条件的值都是 false,因此整个表达式的求值结果是词 'yay!。
测试条件时,>、< 和 = 都是很好用的过程。
and
and 检查它的实参是否全部为 true:
-> (and (> 5 3) (< 2 4))
#t
-> (and (> 5 3) (< 2 1))
#f
(注意:#t 和 true 可以互换使用,#f 和 false 同样如此。)
为什么 and 是特殊形式?因为它按顺序求值各个实参,一旦能够确定结果就立即停止——只要有某个实参求值为 false,它就立刻返回 false。这非常有用。假设我们有:
(define (divisible? big small)
(= (remainder big small) 0))
(define (num-divisible-by-4? x)
(and (number? x) (divisible? x 4)))
于是我们可以这样做:
-> (num-divisible-by-4? 16)
#t
-> (num-divisible-by-4? 6)
#f
-> (num-divisible-by-4? 'aardvark)
#f
请注意最后一次调用并没有出错。因为 (number? 'aardvark) 求值为 false,and 在求值第二个实参之前就返回了 #f。而如果调用 (divisible? 'aardvark 4) 就会报错:
-> (divisible? 'aardvark 4)
; remainder: contract violation
; expected: integer?
; given: 'aardvark
; argument position: 1st
; [,bt for context]
这条消息的意思很简单:过程 remainder 报错了,因为它期望一个整数,实际得到的却是 'aardvark。
关于 and 有一个微妙的细节:如果它的所有实参都求值为真,它不会简单地返回 #t,而是返回最后一个实参的值。
-> (and #t (+ 3 5))
8
-> (and (- 2 1) 100)
100
任何不是 #f 的值都算 #t。所以,100 是真,'foo 是真,等等。
or
or 检查它的实参中是否有任意一个为 true。
-> (or (> 5 3) (< 2 1))
#t
-> (or (> 5 6) (< 2 1))
#f
为什么 or 也是特殊形式?因为它按顺序求值各个实参,一旦某个实参求值为 true 就立即停止。
> (or #f #t (/ 1 0))
#t
关于 or 也有一个微妙的细节:与 and 类似,只要有一个实参求值为真,or 返回的是该表达式的值,而不是简单地返回 #t。
-> (or #f (+ 1 2 3))
6
-> (or (* 3 4) (- 2 1))
12
练习 2
Part a
先花一点时间读读上面的内容,并在解释器里把每个例子都试一遍。然后,写一个名为 can-drive 的过程,它接受一个人的年龄作为实参。如果年龄小于 16,返回句子 '(Not yet);否则返回句子 '(Good to go)。务必在解释器里测试你的代码。
完成这个练习后,在终端输入以下命令,运行自动评分器检查代码是否正确:
grader hw0-1 hw0-1.rkt can-drive
在你自己的电脑上则输入:
racket -tm grader.rkt -- hw0-1-tests.rkt hw0-1.rkt can-drive
Part b
写一个名为 fizzbuzz 的过程,它接受一个数:如果能被 3 整除,输出词 'fizz;如果能被 5 整除,输出词 'buzz;如果能同时被 3 和 5 整除,输出词 'fizzbuzz;其他情况则输出这个数本身。remainder 函数可能对你有帮助。务必在解释器里测试你的代码。
完成这个练习后,在终端输入以下命令检查你的解答:
grader hw0-1 hw0-1.rkt fizzbuzz
在你自己的电脑上则输入:
racket -tm grader.rkt -- hw0-1-tests.rkt hw0-1.rkt fizzbuzz
练习 3
海象为什么要穿过塞伦盖蒂大草原?
要找出答案,请在 Piazza 上找到标题为 “Answer to Homework 0-1 Exercise 3” 的帖子。
练习 4
在解释器中输入下面的代码片段,看看会发生什么:
(define (infinite-loop) (infinite-loop))
(if (= 3 6)
(infinite-loop)
(/ 4 2))
现在,我们想看看能不能写一个行为与 if 完全一样的过程。下面是我们的一次尝试:
(define (new-if test then-case else-case)
(if test
then-case
else-case))
来试一试:
(new-if (= 3 6)
(infinite-loop)
(/ 4 2))
它失灵了!
再看另一个会出问题的例子:
(new-if (= 3 6)
(/ 1 0)
(/ 4 2))
为什么 new-if 的行为不像 if?从这个例子中,你能对 if 有什么认识?想一想,试着找出原因。以后你还会再遇到这个问题。
推荐阅读
推荐以下阅读材料:
手动测试
在运行自动评分器之前,你应该先在 Racket 解释器中手动测试代码。这很重要,因为自动评分器并不会测试所有可能的情况。
要把单个定义加载进 Racket,先在终端输入下面的命令启动 Racket 解释器:
racket
然后把定义从文件中逐个复制粘贴到解释器里。
要把整个文件加载进 Racket,使用:
racket -it hw0-1.rkt
运行自动评分器
提交任何作业之前,你需要做两项检查:
- 你的作业必须能在 Racket 解释器中加载。任何无法加载的提交都不会获得分数。
- 用自动评分器跑一遍作业,检查你的答案。如果没能让作业通过全部测试,也别烦恼——照常提交就好。记住,作业是按努力程度评分的。
运行自动评分器,在终端输入:
grader <assignment name> <file name>
例如,对这次作业运行自动评分器,在终端输入:
grader hw0-1 hw0-1.rkt
提交作业!
提交方法参见这份指南。其中介绍了基本的终端命令和作业提交流程。
提交过程中遇到任何问题,随时都可以问助教!