项目 3:搭档 B 练习

Project 3 中搭档 B 的第一部分练习(B3–B5):take-all、属性表系统、热点与笔记本电脑。

第 B3 题

为人物定义一个方法 take-all。如果收到这条消息,人物应当 take(拿走)当前地点里所有还不属于任何人的物品。

> (ask someperson 'take-all)

第 B4 题:第一部分

现实中谁都能从谁那里随便拿东西,这很不真实。我们想给角色一项 strength,这样只有第一个人比第二个人 strength 高时,第一个人才能从第二个人那里拿走东西。

不过,我们不打算给 person 类添一个局部 strength 变量而把类搞乱。因为可以预见,随着程序进一步开发,我们想添加的属性还会有很多。人物可以有魅力或智慧;物品可以是食物或不是;地点可以在室内或不在室内。因此,你要创建一个叫 basic-object 的类,它持有一个叫 properties 的局部变量,里面是一张属性—值表,就像第 6 课里 get 和 put 用的那种。不过,get 和 put 针对的是所有操作共用的一个固定表;而这里我们需要每个对象各有一张表。文件 tables.scm 里提供了表抽象数据类型的实现:

  • 构造函数:(make-table) 返回一个新的空表。
  • 赋值函数:(insert! key value table) 向表中添加一组键值对。
  • 选择函数:(lookup key table) 返回对应的值,若表中没有该键则返回 #f。

表的实现原理见 SICP 3.3.3(第 266–268 页)。目前先把它当作现成的基本工具。

你要修改 person、place 和 thing 类,让它们继承自 basic-object。这个对象会接受一条 put 消息,使下面的调用行为正确:

    > (ask Brian 'put 'strength 100)

此外,basic-object 应当把任何其他不认识的消息都当作对同名属性的请求,因此:

    > (ask Brian 'strength)
    100

应当无需在类定义里写一个显式的 strength 方法就能工作。

别忘了,属性表机制在查询表中没有的属性时会返回 #f。这意味着,即使我们没有往某个对象里 put 过该属性,下面这样的调用也永远不该报错:

    > (ask Brian 'charisma)

这对真假型属性很重要:除非我们显式 put 一个 #t 值,它们会自动是 #f(而不是报错)。

给人物一些合理的初始 strength。(每个新实例化的人物对象都应当一样。)以后他们可以通过吃东西变强。

第 B4 题:第二部分

你会注意到,类型判定过程 person? 会检查参数的类型是否是列表 '(person police thief) 的成员。这意味着 person? 过程必须维护一个所有继承自 person 的类的列表——如果我们新建一个子类,这就很烦人。

我们将利用属性表来实现一个更好的类型检查系统。如果我们给 person 类加一个名为 person? 的方法,并让它总是返回 #t,那么任何属于人物类型的对象都会自动继承这个方法。不继承自 person 的对象既找不到 person? 方法,在属性表里也没有 person? 条目,于是会返回 #f。

同理,地点应当有 place? 方法,物品应当有 thing? 方法。

> (ask brian 'person?)
 #t

请添加这些类型方法,并把(位于 adv.scm 最底部的)类型判定过程的实现改成这个新版本。别忘了加上 place? 的定义。

新的类型判定过程应当实现如下行为:

 > (person? brian)
 #t
 > (place? soda)
 #t
 > (thing? coffee)
 #t

记住,person? 应当对继承自 person 的类(比如后面定义的 thief 和 police)也有效。place? 和 thing? 同理。

第 B5 题:第一部分

当今时代,很多地方允许你联网。请把 hotspot 定义为一种允许网络连接的地点。每个热点都应当有一个 name 和一个 password 作为实例化变量,连接时必须知道它们。

> (define library (instantiate hotspot 'library 1234))   
;热点的名字是 library,密码是 1234

(注意:我们设想的是按网络设密码,不是像你用 AirBears 那样按人设密码。)热点有一个 connect 方法,接受两个参数:一台 laptop(一种 thing,马上要发明)和一个密码。如果密码正确,且笔记本电脑在该热点内,就把它加进已连接笔记本电脑的列表;否则返回一个错误。当笔记本电脑离开热点时,把它从列表中移除。

> (ask library 'connect somelaptop 1234)

热点还有一个 surf 方法,接受两个参数:一台笔记本电脑和一个文本字符串,例如:

    "http://www.cs.berkeley.edu"

如果笔记本电脑已联网,那么 surf 方法应当执行:

(system (string-append "lynx " url))

其中 URL 是文本字符串参数(注意 “lynx ” 里 x 后面有个空格)。否则返回一个错误。

> (ask library 'surf somelaptop "http://www.cs.berkeley.edu")

第 B5 题:第二部分

现在来发明 laptop 类。一台笔记本电脑有一个实例化变量,即它的名字。

> (define somelaptop (instantiate laptop 'somelaptop)

笔记本电脑是一种 thing,另有两个方法:connect,接受一个密码作为参数,向笔记本电脑所在的地点发一条 connect 消息。如果密码错误,返回一个错误。

> (ask somelaptop 'connect 1234)

笔记本电脑还有另一个方法 surf,接受一个 URL 文本字符串作为参数,向它所在的地点发一条 surf 消息。因此,每当笔记本电脑进入一个新的热点,用户就必须请求连接该热点的网络;笔记本电脑离开热点时,必须自动断开网络连接。(如果它在一个非热点的地方,surf 消息不会被理解;如果它在热点里但未连接,返回一个错误。)

> (ask somelaptop 'surf "www.berkeley.edu")