关于SICP中练习2.82的实现

起初在看到这道练习的时候是一头雾水,感觉作者真的有点画蛇添足的味道。因为我在前面的文章中提到过,其实我感觉书中第125页上的那个apply-generic过程已经写得很完美了,能用最少的代码来实现过程的多态。所以,我感觉象134页那样的对过程apply-generic的扩展(这里说是扩展,其实是削弱更恰当,因为不定长参数被降低到了两个参数的特殊情况)还是分装进表格中相应的被调用的过程里为好(可能还是我的阅历太浅,体会不到作者的用心吧)。
说了那么多废话,还是来看题目吧。其实,当多读一下题目后就会发现,如果你用其他程序语言的思想去看这题,比如C语言,会觉得相当难以下手,但是如果你用lisp的思维去看待它,就会发现这道题目其实很简单。有一部电影,名字叫做降临,在这部电影中有一种观点我认为很有道理:语言在一定程度上也决定着你的思维方式。这句话我感觉不仅仅在自然语言中适用,在编程语言中也应该适用。说实话,我们大家大都是接受的天朝教育,因此,估计大多数人的计算机母语可能会是C或者是类C的语言(我的计算机母语就是C语言)。因此,当编写程序的时候,自然而然就会率先顺着类C的思维方式去思考程序,但是,这往往不是万能的,所以我们才会看SICP,学习lisp的思维。
lisp是List Processing的递归缩写法,故名思意,这是一种对列表的处理语言。因此,一个合格的lisp程序员心中应该永远把对列表的处理放在第一位。scheme是最简单的一种lisp方言,它对列表的处理主要可以分为三个重要过程:map、filter、accumulate。这三个过程可以构建起对列表处理的几乎所有算法,熟练地运用它们将可以大大的简化代码量,可以说这是函数式编程的精髓所在(在python解释器里也内置了这三个方法,加上python之后也支持了lambda,所以python才敢号称自己也能支持函数式编程)。但是,mit-scheme解释器中只内置了前两个过程,因此,我个人强烈建议将accumulate过程写进解释器的预读取文件scheme.ini中。
下面我们开始用lisp的思维去解决练习2.82的前半部分。首先,题目中说参数列表的强制对每个参数都将试验,直到找到一种合适的,大家都强制成它的类型,这个让大家都能够接受的类型,当然就是参数列表中所有类型的超集类,在参数列表中它的辈分最高----也就是类型塔中最高的那个(如果此时存在类型塔)。那怎么确定谁的辈分最高呢?注意这里最关键,要是用类C的思维去思考,这个问题会变得非常得棘手。所以,我们要学会用函数式编程的思维去解决。有一种和lisp一样的很古老的解释型语言叫APL,用它编写的求一定范围内的质数的程序非常经典,细节我就不在这里多说了,就说一下程序大致的思想,可以简单地总结为两个字,缩放:先放大结果可能存在的空间,然后再缩小范围,一口气得出答案。全程非常干净利落,这也是我们这里所要用到的主要思想。
首先,一个合理的过程的参数数量应该保持在7个左右,这是我们人脑智力所能控制的最佳参数个数,如果再多,那么你就应该考虑将他们打包成列表什么的再传入了。这样,一般合理的过程的参数的数量就不会太多,这给我们之后的参数转换过程将带来便利。我们与其去寻找形参列表中水的辈分最高,倒还不如让每个形参都做一回长辈,从而得到一个包含所有转换可能的二维列表,这就是放大的思想。代码如下:

	; 做参数列表的强制转换过程表
    ; 返回一张二维的过程表
    ; 项目数量 (square (length args))
    (define (coercion-proc-table args)
        (map
            (lambda (x)
                (let ((type-x (type-tag x)))
                    (map
                        (lambda (y)
                            (let ((type-y (type-tag y)))
                                (if (eq? type-x type-y)
                                    (lambda (this) this)
                                    (get-coercion type-y type-x))))
                        args)))
            args))

