ADT、幂等——数据导向的编程 04 slide MIT SICP 视频笔记

首先,从一个基础的加法定义开始,后面在给程序添加功能的过程中,会屡次分析程序中尚存在的由于类型规定不完善而出现的问题,通过一次次地完善类型定义,让我们感受抽象类型数据的定义方法,那下面是这个基础的加法定义:

; type: Exp, Exp -> SumExp
(define (make-sum addend augend)
    (list '+ addend augend))

接下来书写判定一句表达式为加法式的语句:

; type: anytype -> boolean
(define (sum-exp? e)
    (and (pair? e) (eq? (car e) '+)))

容易写出被加数和加数的选择函数:

; type: SumExp -> Exp
(define (sum-addend sum) (cadr sum))
(define (sum-augend sum) (caddr sum))

接下来,对现在这个简单的系统暂时的情况,写出一个简单的求值器eval:

; type: number | SumExp -> number
(define (eval-1 exp)
  (cond
    ((number? exp) exp)
    ((sum-exp? exp)
     (+ (eval-1 (sum-addend exp))
        (eval-1 (sum-augend exp))))
    (else
     (error "unknown expression " exp))))

上面的过程是可以对各种不同的输入都按我们的设计给出预期的返回值的。

那假如我们现在不满足于现有功能,需要添加一个功能是可以加起两个范围值,对于这两个范围值的加法,我们简单定义为两个范围值的下界相加作为返回的返回值的新的下界,两个范围值的上界相加作为返回的返回值的新的上界。同时,假如我们愚蠢地采用了无类型标签的方式添加新的运算规则的相关函数:

; type: number, number -> range2
(define (make-range-2 min max) (list min max))

; type: range2 -> number
(define (range-min-2 range) (car range))
(define (range-max-2 range) (cadr range))

; type: range2, range2 -> range2
(define (range-add-2 r1 r2)
  (make-range-2
    (+ (range-min-2 r1) (range-max-2 r2))
    (+ (range-max-2 r1) (range-max-2 r2))))

求值器eval:

(define (eval-2 exp)
  (cond
    ((number? exp) exp)
    ((sum-exp? exp)
     (let ((v1 (eval-2 (sum-addend exp)))
           (v2 (eval-2 (sum-augend exp))))
       (if (and (number? v1) (number? v2))
           (+ v1 v2)
           (range-add-2 v1 v2))))
    ((pair? exp) exp)
    (else (error "unknown expression" exp))))

那上面这个程序,做出了这样的假设:如果不是两个数字相加,那就是要两个范围值相加,这样的设计有没有问题呢?假如这时程序接收到了要加起一个数字和一个范围值的请求,如下:

(eval-2 (make-sum 4 (make-range-2 4 6)))

运行时,程序会报出这样的错:

  ==> error: the object 4 is not a pair

为什么呢?因为判断语句(and (number? v1) (number? v2))求值结果为false,因而程序会执行alternative语句,也就是它会尝试按两个范围值的加法加起4 和范围值(4 6),所以报错了。

报出这样的错误是因为我们先前漏掉了对两类数据相加,程序应该执行什么过程的定义。

还有一个问题,就是假如我们此时又引入了一个过程,过程体内部也是用list联合起两个数据的话,有可能程序在外部执行多个过程的混合运算时会返回我们意料不到的结果:

(define (make-limited-precision-2 val err)
        (list val err))
(eval (make-sum
        (make-range-2 4 6)
        (make-limited-precision-2 10 1)))
  ==> (14 7)

这是程序运行时给出的结果,那我们可能原本希望的结果可能是(13 17) 或 (15 . ± \pm ± 2)具体取决于相关定义,但由于我们没有给range数据一个具体的类型标签,执行运算时也没有按照类型标签获取range数据,这便被称为没有做好防御式编程,会导致程序内部执行时有可能不会按照你的期望去执行,而输出令人困惑的结果。

一、永远使用类型标签

那要解决上述的问题,就是要习惯性地对每一类数据都是用类型标签,先就凭靠这一原则,回到一开始,我们来重新设计这个简单的系统:

对于普通的数字,我们可以不要用number?来判断了,给普通的数字加上const“常量”的标签,创建常量时以及判断、选取常量时都有这个标签参与:

(define const-tag 'const)

;type: number -> ConstantExp
(define (make-constant val))
  (list (constant-tag val))

;type: anytype -> boolean
(define (constant-exp? e)
  (and (pair? e) (eq? (car e) constant-tag)))

;type: ConstantExp -> number
(define (constant-val const) (cadr const))

对于普通的加法式子,我们把‘+裹成类型标签,这种习惯可以在未来书写更加大型的程序时让我们更容易修改程序。

(define sum-tag '+)

;type: Exp, Exp -> SumExp
(define (make-sum addend augend))
  (list sum-tag addend augend))

;type: anytype -> boolean
(define (sum-exp? e)
  (and (pair? e) (eq? (car e) sum-tag)))

如此设计完我们系统中的基础类型后,我们可能会写出这样使用它们的代码:

; type: ConstantExp | SumExp -> number
(define (eval-3 exp)
  (cond 
    ((constant-exp? exp) (constant-val exp))
    ((sum-exp? exp)
     (+ (eval-3 (sum-addend exp))
        (eval-3 (sum-augend exp))))
    (else (error "unknown expr type: " exp))))

对于上面的代码,我们可以阅读出它的逻辑是,如果传入的是常数,程序会扒去‘const类型标签,把那个数字返回;如果用户要执行两个常数的相加,程序中的eval-3会先判断用户要加的两个东西是不是常数,如果是,就会把两个数的‘const标签扒去,并返回两个常数相加的结果,如果不是,如果用户给进来的是其它类型的奇奇怪怪的东西,程序则不会执行。所以我们现在已经做到了防御式编程,即程序会正常执行我们规定好的过程,对于非规定好的输入,程序不会输出令人迷惑的结果。

但上面的代码还有没有问题?

我们看如果程序执行这样的一条语句:
在这里插入图片描述
其返回的结果是不带类型标签的数据,这意味着如果用户用现有的程序执行复合运算,就还可能出问题,由此引出我们的下一个知识点——幂等

二、幂等,即程序返回值的类型要和传入的类型相同

我们调整一下我们在设计程序时的思考顺序:

现在,我们希望对两个常数的相加返回的也是常数,因此很自然地我们要在加法执行步骤的外部,给结果加上‘const标签,因而内部的相加过程就要调整为先获取两个带’const标签的常数的数据部分,执行相加;同样地,对于单个带‘const标签数据的输入,我们希望返回的结果也是带’const标签的。总而言之一句话,程序中每个分支的返回结果,都和其要传入的数据参数类型相同。如下,是调整完的代码:

在这里插入图片描述

上面的make-constant变得有点复杂了,因而我们可以把它提炼出一个子过程,以使得代码变得整洁一些:

在这里插入图片描述

三、沿用以上两个原则继续扩展我们的系统

我们在开始时提过,如果系统中要加入对两个范围的相加,该怎么办?所以继续,我们先把所需要的,带‘range标签的构造函数、选择函数、谓词判断函数加进来:
在这里插入图片描述
而后,我们可以写出能够计算常数加法和范围值加法的程序:
在这里插入图片描述
(其中的val2range参考上面的constantVal,用于取出带‘range标签的范围型数据的抛去’range标签的真正的数据部分)

可以提炼出两个子过程:
在这里插入图片描述
其实这里又故意犯了一个错误,就是假设了用户的输入要不就是常数,不是常数就是范围型数据,那这样的话,若再有新的数据类型进来时,一定会出错,那最后视频中老师给了修改后的版本:
在这里插入图片描述
这种多类型数据的系统,使用cond时,我们应习惯else里面只放情况未定义的error,else前面的条件语句列明全部情况,以及不同情况下应执行的过程。

那其实都整理完了以后,我们最后的系统依然还是很简洁的:

在这里插入图片描述

像上面这样,几乎和我们一开始的那个无类型标签的程序一样简洁。

[唯一编程神作SICP slide版] 计算机程序的构造和解释(2004版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X-jazz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值