关于SICP中练习2.79与练习2.80

在SICP第二章的后半部分,基本讲的都是面向对象编程了,只是作者并不这么称呼而已。其实如果类比Java,那么消息传递可以类比于泛型,而数据导向就好比是多态,只不过这里要原生态得多。
起初,我看到练习2.79的时候是一头雾水,以为编辑是不是搞错排版的顺序了,因为这道练习里牵扯到类型转换的内容,这应该是后面一小节的内容才是,那么这道练习过早得出现在这里实属不应该啊。因为equ?过程中有两个参数,而且题目中也明确指出,这两个参数可以是基本类型、有理数、复数之中的任意一种;所以,两个参数的组合情况就应该有九种。那么把九种都写出来?这显然是极度不明智的。因为,比如再添加进一种四元数进去的时候,难道要写16种情况? 最优雅的解决办法应该是在比较之前提前进行类型的转换,把这两个参数都统一转换成同一类型的,然后再比较,那就省事多了。这就有点像党国推行的普及普通话政策一样,让每个地区的人都说上普通话,那不管你出生地的方言如何晦涩难懂,大家就都可以东南西北无障碍交流了。再顺着这个思路下去,那转换成什么类型好呢,那当然是复数最好。因为复数的真子集是有理数,而有理数的真子集是基础数据类型,所以这样的转换就不会失真。
到这里,练习2.79的基本思路算是有了,但是我们先放一放,看看练习2.80再说,因为练习2.80更简单。但是这里有一个陷阱,就是numer与denom这两个过程。当我们仔细看书本上给出的有理数安装包时会发现,这两个方法被封装了起来,没有提供公用接口,拿Java的话来说就是有理数对象的numer与denom这两个属性的get方法是private的。但是我们在练习2.79与练习2.80中还是需要用到他们的,虽然有点破坏封装性,但是恕我直言,我目前想不到更好的方法来解决了。因此我们先来提供这两个方法的外部接口:

; 安装有理数选择器
(define (install-rational-selector-package)
    (define (numer x) (car x))
    (define (denom x) (cdr x))
    (put 'numer '(rational) numer)
    (put 'denom '(rational) denom)
    'done)

; 有理数选择器
(define (numer r) (apply-generic 'numer r))
(define (denom r) (apply-generic 'denom r))

这里要千万注意,选择器的实现不能使用get过程,而必须要使用apply-generic过程!因为书中之前给出的代码中所有的数据都是有分层标签的,这有点像OSI的七层结构,都是不断打包与不断拆包的对应过程。而get出的只是同一层内之间的调用关系,而apply-generic则可以实现跨层地调用,这才是我们想得到的效果。(其实本质就是相差了一个标签而已,不过这里还是想再说一句,书中那个数据导向风格的apply-generic过程写得真是漂亮,不愧为神书的称号,一流的水准!)
写完这两个过程,我就可以来做练习2.80了,现在就变得非常简单了:

; 安装分派函数
(define (install-eq-zero-package)
    ; 基本数比较
    (put '=zero? '(scheme-number)
        (lambda (x) (= x 0)))
    ; 有理数比较
    (put '=zero? '(rational)
        (lambda (r)
            (and (= (numer r) 0)
                (not (= (denom r) 0)))))       
    ; 复数的比较
    (put '=zero? '(complex)
        (lambda (z)
            (= (magnitude z) 0)))
    'done)

; 通用型比较
(define (=zero? x)
    (apply-generic '=zero? x))

至此练习2.80解决。我们再回过头去看练习2.79就会发现豁然开朗的感觉了。首先,我们需要实现所有数据类型向复数转换的通用转换过程:

; 安装转换器
(define (install-type->complex-package)
    ; 将基本类型转换成复数
    (define (scheme-number->complex number)
        (make-complex-from-real-imag number 0))
    ; 将有理数类型转换成复数
    (define (rational->complex rat)
        (let ((n (numer rat))
              (d (denom rat)))
            (div (make-complex-from-real-imag n 0)
                (make-complex-from-real-imag d 0))))
    ; 导入转换
    (put 'transform '(scheme-number) scheme-number->complex)
    (put 'transform '(rational) rational->complex)
    (put 'transform '(complex) (lambda (z) (attach 'complex z)))
    'done)

; 将所有类型统一转换成复数
(define (transform type) (apply-generic 'transform type))

当然,你会发现向div这种过程,我们已经知道它只是作用于两个复数类型上,所以完全可以用get复数中的div过程来代替,这样还能省下一次对apply-generic过程的调用。但是在这里我并不想这样做,其一是这样写代码太不美观了,可读性降低了,其二是破坏了封装。这当然也因人而异,各有所好,并不绝对。
好了,有了transform这个神奇的过程,不管他是有9种还是16种情况,接下来一种写法就可以搞定了:

; 比较任意两个类型的数是否相等
(define (equ? number1 number2)
    (let ((z1 (transform number1))
          (z2 (transform number2)))
        (=zero? (sub z1 z2))))

至此练习2.79解决。这里同样,=zero?与sub过程为了代码的美观性与封装性,我并没有用get去做。

; 在练习2.85中 equ? 必须如下实现
; 否则在执行 2.85中改写后的apply-generic时候
; 会产生无限递归
; 是因为 equ? 使用了 sub 定义

; 用于判断两个数是否相等的谓词
(define (install-equal-package)
    ; 比较两个基本数据
    (put 'equ? '(scheme-number scheme-number)
        (lambda (x y) (= x y)))
    ; 比较两个有理数
    (put 'equ? '(rational rational)
        (lambda (x y)
            (let ((r-x (attach-tag 'rational x))
                  (r-y (attach-tag 'rational y)))
                (or (and (= (numer r-x) (numer r-y))
                         (= (denom r-x) (denom r-y)))
                    (and (=zero? r-x)
                         (=zero? r-y))))))
    ; 比较两个实数
    (put 'equ? '(real real)
        (lambda (x y) (= x y)))
    ; 比较两个复数
    (put 'equ? '(complex complex)
        (lambda (x y)
            (let ((z-x (attach-tag 'complex x))
                  (z-y (attach-tag 'complex y)))
                (or (and (= (real--part z-x) (real--part z-y))
                         (= (imag--part z-x) (imag--part z-y)))
                    (and (= (magnitude- z-x) (magnitude- z-y))
                         (= (angle- z-x) (angle- z-y)))))))
    'done)

; 安装相等的谓词
(install-equal-package)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值