scheme语言直译为汉语(十八)

一、复数的表示

在之前的文章中,我曾有过一个疑问,就是scheme语言要怎么实现多态?这不,书里好像很快就给出了咱们这个疑问的答案。

我们知道复数有两种表示方式:

1.直角坐标系表示法
实部( z 1 + z 2 z_1+z_2 z1+z2) = 实部( z 1 z_1 z1) + 实部( z 2 z_2 z2)
虚部( z 1 + z 2 z_1+z_2 z1+z2) = 虚部( z 1 z_1 z1) + 虚部( z 2 z_2 z2)

2.极坐标系表示法
模( z 1 + z 2 z_1+z_2 z1+z2) = 虚部( z 1 z_1 z1) + 虚部( z 2 z_2 z2)
幅角( z 1 + z 2 z_1+z_2 z1+z2) = 幅角( z 1 z_1 z1) + 幅角( z 2 z_2 z2)

我们可以把两种表示法在一张图中展示:
在这里插入图片描述

实际上,在实际大部分情况下,程序员们都倾向于使用直角坐标系表示法,而不是极坐标系表示法,原因是在直角坐标系和极坐标系表示法之间的舍入误差。但复数表示的实例可以让我们简明清晰地明白通用型操作的程序要如何设计以及如何实现。

对于上面两种表示法,我们可以用按愿望编程的方法写出下面两种实现构建一个复数的过程程序(愿望是,假如我们已有针对直角坐标系表示法下获取实部的real-part、获取虚部的imag-part以及针对极坐标系表示法下获取模长的magnitude和获取幅角的angle等四个选择函数):

1.直角坐标系下的复数:

(make-from-real-imag (real-part z) (imag-part z))

2.极坐标系下的复数:

(make-from-mag-ang (magnitude z) (angle z))

尝试将上述两个过程直译为汉语:

1.直角坐标系下的复数:

(构建直角坐标系下的复数 (实部 复数) (虚部 复数))
(根据实部虚部构造复数 (实部 复数)(虚部 复数))

2.极坐标系下的复数:

(构建极坐标系下的复数 (模 复数) (幅角 复数))
(根据模长幅角构造复数 (模长 复数)(幅角 复数))

那有了上面的构造函数和选择函数以后,我们就可以用代码描述一下基于复数的加、减、乘、除四种基本过程了(加减用直角坐标系法来描述,乘除用极坐标系法来描述)

(define (add-complex z1 z2)
    (make-from-real-imag (+ (real-part z1) (real-part z2))
                         (+ (imag-part z1) (imag-part z2))))
(define (sub-complex z1 z2)
    (make-from-real-imag (- (real-part z1) (real-part z2))
                         (- (imag-part z1) (imag-part z2))))
(define (mul-complex z1 z2)
    (make-from-mag-ang (* (magnitude z1) (magnitude z2))
                       (+ (angle z1) (angle z2))))
(define (div-complex z1 z2)
    (make-from-mag-ang (/ (magnitude z1) (magnitude z2))
                       (- (angle z1) (angle z2))))

尝试将上述过程直译为汉语:

(定义 (复数加法 复数甲 复数乙)
    (构造直角坐标系下的复数 (+ (实部 复数甲)(实部 复数乙))
                         (+ (虚部 复数甲)(虚部 复数乙))))
(定义 (复数减法 复数甲 复数乙)
    (构造直角坐标系下的复数 (- (实部 复数甲)(实部 复数乙))
                         (- (虚部 复数甲)(虚部 复数乙))))
(定义 (复数乘法 复数甲 复数乙)
    (构造极坐标系下的复数 (* (模长 复数甲)(模长 复数乙))
                        (+ (幅角 复数甲)(幅角 复数乙))))
