作业 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

首先,向课程团队介绍一下你自己!

在作业文件中回答以下问题:

  1. 你的名字是什么?
  2. 你的专业是什么?
  3. 你是重修的学生吗?(也就是说,你上学期修过 61AS 吗?)
  4. 是什么促使你选修 61AS?
  5. 讲一些关于你自己的有趣的事情。

然后,试着在 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

运行自动评分器

提交任何作业之前,你需要做两项检查:

  1. 你的作业必须能在 Racket 解释器中加载。任何无法加载的提交都不会获得分数。
  2. 用自动评分器跑一遍作业,检查你的答案。如果没能让作业通过全部测试,也别烦恼——照常提交就好。记住,作业是按努力程度评分的。

运行自动评分器,在终端输入:

grader <assignment name> <file name>

例如,对这次作业运行自动评分器,在终端输入:

grader hw0-1 hw0-1.rkt

提交作业!

提交方法参见这份指南。其中介绍了基本的终端命令和作业提交流程。

提交过程中遇到任何问题,随时都可以问助教!