这里使用嵌套映射生成了一张包含所有强制可能的二维表,表中是每一种可能的转换过程,如果不可能转换,那么根据get-coercion过程的定义,在表中的相应位置就会返回 #f。这里要注意,为了表中的统一性,更是为了方便日后的操作,必须通过 (lambda (this) this) 过程来对两个类型相等的情况作过渡。如果把这张表想象成一个方阵,那么就是其中一条type-x =type-y的对角线的位置全是(lambda (this) this) 过程。
然后就是缩小的思想,用精确的条件去过滤,得到需要的结果:

	; 从表中寻找满足条件的转换过程序列
    ; 如果没有就返回空
    (define (find-proc-list table)
        (filter
            (lambda (proc-seq)
                (accumulate and #t proc-seq))
            table))

注意,这里被过滤器迭代的每一个元素都是表中的一行。在scheme中,有一条准则,当逻辑谓词进行判断时,只要不是 #f 的对象就都认为是真。所以,过滤器的判断条件可以用累加器来实现:先将每一行的所有元素都and起来,只要其中出现了不存在的强制过程,那么相应位置就会是#f ,只要一旦出现了#f,那么累加器最后的计算结果就将是 #f ,即这样的一次尝试不符合,被过滤掉,那么最后剩下的结果中就只能是全部存在相应的强制过程的行。
然后,我们再写一个对形参列表的转换过程,这个就比较简单了:

	; 转换参数列表
    (define (coercion-transform proc-seq args)
        (map
            (lambda (f x) (f x))
            proc-seq
            args)) 

如果对这个写法有疑问,可以去翻看书中的第70页下面的注释。
接下来,为了将来整体代码的可读性,我们把异常过程给封装一下:

	; 抛出过程调用的异常
    (define (no-method-error tags)
        (error
            "No method for these types"
            (list op tags)))

最后,就是写主要的apply-generic过程的逻辑了:

	; 主要逻辑
    (let ((type-tags (map type-tag args)))
        (let ((proc (get op type-tags)))
            (if proc
                (apply proc (map contents args))
                (let ((proc-list (find-proc-list (coercion-proc-table args))))
                    (if (null? proc-list)
                        (no-method-error type-tags)
                        (let ((new-args (coercion-transform (car proc-list) args)))
                            (let ((new-type-tags (map type-tag new-args)))
                                (if (equal? new-type-tags type-tags)
                                    (no-method-error type-tags)
                                    (apply apply-generic (cons op new-args))))))))))

前半部分基本不变,和原先一样,如果能够从表中找到相应参数的过程,就直接分派。但是,后面有一些不同。首先,用一缩一放的方式:
(find-proc-list (coercion-proc-table args))
找出所有可能的转换方式,如果没有就抛异常。
然后随便取出一种方式来进行转换,得到新的形参列表 new-args。之后是最重要的一步,必须检查新的和旧的形参列表是否类型一样,因为,如果一样,并且表中又找不到相应的过程调用,就会出现和练习2.81中第a)小题中的例子一样的问题,导致过程进入无限递归。而且,这里需要注意,必须用equal? 而不能用eq?。后者只是比较指针是否指向同一个对象,但是前者是比较对象的内容与结构是否完全相同,两者区别有点python中的深浅拷贝的意思。最后的递归调用有一点小技巧,千万不能写成像这个样子:(apply-generic op new-args),其中的原因自己慢慢体会吧。我们把上面的代码全综合起来就是这个样子:

(load "accumulate.scm")

; 将过程 apply-generic 推广到n个参数
;   实现策略:
;       试着将所有的参数都强制到第一个参数的类型
;       而后试着强制到第二个参数的类型 并如此试下去
(define (apply-generic op . args)
    ; 抛出过程调用的异常
    (define (no-method-error tags)
        (error
            "No method for these types"
            (list op tags)))
    
    ; 做参数列表的强制转换过程表
    ; 返回一张二维的过程表
    ; 项目数量 (square (length args))
    (define (coercion-proc-table args)
        (map
            (lambda (x)
                (let ((type-x (type-tag x)))
                    (map
                        (lambda (y)
                            (let ((type-y (type-tag y)))
                                (if (eq? type-x type-y)
                                    (lambda (this) this)
                                    (get-coercion type-y type-x))))
                        args)))
            args))
    
    ; 从表中寻找满足条件的转换过程序列
    ; 如果没有就返回空
    (define (find-proc-list table)
        (filter
            (lambda (proc-seq)
                (accumulate and #t proc-seq))
            table))
    
    ; 转换参数列表
    (define (coercion-transform proc-seq args)
        (map
            (lambda (f x) (f x))
            proc-seq
            args))
    
    ; 主要逻辑
    (let ((type-tags (map type-tag args)))
        (let ((proc (get op type-tags)))
            (if proc
                (apply proc (map contents args))
                (let ((proc-list (find-proc-list (coercion-proc-table args))))
                    (if (null? proc-list)
                        (no-method-error type-tags)
                        (let ((new-args (coercion-transform (car proc-list) args)))
                            (let ((new-type-tags (map type-tag new-args)))
                                (if (equal? new-type-tags type-tags)
                                    (no-method-error type-tags)
                                    (apply apply-generic (cons op new-args)))))))))))

至此,练习2.82的前半部分算是完成了,然后考虑后半部分。比方说我在表中导入了如下过程:

; 不要去管过程的实际意义,我乱写的
(put 'las '(scheme-number complex rational)
    (lambda (x z r) (tag (add x (mul z r)))))

那么当形参列表被传入了(rational complex rational)这样的类型组合的时候,apply-generic过程会自动将它强制成(complex complex complex)这样的组合,然后就会因为找不到分派函数而抛异常,自然,表格中原先就导入进去的las过程将不会被考虑了。
至此,全部完成练习2.82。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值