(定义 (复数除法 复数甲 复数乙)
    (构造极坐标系下的复数 (/ (模长 复数甲)(模长 复数乙))
                        (- (幅角 复数甲)(幅角 复数乙)))

除此之外我们还可以根据两种复数表示方法之间的转化的三角函数公式写出二者之间转化的过程函数:
x = r c o s A r = x 2 + y 2 y = r s i n A A = a r c t a n ( y , x ) x = rcosA \qquad r = \sqrt {x^2 + y^2} \\ \quad y = rsinA \qquad A=arctan(y,x) x=rcosAr=x2+y2 y=rsinAA=arctan(y,x)
while,我觉得上面的公式如果直译为汉语的话,可以这么写:
实 部 = 模 长 ⋅ 余 弦 ( 幅 角 ) 模 长 = 实 部 2 + 虚 部 2 虚 部 = 模 长 ⋅ 正 弦 ( 幅 角 ) 幅 角 = 反 正 切 ( 虚 部 , 实 部 ) 实部 = 模长 \cdot 余弦(幅角)\qquad 模长 = \sqrt {实部^2 + 虚部^2} \\ \quad 虚部 = 模长 \cdot 正弦(幅角) \qquad 幅角 = 反正切(虚部,实部) ==2+2 ==

我觉得“反正切”这个名字似乎还是有些不够简洁,我在想,要不要直接采用“倾角”这样的名称?(英语中,arc是弧的意思,所以客观比较arctan跟中国翻译的“反正切”,前者要更形象一些,我好希望找到一个汉语描述的也很形象的说法,但无奈中国自古并未在“角”这个含义上有太多的发现,甚至古代中国可以说没有现代数学中的“角”的概念,“角”这个字在古代也并不具有如今的数学含义,曾有潜力发展为现代数学中角的概念的,有个说法叫“倨余”,其中“倨”字为钝角之意,“余”为锐角之意,二者合为折线的弯折程度,但说实话,从这个名词出发我依然想不到什么简洁,亲民的对arctan的另一种译法。)我通过阅读与思考似乎暂时还很难给出一个用汉语描述的简洁而又形象的说法?

唔……其实我觉得正切化角,正弦化角,余弦化角是种还凑合的描述方法?但无奈还是多一个字,不够简练。
当然,要是不嫌絮叨的话,还有“由正切求角”这类,但这当然不行,也忒长了,对于一个常用的数学函数来说。
这里推荐一篇讲述了三角函数相关命名的一篇文章:https://zhuanlan.zhihu.com/p/56285253

首先是一种选用直角坐标表示的过程,也就是实部和虚部是直截了当操作的,但模和幅角需要根据三角关系得到:

(define (real-part z) (car z))

(define (imag-part z) (cdr z))

(define (magnitude z)
    (sqrt (+ (square (real-part z)) (square (imag-part z)))))

(define (angle z)
    (atan (imag-part z) (real-part z)))

(define (make-from-real-imag x y) (cons x y))

(define (make-from-mag-ang r a)
    (cons (* r (cos a)) (* r (sin a))))

尝试将上述过程直译为汉语:

(定义 (实部 复数)(前项 复数))

(定义 (虚部 复数)(后项 复数))

(定义 (模长 复数)
    (开方 (+ (平方 (实部 复数))(平方 (虚部 复数)))))

(定义 (幅角 复数)
    (反正切 (虚部 复数)(实部 虚部)))

(定义 (根据实部虚部构造复数 实部 虚部)(序对 实部 虚部))

(定义 (根据模长幅角构造复数 模长 幅角)
    (序对 (* 模长 (余弦 幅角))(* 模长 (正弦 幅角))))

接下来是一种选用极坐标表示的过程,也就是模和幅角都是直截了当得到,但实部和虚部需要根据三角关系得到:

(define (magnitude z) (car z))

(define (angle z) (cdr z))

(define (real-part z) 
    (* (magnitude z) (cos (angle z))))

(define (imag-part z)
    (* (magnitude z) (sin (angle z))))

(define (make-from-mag-ang r a)
    (cons r a))

(define (make-from-real-imag x y)
    (cons (sqrt (+ (square x) (square y)))
          (atan y x)))

尝试将上述过程直译为汉语:

(定义 (模长 复数)(前项 复数))

(定义 (幅角 复数)(后项 复数))

(定义 (实部 复数)
    (* (模长 复数)(余弦 (幅角 复数))))

(定义 (虚部 复数)
    (* (模长 复数)(正弦 (幅角 复数))))

(定义 (根据模长幅角构造复数 模长 幅角)
    (序对 模长 幅角))

(定义 (根据实部虚部构造复数 实部 虚部)
    (序对 (开方 (+ (平方 实部)(平方 虚部)))
          (反正切 虚部 实部)))

上面的两种不同实现都能保证add-complex、sub-complex、mul-complex和div-complex在它们各自之上均可正常工作。

下一步,就到了咱们本节要讨论的重点了,上面的底层实现不同的两类同名过程,如何在一个系统中和谐共存。我不知道其它读者怎么想啊,我觉得书中后面的部分是启发了我对于多态的底层实现的原理的。其实很简单啊,我们在C++中能很容易写出有多态特性的函数,就比如function(int a, int b)和function(double a, double b)两个函数摆在这,后面调用两个函数的时候编译器可以根据传入的数据类型自动调用该调用的那个。这里其实最直观简明的一种实现方案(我相信对于C++这种更为底层的语言有更高效的实现方式,但现在不妨就想想最直观的一种),就是可以把int,double这种类型标签存储为序列式数据中的一项,在发生调用时,比对出序列中存储数据类型标签的那一项是啥就行了,然后根据比对出的数据类型的结果执行对应该执行的函数。

ok,这事儿捋完了,咱们就来看看书中是怎样用这种加标签的方式,让两种不同的复数表示能在一个系统中和谐共存的吧:

首先我们需要一个像上面我聊的那种给数据内容加类型标签的过程,以及判断数据的类型标签是什么的过程:

(define (attach-tag type-tag contents)
    (cons type-tag contents))

(define (type-tag datum)
    (if (pair? datum)
        (car datum)
        (error "Bad tagged datum -- TYPE-TAG" datum)))

(define (contents datum)
    (if (pair? datum)
        (cdr datum)
        (error "Bad tagged datum -- CONTENTS" datum)))

尝试将上述过程直译为汉语:

(定义 (绑定标签 类型标签 内容)
    (构建序对 类型标签 内容))

(定义 (类型标签 数据内容)
    (如果 (成对? 数据内容)
          (前项 数据内容)
          (错误 “错误的带标签数据 —— 类型标签” 数据内容)))

(定义 (内容 数据内容)
    (如果 (成对? 数据内容)
          (后项 数据内容)
          (错误 “错误的带标签数据 —— 内容” 数据内容)))

(定义 (数据部分 带标签数据)
    (如果 (成对? 带标签数据)
          (后项 带标签数据)
          (错误 “错误的带标签数据 —— 数据部分” 带标签数据)))

(定义 (类型标签 带标签数据)
    (如果 (成对? 带标签数据)
          (前项 带标签数据)
          (错误 “错误的带标签数据 —— 类型标签” 带标签数据)))

而后我们就可以写出根据绑定的标签是“直角坐标”还是“极坐标”判别是哪种表示法,是“直角坐标系下的复数”还是“极坐标系下的复数”:

(define (rectangular? z)
    (eq? (type-tag z) 'rectangular))
(define (polar? z)
    (eq? (type-tag z) 'polar))

尝试将上述过程直译为汉语:

(定义 (直角坐标系下的? 复数)
    (为? (类型标签 复数) ‘直角坐标))
(定义 (极坐标系下的? 复数)
    (为? (类型标签 复数) ‘极坐标))

有了根据标签判断的过程,我们就该写一写加标签的过程了,先以之前的选用直角坐标表示和选用极坐标表示的两类过程为基础,如下:

1.直角坐标表示

(define (real-part-rectangular z) (car z))

(define (imag-part-rectangular z) (cdr z))

(define (magnitude-rectangular z)
    (sqrt (+ (square (real-part-rectangular z)) 
             (square (imag-part-rectangular z)))))

(define (angle-rectangular z)
    (atan (imag-part z) (real-part z)))

(define (make-from-real-imag-rectangular x y) 
    (attach-tag 'rectangular (cons x y)))

(define (make-from-mag-ang-rectangular r a)
    (attach-tag 'rectangular
                (cons (* r (cos a)) (* r (sin a)))))

尝试将上述过程直译为汉语:

(定义 (直角坐标表示法求实部 复数)(前项 复数))
(定义 (直角坐标表示法得实部 复数)(前项 复数))
(定义 (直角坐标表示法取实部 复数)(前项 复数))
(定义 (直角坐标表示法下得实部 复数)(前项 复数))
(定义 (直角坐标表示法下求实部 复数)(前项 复数))
(定义 (直角坐标表示法下取实部 复数)(前项 复数))

(定义 (直角坐标表示法求虚部 复数) (后项 复数))
(定义 (直角坐标表示法得虚部 复数) (后项 复数))
(定义 (直角坐标表示法取虚部 复数) (后项 复数))
(定义 (直角坐标表示法下得虚部 复数) (后项 复数))
(定义 (直角坐标表示法下求虚部 复数) (后项 复数))
(定义 (直角坐标表示法下取虚部 复数) (后项 复数))

(定义 (直角坐标表示法求模长 复数) 
    (开方 (+ (平方 (直角坐标表示法下取实部 复数))
              (平方 (直角坐标表示法下取虚部 复数)))))

(定义 (直角坐标表示法下求模长 复数)
    (直角坐标表示法求模长 复数))

(定义 (直角坐标表示法求幅角 复数)
    (反正切 (直角坐标表示法下取虚部)(直角坐标表示法下取实部)))

(定义 (根据实部虚部构造直角坐标表示法表示的复数 实部 虚部)
    (绑定标签 ‘直角坐标 (构建序对 实部 虚部)))

(定义 (根据模长幅角构造直角坐标表示法表示的复数 模长 幅角)
    (绑定标签 ‘直角坐标 (构建序对 (* 模长 (余弦 幅角))(* 模长 (正弦 幅角)))))

然后是修改后的极坐标表示:

(define (real-part-polar z)
    (* (magnitude-polar z) (cos (angle-polar z))))
(define (imag-part-polar z)
    (* (magnitude-polar z) (sin (angle-polar z))))
(define (magnitude-polar z) (car z))
(define (angle-polar z) (cdr z))
(define (make-from-real-imag-polar x y)
    (attach-tag 'polar
                (cons (sqrt (+ (square x) (square y)))
                      (atan y x))))
(define (make-from-mag-ang-polar r a)
    (attach-tag 'polar (cons r a)))

尝试将上述过程直译为汉语:

(定义 (极坐标表示法下求实部 复数)
    (* (极坐标表示法下取模长 复数)(余弦 (极坐标表示法下取幅角 复数))))
(定义 (极坐标表示法下求虚部 复数)
    (* (极坐标表示法下取模长 复数)(正弦 (极坐标表示法下取幅角 复数))))
(定义 (极坐标表示法下取模长 复数)(前项 复数))
(定义 (极坐标表示法下取幅角 复数)(后项 复数))
(定义 (根据模长幅角构造极坐标表示法表示的复数 模长 幅角)
    (绑定标签 ‘极坐标 (序对 模长 幅角)))
(定义 (根据实部虚部构造极坐标表示法表示的复数 实部 虚部)
    (绑定标签 ‘直角坐标 
             (构建序对 (开方 (+ (平方 实部)(平方 虚部)))
                      (反正切 虚部 实部))))

有了这两个改造过的过程,我们可以给原先的real-part,imag-part,magnitude,angle函数内部加上选择分支,根据传入的复数采用的表示法,选取对应的表示法下的选择函数运行:

(define (real-part z)
    (cond ((rectangular? z)
           (real-part-rectangular (contents z)))
          ((polar? z)
           (real-part-polar (contents z)))
          (else (error "Unknown type -- REAL_PART" z))))
(define (imag-part z)
    (cond ((rectangular? z)
           (imag-part-rectangular (contents z)))
          ((polar? z)
           (imag-part-polar (contents z)))
          (else (error "Unknown type -- IMAG_PART" z))))
(define (magnitude z)
    (cond ((rectangular? z)
           (magnitude-rectangular (contents z)))
          ((polar? z)
           (magnitude-polar (contents z)))
          (else (error "Unknown type -- MAGNITUDE" z))))
(define (angle z)
    (cond ((rectangular? z)
           (angle-rectangular (contents z)))
          ((polar? z)
           (angle-polar (contents z)))
          (else (error "Unknown type -- ANGLE" z))))

不得不说上面的这些内容,中文编程全无优势啊,但还是写一下吧:

(定义 (实部 复数)
    (情况符合 ((直角坐标表示法下的? 复数)
               (直角坐标表示法下求实部 (数据部分 复数)))
             ((极坐标表示法下的? 复数)
               (极坐标表示法下求实部 (数据部分 复数)))
             (否则 (错误 “未知类型 —— 实部” 复数))))
(定义 (虚部 复数)
    (情况符合 ((是直角坐标表示法表示的? 复数)
               (直角坐标表示法下求虚部 (数据部分 复数)))
             ((是极坐标表示法表示的? 复数)
               (极坐标表示法下求虚部 (数据部分 复数)))
             (否则 (错误 “未知类型 —— 虚部” 复数))))
(定义 (模长 复数)
    (情况符合 ((是用直角坐标表示法表示的? 复数)
               (直角坐标表示法求模长 (数据部分 复数)))
             ((是用极坐标表示法表示的? 复数)
               (极坐标表示法下求模长 (数据部分 复数)))
             (否则 (错误 “未知类型 —— 模长” 复数))))
(定义 (幅角 复数)
    (清康符合 ((是用直角坐标表示法表示的? 复数)
               (直角坐标表示法下求幅角 (数据部分 复数)))
             ((是用极坐标表示法表示的? 复数)
               (极坐标表示法下求幅角 (数据部分 复数)))
             (其它情况 (错误 “未知类型 —— 幅角” 复数))))

最后你需要补齐复数的构造过程,现在,你可以在手头有实部和虚部时选用直角坐标表示法,手头有模长和幅角时选用极坐标表示法:

(define (make-from-real-imag x y)
    (make-from-real-imag-rectangular x y))
(define (make-from-mag-ang r a)
    (make-from-mag-ang-polar r a))

尝试将上述过程直译为汉语:

(定义 (构造直角坐标表示法下的复数 实部 虚部)
    (根据实部虚部构造直角坐标表示法下的复数 实部 虚部))
(定义 (由实部虚部构造复数 实部 虚部)
    (根据实部虚部构造直角坐标表示法下的复数 实部 虚部))
(定义 (构造极坐标表示法下的复数 模长 幅角)
    (根据实部虚部构造极坐标表示法下的复数 模长 幅角))
(定义 (由模长幅角构造复数 模长 幅角)
    (根据模长幅角造极坐标表示法下的复数 模长 幅角))

应用上述改造后的过程的话,先前的复数加法,复数减法,复数乘法和复数除法等过程无需改变实现,即可使用,比如,add-complex仍然是:

(define (add-complex z1 z2)
    (make-from-real-imag (+ (real-part z1) (real-part z2))
                         (+ (imag-part z1) (imag-part z2))))

直译为汉语是:

(定义 (复数加法 复数甲 复数乙)
    (构造直角坐标系下的复数 (+ (实部 复数甲)(实部 复数乙))
                         (+ (虚部 复数甲)(虚部 复数乙))))

其实上面的整合思路,用书上的这张图可以直观表示:
在这里插入图片描述

但是,这样改造完的过程有一个明显的问题,就是,太絮叨了!无论是英文“magnitude-rectangular”还是我写的那种“直角坐标表示法下求模长”一类的,而且它不仅仅是絮叨的问题,上面的把两种表示法怼进一个系统时,是需要给两类表示法的各个过程都改名的(以保证不重名),也就是英文里面是比如把原来的“angle”改为“angle-rectangular”和“angle-polar”,而我写的中文版本,为了追求一种自然语言的流畅感吧,甚至“直角坐标系下求幅角”和“极坐标系下求幅角”这么老长的命名都给干出来了。可能现在系统只有两层两个分支就还好,这家伙,系统但凡有个三层四层,三类分支四类分支的话,那还这样命名还不得给人干崩溃喽?

(还有一个问题,就是对于上面的过程中,有这么一句(数据部分 复数)或者真的直译的话,叫(内容 复数),这句话读起来就真的很奇怪,虽然我们知道这里是因为带标签数据的话,需要这样的语句来获取不是标签的真正的数据部分,但这改变不了它读起来就是很奇怪的这个事实,这个问题有没有可能得到改善我现在还没有答案。)

其实我觉得要理解该如何解决我上面提出的那第一个问题,咱们还可以回归之前聊的像C++中,那多态是什么样的,就比如:

void option(int a){}
//和
void option(double b){}
//然后你声明:
int a;
//和
double b;
//在调用时,你写
option(a);
//和
option(b);
//编译器就会自动调用该调用的函数。

你发现这里一个很通顺的点就是,当你声明了int a;之后,再写a,编译器就很清楚这个a是int型的了,作为程序员,不必也不应该在这份可见程序里还需要呈现出如何给a添加int标签,以及当用户(当然,用户这里就是指咱们自己)在声明a时,如何获取a的类型标签的过程(这里依然还是很不专业地假设C++获取一个变量的类型也是像类似获取存储于变量之中的某个序列里的某一项是啥这样的)。当然逻辑上应该有这样的过程,但这样的过程应该存在于另一层级之中。

那回到书中,来看一看书中的例子是怎么把变量相关的类型标签在另一层级中注入变量,以及如何实现在使用一个变量或过程(当然在scheme语言中,有着变量和过程是同等的一个特性)时,就能提前执行完毕关乎这个变量或过程所应有的类型标签的所对应的过程的吧。(我感觉我写了一句“中文长难句”?枯了,回头把这句话再改的像人话一点吧)

其实就是后面这两个过程啦,put和get,put能够把类型标签注入变量;get能够get能够获取变量的类型标签,二者格式如下:

(put <op> <type> <item>)
(get <op> <type>)

那这里我经过一定的思考决定暂将put命名为“注入”,从代码上来看,它是把所要执行的过程和一个类型标签注入了你所声明的某个变量(再来一遍,scheme中变量与过程等价的哦)中。

而get,可能最直接的翻译是“获取”了吧,套进上面的伪码中,意为“获取某类型标签下的对应操作”。

嗯,尝试将上述两个过程直译为汉语:

(注入 <操作> <类型标签> <变量>)
(获取 <操作> <类型标签>)

ok,有了这两个过程后,我们先来看一看现在加标签可以怎么写了吧:

1.直角坐标表示法

(define (install-recatngular-package)

;;internal procedures
(define (real-part z) (car z))
(define (imag-part z) (cdr z))
(define (make-from-real-imag x y) (cons x y))
(define (magnitude z)
    (sqrt (+ (square (real-part z))
             (square (imag-part z))))))
(define (angle z)
    (atan (imag-part z) (real-part z)))
(define (make-from-mag-ang r a)
    (cons (* r (cos a)) (* r (sin a))))

;;interface to the rest of the system
(define (tag x) (attach-tag 'rectangular x))
(put 'real-part '(rectangular) real-part)
(put 'imag-part '(rectangular) imag-part)
(put 'magnitude '(rectangular) magnitude)
(put 'angle '(rectangular) angle)
(put 'make-from-real-imag 'rectangular
     (lambda (x y) (tag (make-from-real-imag x y))))
(put 'make-from-mag-ang 'rectangular
     (lambda (r a) (tag (make-from-mag-ang r a))))
'done)

尝试将上述过程直译为汉语:

(定义 (复数直角坐标表示法相关过程集)
;;内部过程
(定义 (实部 复数)(前项 复数))
(定义 (虚部 复数)(后项 复数))
(定义 (由实部虚部构造复数 实部 虚部)(构造序对 实部 虚部))
(定义 (模长 复数)
    (开方 (+ (平方 (实部 复数))
              (平方 (虚部 复数)))))
(定义 (幅角 复数)
    (反正切 (虚部 复数)(实部 复数)))
(定义 (由模长幅角构造复数 模长 幅角)
    (构造序对 (* 模长 (余弦 幅角)) (* 模长 (正弦 幅角))))

;;给所有内部过程添加“直角坐标系”标签以形成对系统外部的接口
(定义 (加标签 元) (绑定标签 '直角坐标系 元))
(注入 ‘实部  ’(直角坐标系) 实部)
(注入 ‘虚部  ’(直角坐标系) 虚部)
(注入 ‘模长  ’(直角坐标系) 模长)
(注入 ‘幅角  ’(直角坐标系) 幅角)
(注入 ‘由实部虚部构造复数 ’直角坐标系
     (规定对于 (实部 虚部)(加标签 (由实部虚部构造复数 实部 虚部))))
(注入 ‘由模长幅角构造复数 ’直角坐标系
     (规定对于 (模长 幅角)(加标签 (由模长幅角构造复数 模长 幅角))))
‘完毕)

优秀的编码,代码足以解释其自身,要让我再解释一遍上面的代码在做什么的话,我几乎也只能这样说,“先是一段对于直角坐标表示法下的复数的构造函数与选择函数的定义,这一块代码在上文出现过的,然后就是把这些函数各自加上对应的操作和类型标签,这样再在外面使用某个构造函数或选择函数时,‘直角坐标’标签以及该执行的操作就都在‘函数名字里面’(我不知道我这种说法你能不能理解,就类似一个int形的a,你使用a的时候,‘int’就好似在它里面一样)了。”这样的一个解释,看起来是真的用心解释了,但在我这里我感觉我简直如同只是把代码念了一遍一样,上面这个干净利落地分成两块的代码,我觉得你要理解的话,你去扫一眼它,可能比看我解释这么一大句来的快。

2.极坐标表示法

(define (install-polar-package)
;;internal procedures
(define (magnitude z) (car z))
(define (angle z) (cdr z))
(define (real-part z) 
    (* (magnitude z) (cos (angle z))))
(define (imag-part z)
    (* (magnitude z) (sin (angle z))))
(define (make-from-mag-ang r a)
    (cons r a))
(define (make-from-real-imag x y)
    (cons (sqrt (+ (square x) (square y)))
          (atan y x)))
          
;;interface to the rest of the system
(define (tag x) (attach-tag 'polar x))
(put 'real-part '(polar) real-part)
(put 'imag-part '(polar) imag-part)
(put 'magnitude '(polar) magnitude)
(put 'angle '(polar) angle)
(put 'make-from-real-imag 'polar
     (lambda (x y) (tag (make-from-real-imag x y))))
(put 'make-from-mag-ang 'polar
     (lambda (r a) (tag (make-from-mag-ang r a))))
'done)
(定义 (复数极坐标表示法相关过程集)
;;内部过程
(定义 (模长 复数)(前项 复数))
(定义 (幅角 复数)(后项 复数))
(定义 (实部 复数)
    (* (模长 复数)(余弦 (幅角 复数))))
(定义 (虚部 复数)
    (* (模长 复数)(正弦 (幅角 复数))))
(定义 (由模长幅角构造复数 模长 幅角)
    (构造序对 模长 幅角))
(定义 (由实部虚部构造复数 实部 虚部)
    (构造序对 (开方 (+ (平方 实部)(平方 虚部)))
             (反正切 虚部 实部)))

;;给所有内部过程加上“极坐标系”标签以形成对系统外部的接口
(注入 ‘实部  ’(极坐标系) 实部)
(注入 ‘虚部  ’(极坐标系) 虚部)
(注入 ‘模长  ’(极坐标系) 模长)
(注入 ‘幅角  ’(极坐标系) 幅角)
(注入 ‘由实部虚部构造复数 ’极坐标系
    (规定对于 (实部 虚部)(绑定标签 ‘极坐标系 (由实部虚部构造复数 实部 虚部))))
(注入 ‘由模长幅角构造复数 ’极坐标系
    (规定对于 (实部 虚部)(绑定标签 ‘极坐标系 (由模长幅角构造复数 模长 幅角))))
‘完毕)

这样写的话,咱们就可以很踏实地按照一开始的按欲望编程时写的那种写出两种复数表示方法的相关过程,把添加“直角坐标”“极坐标”标签什么的放到下面的另一块地方,用统一的“注入”即put进这些东西的方法。这样的话就不必顾虑还有重名问题什么的了,不用随着系统变大还要把原本好好的过程名改的贼拉长,编写起代码来就很爽。

那知道了该怎么加标签了,咱们就该学习如何取标签,以及执行和标签对应的操作过程了,书中是用一个叫apply-generic的函数来做这件事的:

(define (apply-generic op . args)
    (let ((type-tags (map type-tag args)))
      (let ((proc (get op type-tags)))
        (if proc
            (apply proc (map contents args))
            (error
              "No method for these types -- APPLY-GENERIC"
              (list op type-tags))))))

尝试将上述过程直译为汉语:

(定义 (通用应用 操作 · 参数) ;这里参数可能不止一个,这一句应该读做“对所有参数执行某一操作”
    (命 ((类型标签集合 (每一项 类型标签 参数)));这里可以读做“每一项参数的类型标签”
      (命 ((该执行的操作 (获取 操作 类型标签集合))) ;这里可以读做“获取类型标签集合下的操作”
        (如果 该执行的操作 
             (应用 该执行的操作 (每一项 数据部分 参数));这一句可以读做“对每一项参数的数据部分应用该执行的操作”
             (错误
                “没有所要类型标签下的对应操作 —— 通用应用”
                (序列 操作 所有类型标签))))))

利用apply-generic,各种通用型选择函数可以定义如下:

(define (real-part z) (apply-generic 'real-part z))
(define (imag-part z) (apply-generic 'imag-part z))
(define (magnitude z) (apply-generic 'magnitude z))
(define (angle z) (apply-generic 'angle z))

尝试将上述过程直译为汉语:

(定义 (实部 复数)(通用应用 ‘实部 复数))
(定义 (虚部 复数)(通用应用 ‘虚部 复数))
(定义 (模长 复数)(通用应用 ‘模长 复数))
(定义 (幅角 复数)(通用应用 ‘幅角 复数))

(其实对于“通用应用”这一部分,我对自己现在给出的这些汉语版本是不够满意的,它们现在阅读起来其实并不够自然,以后看看会不会灵感来了,能给出更好的版本吧)

现在,我们的构造函数可以这么写了,也终于可以这么写了:

(定义 (构造直角坐标系下的复数 实部 虚部)
    ((获取 ‘由实部虚部构造复数 ’直角坐标系)实部 虚部))
;这里这行就是先获取“直角坐标系”下的“由实部虚部构造复数”,然后对“实部”和“虚部”两个参数,执行这个获取到的过程
(定义 (构造极坐标系下的复数 模长 幅角)
    ((获取 ‘由模长幅角构造复数 ’极坐标系)模长 幅角))
;同样的,先获取"极坐标系"下的“由模长幅角构造复数”,获取到后,你就发现可以把前面的那个括号拆掉了,用获取到的那个过程名对后面的“模长”和“幅角”两个参数执行即可。

一激动都不先写英文版了,英文版:

(define (make-from-real-imag x y)
    ((get 'make-from-real-imag 'rectangular) x y))
(define (make-from-mag-ang r a)
    ((get 'make-from-mag-ang 'polar) r a))

练习2.73 上一篇“scheme语言直译为汉语(十七)”中写过一个描述执行符号求导的程序:

(define (deriv exp var)
    (cond ((number? exp) 0)
          ((variable? exp) (if (same-variable? exp var) 1 0))
          ((sum? exp)
           (make-sum (deriv (addend exp) var)
                     (deriv (augend exp) var)))
          ((product? exp)
           (make-sum
             (make-product (multiplier exp)
                           (deriv (multiplicand exp) var))
             (make-product (deriv (multiplier exp) var)
                           (multiplicand exp))))
<更多规则可以加在这里>
          (else (error "unknown expression type -- DERIV" exp))))

尝试将上述过程直译为汉语:

(定义 (导数 表达式 变量)
    (情况符合 ((是数字? 表达式)0)
               ((是变量? 表达式)(如果 (是同一个变量? 表达式 变量)1 0))
             ((是和式? 表达式)
               (构造和式 (导数 (被加数 表达式)变量)
                        (导数 (加数 表达式)变量)))
             ((是乘式? 表达式)
               (构造和式 
                 (构造乘式 (被乘数 表达式)
                          (导数 (乘数 表达式)变量))
                 (构造乘式 (导数 (被乘数 表达式)变量)
                          (乘数 表达式))))
              <更多规则可以加在这里>
             (其它情况 (错误 “未知表达式类型 —— 导数” 表达式))))

那我们注意到对求导过程,我们可以理解为是根据表达式符号调取相应过程的一个过程,有了这个思路后,我们能写出如下代码:

(define (deriv exp var)
    (cond ((number? exp) 0)
          ((variable? exp) (if (same-variable? exp var) 1 0))
          (else ((get 'deriv (operator exp)) (operands exp) var))))
(define (operator exp) (car exp))
(define (operands exp) (cdr exp))

尝试将上述过程直译为汉语:

(定义 (导数 表达式 变量)
    (情况符合 ((是数字? 表达式) 0)
             ((本身是变量? 表达式)(如果 (是同一个变量? 表达式 变量)1 0))
             (其它情况 ((获取 ‘导数 (运算符号 表达式))(运算数 表达式)变量))))
(定义 (运算符号 表达式)(前项 表达式))
(定义 (运算数 表达式)(后项 表达式))

a)请解释上面究竟做了些什么。为什么我们无法将相近的谓词number?和same-variable?也加入数据导向分派中?
b)请写出针对和式与积式的求导过程,并把它们安装到表格里,以便上面程序使用所需要的辅助性代码。
c)请选择一些你希望包括的求导规则,例如对乘幂(练习2.56) 求导等等,并将它们安装到这一数据导向的系统里。
d)在这一简单的代数运算器中,表达式的类型就是构造起它们来的代数运算符。假定我们想以另一种相反的方式做索引,使得deriv里完成分派的代码行像下面这样:
((get (operator exp) 'deriv) (operands exp) var)
求导系统里还需要做哪些相应的改动?

解:
a) 上面的新的过程就是根据表达式运算符选取相应的求导过程的一个程序,因为对于number? 和same-variable?所要判别的表达式没有运算符号,所以我们无法把它俩也按这种方式写进来。
b) 其实我不太清楚这一问的原意啊,是应该自己想出这个“安装进表格”的写法吗?我上网查了一下,别人都说要完成这题要先把书中186页的一个看起来是构建和把语句插入进表格的一个程序搬过来,用以先实现对put和get的定义,我决定也先苟且这么办了。

define-put-get.ss:

;书上第186页讲的对put和get的定义
(define (make-table)
  (let ((local-table (list '*table*)))
    (define (lookup key-1 key-2)
      (let ((subtable (assoc key-1 (cdr local-table))))
        (if subtable
            (let ((record (assoc key-2 (cdr subtable))))
              (if record
                  (cdr record)
                  false))
            false)))
    (define (insert! key-1 key-2 value)
      (let ((subtable (assoc key-1 (cdr local-table))))
        (if subtable
            (let ((record (assoc key-2 (cdr subtable))))
              (if record
                  (set-cdr! record value)
                  (set-cdr! subtable
                            (cons (cons key-2 value)
                                  (cdr subtable)))))
            (set-cdr! local-table
                      (cons (list key-1
                                  (cons key-2 value))
                            (cdr local-table)))))
      'ok)
    (define (dispatch m)
      (cond ((eq? m 'lookup-proc) lookup)
            ((eq? m 'insert-proc!) insert!)
            (else (error "Unknown operation -- TABLE" m))))
    dispatch))

(define operation-table (make-table))
(define get (operation-table 'lookup-proc))
(define put (operation-table 'insert-proc!))

basic-judgment.ss:

(define (variable? x) (symbol? x))

(define (same-variable? x v) (eq? x v))

(define (=number? x num)
  (and (number? x) (= x num)))

73-deriv.ss:

(load "base-judgment.ss")
(define (deriv exp var)
    (cond ((number? exp) 0)
          ((variable? exp) (if (same-variable? exp var) 1 0))
          (else ((get 'deriv (operator exp)) (operands exp) var))))
(define (operator exp) (car exp))
(define (operands exp) (cdr exp))

install-sum-package.ss

(define (install-sum-package)
  (define (make-sum a1 a2)
    (cond ((=number? a1 0) a2)
          ((=number? a2 0) a1)
          ((and (number? a1) (number? a2)) (+ a1 a2))
          (else (list '+ a1 a2))))
  (define (addend operands) (car operands))
  (define (augend operands) (cadr operands))
  (define (deriv-sum operands var)
    (make-sum (deriv (addend operands) var)
              (deriv (augend operands) var)))

  (put 'make '+ make-sum)
  (put 'deriv '+ deriv-sum)
  (display "Install sum package done\n"))

(define (make-sum a1 a2)
  ((get 'make '+) a1 a2))

install-product-package.ss

(define (install-product-package)
  (define (make-product m1 m2)
    (cond ((or (=number? m1 0) (=number? m2 0)) 0)
          ((=number? m1 1) m2)
          ((=number? m2 1) m1)
          (else (list '* m1 m2))))
  (define (multiplier operands) (car operands))
  (define (multiplicand operands) (cadr operands))
  (define (deriv-product operands var)
    (make-sum
     (make-product (multiplier operands)
                   (deriv (multiplicand operands) var))
     (make-product (deriv (multiplier operands) var)
                   (multiplicand operands))))

  (put 'make '* make-product)
  (put 'deriv '* deriv-product)
  (display "Install product package done\n"))

(define (make-product m1 m2)
  ((get 'make '*) m1 m2))

test.ss:

(load "install-sum-package.ss")
(load "install-product-package.ss")
(load "73-deriv.ss")
(load "define-put-get.ss")

(install-sum-package)
(install-product-package)

(display (deriv '(+ x 3) 'x))
(newline)
(display (deriv '(* x 3) 'x))
(exit)

然后是中文命名的版本:

中文过程名.ss:

(define (是数字 元) (number? 元))
(define (为数字 元) (number? 元))
(define (如果是数字 元) (number? 元))
(define (如果为数字 元) (number? 元))
(define (是数字吗 元) (number? 元))
(define (为数字吗 元) (number? 元))

;(define (与 条件甲 条件乙) (and 条件甲 条件乙))
;(define (或 条件甲 条件乙) (or 条件甲 条件乙))
;实测对于“与”“或”是不能草率地这样弄的,会出现一些奇怪的bug,日后真正明白了对无穷多项的与,或运算是个什么原理后再写吧


(define (前项 序对) (car 序对))
(define (前头的项 序对) (car 序对))
(define (后项 序对) (cdr 序对))
(define (后面的项 序对) (cdr 序对))

(define (第一项 序列) (car 序列))
(define (第二项 序列) (cadr 序列))
(define (第三项 序列) (caddr 序列))
(define (第四项 序列) (cadddr 序列))

(define (输出 内容) (display 内容))

(define (换行) (newline))

(define (退出) (exit))

注入和获取的定义.ss(这里因为其实还没学到,就先不魔改了):

;书上第186页讲的对put和get的定义
(define (make-table)
  (let ((local-table (list '*table*)))
    (define (lookup key-1 key-2)
      (let ((subtable (assoc key-1 (cdr local-table))))
        (if subtable
            (let ((record (assoc key-2 (cdr subtable))))
              (if record
                  (cdr record)
                  false))
            false)))
    (define (insert! key-1 key-2 value)
      (let ((subtable (assoc key-1 (cdr local-table))))
        (if subtable
            (let ((record (assoc key-2 (cdr subtable))))
              (if record
                  (set-cdr! record value)
                  (set-cdr! subtable
                            (cons (cons key-2 value)
                                  (cdr subtable)))))
            (set-cdr! local-table
                      (cons (list key-1
                                  (cons key-2 value))
                            (cdr local-table)))))
      'ok)
    (define (dispatch m)
      (cond ((eq? m 'lookup-proc) lookup)
            ((eq? m 'insert-proc!) insert!)
            (else (error "Unknown operation -- TABLE" m))))
    dispatch))

(define operation-table (make-table))

(define get (operation-table 'lookup-proc))
(define 获取 (operation-table 'lookup-proc))

(define put (operation-table 'insert-proc!))
(define 注入 (operation-table 'insert-proc!))

所需判定语句.ss:

(define (加载 文件) (load 文件))
(加载 "中文过程名.ss")

(define (variable? x) (symbol? x))
(define (是变量 元) (variable? 元))
(define (为变量 元) (variable? 元))
(define (如果是变量 元) (variable? 元))
(define (如果为变量 元) (variable? 元))
(define (是变量吗 元) (variable? 元))
(define (为变量吗 元) (variable? 元))

(define (same-variable? x v) (eq? x v))
(define (是同一个变量 甲 乙) (same-variable? 甲 乙))
(define (为同一个变量 甲 乙) (same-variable? 甲 乙))
(define (如果是同一个变量 甲 乙) (same-variable? 甲 乙))
(define (如果为同一个变量 甲 乙) (same-variable? 甲 乙))
(define (是同一个变量吗 甲 乙) (same-variable? 甲 乙))
(define (为同一个变量吗 甲 乙) (same-variable? 甲 乙))


(define (=number? x num)
  (and (number? x) (= x num)))
(define (等于给定数值 元 给定数值)
    (and (为数字 元) (= 元 给定数值)))
(define (等于给定值 元 给定数值) 
    (and (为数字 元) (= 元 给定数值)))
(define (如果等于给定数值 元 给定数值) 
    (and (为数字 元) (= 元 给定数值)))
(define (如果等于给定值 元 给定数值) 
    (and (为数字 元) (= 元 给定数值)))

73题题干中的求导.ss:

(define (加载 文件) (load 文件))
(加载 "中文过程名.ss")
(加载 "所需判定语句.ss")

(define (导数 表达式 自变量)
  (cond ((如果是数字 表达式) 0)
        ((如果是变量 表达式) (if (是同一个变量 表达式 自变量) 1 0))
        (else ((获取 '求导 (运算符 表达式)) (运算数 表达式)
                                           自变量))))

(define (运算符 表达式) (前头的项 表达式))
(define (运算符号 表达式) (运算符 表达式))

(define (运算数 表达式) (后面的项 表达式))

和式求导准备.ss:

(define (加载 文件) (load 文件))
(加载 "中文过程名.ss")

(define (和式求导准备)
  (define (构造和式 被加数 加数)
    (cond ((如果等于给定数值 被加数 0) 加数)
          ((如果等于给定数值 加数 0) 被加数)
          ((and (如果是数字 被加数) (如果是数字 加数)) (+ 被加数 加数))
          (else (list '+ 被加数 加数))))
  (define (被加数 运算数) (第一项 运算数))
  (define (加数 运算数) (第二项 运算数))
  (define (和式求导 运算数 自变量)
    (构造和式 (导数 (被加数 运算数) 自变量)
             (导数 (加数 运算数) 自变量)))
    
  (注入 '构造 '+ 构造和式)
  (注入 '求导 '+ 和式求导)
  (输出 "和式求导准备完毕\n"))

(define (构造和式 被加数 加数)
  ((获取 '构造 '+) 被加数 加数))

乘式求导准备.ss:

(define (加载 文件) (load 文件))
(加载 "中文过程名.ss")

(define (乘式求导准备)
  (define (构造乘式 被乘数 乘数)
    (cond ((or (如果等于给定数值 被乘数 0) (如果等于给定数值 乘数 0)) 0)     ;不知道为什么,这里用中文“或”的话,会出错?!
          ((如果等于给定数值 被乘数 1) 乘数)
          ((如果等于给定数值 乘数 1) 被乘数)
          (else (list '* 被乘数 乘数))))
  (define (被乘数 运算数) (第一项 运算数))
  (define (乘数 运算数) (第二项 运算数))
  (define (乘式求导 运算数 自变量)
    (构造和式
     (构造乘式 (被乘数 运算数)
              (导数 (乘数 运算数) 自变量))
     (构造乘式 (导数 (被乘数 运算数) 自变量)
              (乘数 运算数))))

  (注入 '构造 '* 构造乘式)
  (注入 '求导 '* 乘式求导)
  (输出 "乘式求导准备完毕\n"))

(define (构造乘式 被乘数 乘数)
  ((获取 '构造 '*) 被乘数 乘数))

test.ss:

(define (加载 文件) (load 文件))
(加载 "中文过程名.ss")
(加载 "所需判定语句.ss")
(加载 "注入和获取的定义.ss")
(加载 "73题题干中的求导.ss")
(加载 "和式求导准备.ss")
(加载 "乘式求导准备.ss")

(和式求导准备)
(乘式求导准备)

(输出 (导数 '(+ 1 x) 'x))
(换行)
(输出 (导数 '(* x 3) 'x))
(退出)

在这里插入图片描述
c) 把乘幂放进来试试:
英文命名版本:
install-exponentiation-package.ss:

(define (** base exponent)
  (if (= exponent 0)
      1
      (* base (** base (- exponent 1)))))

(define (install-exponentiation-package)
  (define (make-exponentiation b e)
    (cond ((=number? e 0) 1)
          ((=number? e 1) b)
          ((and (number? b) (number? e)) (** b e))
          (else (list '** b e))))
  (define (base operands) (car operands))
  (define (exponent operands) (cadr operands))
  (define (deriv-exponentiation operands var)
    (make-product
     (make-product
      (exponent operands)
      (make-exponentiation (base operands) (- (exponent operands) 1)))
     (deriv (base operands) var)))

  (put 'make '** make-exponentiation)
  (put 'deriv '** deriv-exponentiation)
  (display "Install exponentiation package done\n"))

(define (make-exponentiation b e)
  ((get 'make '**) b e))

中文命名版本:

指数式求导准备.ss:

(define (加载 文件) (load 文件))
(加载 "中文过程名.ss")

;用“**”表示乘幂符号
(define (** 底数 指数)
  (if (= 指数 0)
      1
      (* 底数 (** 底数 (- 指数 1)))))

(define (指数式求导准备)
  (define (构造指数式 底数 指数)
    (cond ((如果等于给定数值 指数 0) 1)
          ((如果等于给定数值 指数 1) 底数)
          ((and (如果是数字 底数) (如果是数字 指数)) (** 底数 指数))
          (else (list '** 底数 指数))))
  (define (底数 运算数) (第一项 运算数))
  (define (指数 运算数) (第二项 运算数))
  (define (指数式求导 运算数 自变量)
    (构造乘式
     (构造乘式
      (指数 运算数)
      (构造指数式 (底数 运算数) (- (指数 运算数) 1)))
     (导数 (底数 运算数) 自变量)))

  (注入 '构造 '** 构造指数式)
  (注入 '求导 '** 指数式求导)
  (输出 "指数式求导准备完毕\n"))

(define (构造指数式 底数 指数)
  ((获取 '构造 '**) 底数 指数))

test.ss:

(define (加载 文件) (load 文件))
(加载 "中文过程名.ss")
(加载 "所需判定语句.ss")
(加载 "注入和获取的定义.ss")
(加载 "73题题干中的求导.ss")
(加载 "和式求导准备.ss")
(加载 "乘式求导准备.ss")
(加载 "指数式求导准备.ss")

(和式求导准备)
(乘式求导准备)
(指数式求导准备)

(输出 (导数 '(+ 1 x) 'x))
(换行)
(输出 (导数 '(* x 3) 'x))
(换行)
(输出 (导数 '(** x 3) 'x))
(换行)
(输出 (导数 '(** (* x 3) 3) 'x))
(退出)

在这里插入图片描述
d) 这一问只需把put里面的前两个参数调换一下顺序即可。

参考文献:
[1] [美]Julie Sussman.计算机程序的构造和解释[M]. 裘宗燕译注.北京:机械工业出版社,1996.
[2] Learn SICP Chapter 2
[3] SICP解题集,练习2.73

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

X-jazz

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

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

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

打赏作者

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

抵扣说明:

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

余额充值