`
阿尔萨斯
  • 浏览: 4184328 次
社区版块
存档分类
最新评论

SICP 习题 (2.3) 解题总结

 
阅读更多

SICP 习题 2.3 要求我们实现一种平面矩形的表示,定义获取数据的相关选择函数。然后定义几个过程来计算矩形的周长和面积。

接着题目还要求我们实现矩形的另一种表示方式,要求这个新的矩形表示方式同样适用于以上定义的周长和面积计算过程。


有关这道题我们可以通过由上而下的方式进行实现,实现过程也不算复杂,原因是这道题涉及到的数学概念还是比较简单,就是矩形的面积和周长,差不多是我们小学的知识吧。不过题目后面要求我们实现矩形的不同表示方式,还要同时支持同一个计算周长和面积的过程,这点有些麻烦,实际上这道题有点偷跑了,涉及到的一些概念在本书的后面章节中才详细讲到。如果只是使用我们目前学习过的一些Scheme技术,实际上无法妥善的完成这道题目,只能部分实现。


我们先从最上层开始实现,首先是面积计算过程和周长计算过程。


面积计算过程就是长乘以宽嘛,代码如下:


(define (area-rect rect)
  (* (get-length rect) (get-width rect)))

周长计算过程就是长加上宽,然后乘以2”,代码如下:

(define (perimeter-rect rect)
  (* 2 (+ (get-length rect) (get-width rect))))


通过以上过程我们就可以发现,我们需要实现的是get-lengthget-width两个过程,用于获取一个矩形的长和宽。


具体的get-length和get-width如何实现,又取决于我们的矩形数据结构如何实现。

我在下面列出我的第一种实现,实现的方式是通过矩形的两个顶点的坐标来定义一个矩形,代码如下:


(define (make-rectangle-1 corner-1 corner-2)
  (cons corner-1 corner-2))


根据我们上两道题的经验,我们知道这里需要定义两个函数分别用于获取一个矩形的两个顶点,代码如下:

(define (corner-1 rectangle)
  (car rectangle))

(define (corner-2 rectangle)
  (cdr rectangle))


当我们可以获取到一个矩形的两个顶点的数据之后,我们就可以去定义get-length和get-width过程了,代码如下:

(define (get-length rectangle)
    (let ((length1 (abs (- (point-x (corner-1 rectangle)) (point-x (corner-2 rectangle)))))
	  (length2 (abs (- (point-y (corner-1 rectangle)) (point-y (corner-2 rectangle))))))
      (if (> length1 length2)
	  length1
	  length2)))

(define (get-width rectangle)
  (let ((width1 (abs (- (point-x (corner-1 rectangle)) (point-x (corner-2 rectangle)))))
	(width2 (abs (- (point-y (corner-1 rectangle)) (point-y (corner-2 rectangle))))))
    (if (> width1 width2)
	width2
	width1)))


以上代码比较简单,就是通过conner-1和conner-2过程获得矩形的顶点的数据,然后通过point-x和point-y获得这些顶点的x坐标和y坐标,通过两个顶点的x坐标的差和y坐标的差可以得到矩形的长和宽。


以上实现都是比较直接的,和上几道题差不多,接着的问题有些麻烦,就是定义另一种矩形的表示方式,并用同一个面积和周长计算过程来计算矩形的面积的周长。


单纯用另一种表示方式来定义矩形还是比较简单,比如我们可以通过矩形的一个顶点,还有长、宽这三个数据来定义一个矩形。

建构这种矩形的代码如下:

(define (make-rectangle-2 corner width length)
 (cons corner (cons width length)))


以上代码就是简单地使用cons过程将顶点、长、宽连接起来。


对应这样的实现,获取矩形的长和宽的代码就很直接了:

(define (get-length rectangle)
    (abs (cdr (cdr rectangle))))

(define (get-width rectangle)
    (abs (car (cdr rectangle))))



因为同样实现了get-lengthget-width过程,我们可以发现make-rectangle-1和make-rectangle-2做出来的矩形都可以通过area-rect过程来计算面积,也可以使用perimeter-rect过程来计算周长。


也就是说,我们通过get-length和get-width将矩形的实现细节隐藏了。不管使用什么形式实现矩形的数据结构,因为获得长和宽的接口是一样的,所以area-rect和perimeter-rect都可以正确计算矩形的面积和周长。


不过这里有一个问题,就是make-rectangle-1 和 make-rectangle-2 分别对应了不同的get-length和get-width过程,而Scheme里我们并没有看到“方法重载”的实现,在一个运行环境里只允许一套get-length和get-width存在。也就是说,我们定义的两种矩形的实现方法无法在一个环境里并存。


如果我们的目标仅仅是满足题目要求,以上的代码已经可以完成工作了,我们定义了两种矩形实现,他们可以使用同一套面积、周长过程来计算面积和周长。

但是,作为一个有自尊的程序员,以上的代码似乎是无法接受的,两种矩形实现甚至都无法在同一个系统里运行,这算什么事嘛。


一旦你考虑到这一点,你就会发现这还不是一个可以简单解决的问题,这里涉及到“方法重载”的概念,说的直接一点,就是我们定义两个或者多个同名方法(或者说同名函数),它们可以根据参数类型的不同做不同的事情。大概的代码是这样的:

(define (catch 鱼)
	(display “使用网抓鱼”))

(define (catch 兔)
	(display “找个树桩等着”))

(define (catch 虎)
	(display “抓什么抓呀,快逃吧”))



这里有三个catch方法,当我们输入的参数是“鱼”,“兔”,“虎”时,它们完成的工作是不一样的。

当然,以上代码在Scheme里并不能像我们希望的那样工作,在Scheme里只有(catch 虎)这个函数是有效的。因为Scheme不支持过程重载,只有最后定义的过程生效。


如果我们考虑自己实现“方法重载”,我们会进一步发现这里面还有很多问题需要解决,比如我们需要去判断参数的类型,就是判断参数是“鱼”,“兔”还是“虎”。为了实现这一点,我们需要进一步引入“类型”的概念,去判断输入的参数属于“鱼”、“兔”还是“虎”,比如以下代码需要调用抓鱼的过程:

(catch “三文鱼”)    ( catch “大头鱼”)

所以我们需要有办法判断“三文鱼”的“类型”是“鱼”,最后才决定调用抓鱼的过程。


进一步,为了有办法判断“三文鱼”的“类型”,我们需要在保存“三文鱼”的数据结构中增加“类型”这一项。


在支持过程重载的语言中,对于参数类型的判断和对应过程的选择是在底层实现的,开发人员不需要知道。

然而,对于我们目前的学习进度来讲,在语言级别实现的过程重载实在太过超前了,SICP以后的章节中还有详细的讲述,事实上也有不同的实现方法。


所以,在完成本题的过程中,我们还是使用一些简单,或许有些拙劣方法,直接通过类型判断来完成工作。


回到我们的矩形定义,我们需要做的第一件事情是在矩形的数据结构中加入“类型”。考虑到不同类型矩形的数据结构不同,把“类型”这一数据放在第一个位置显然是一个好方法。因为这样可以在拿到矩形数据之后在第一时间取第一个数值进行判断,就可以知道该矩形的类型。

对应的代码如下:

(define (make-wrapped-rectangle-1 corner-1 corner-2)
  (cons 'type-1 (make-rectangle-1 corner-1 corner-2)))

(define (make-wrapped-rectangle-2 corner width length)
  (cons 'type-2 (make-rectangle-2 corner width length)))



这样,为了从“wrapped-rectangle”中取得类型和正真的内容,我们需要定义对应的获取方法,代码如下:

(define (type rectangle)
  (car rectangle))

(define (content rectangle)
  (cdr rectangle))

有了新型的矩形数据结构,我们就可以定义我们新的get-width和get-length过程了:

(define (get-length rect)
  (cond ((eq? (type rect) 'type-1)
	 (get-length-1 (content rect)))
	((eq? (type rect) 'type-2)
	 (get-length-2 (content rect)))
	(else (error "incorrect data type" rect))))


(define (get-width rect)
  (cond ((eq? (type rect) 'type-1)
	 (get-width-1 (content rect)))
	((eq? (type rect) 'type-2)
	 (get-width-2 (content rect)))
	(else (error "incorrect data type" rect))))



以上代码也是比较直接的,就是获得矩形的“类型”,如果是type-1,就调用正真的get-width-1和get-length-1过程,如果是type-2,就调用get-width-2和get-length-2过程。


对应的,我们需要把以前实现的两种get-width过程重新命名为get-width-1和get-width-2过程。

get-length过程也一样,代码如下:

(define (get-length-1 rectangle)
    (let ((length1 (abs (- (point-x (corner-1 rectangle)) (point-x (corner-2 rectangle)))))
	  (length2 (abs (- (point-y (corner-1 rectangle)) (point-y (corner-2 rectangle))))))
      (if (> length1 length2)
	  length1
	  length2)))

(define (get-width-1 rectangle)
  (let ((width1 (abs (- (point-x (corner-1 rectangle)) (point-x (corner-2 rectangle)))))
	(width2 (abs (- (point-y (corner-1 rectangle)) (point-y (corner-2 rectangle))))))
    (if (> width1 width2)
	width2
	width1)))

(define (get-length-2 rectangle)
    (abs (cdr (cdr rectangle))))

(define (get-width-2 rectangle)
    (abs (car (cdr rectangle))))


这样我们就基本完成工作了。


当然,就像前面说到的,这是一种并不高明的办法,不过你也不需要担心,后面的题目会讲到更多方法,其中不乏高明的办法。


继续加油!



分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics