Positioning SICP 2: Building Abstractions with Data

作者:何岩,recreating.org出品,谢绝转载。
阅读SICP,站在Lisp发明者的上帝视角来思考:“我为什么要这么设计Lisp?”

0.前言:Why I design the concept of Data? 为什么要设计Data这个概念?Procedure不够用吗? #

因为,control complexity。
因为,人脑习惯Object View,需要让人们产生Data这个Object想象体。这样可以降低思考复杂度。
因为,Object/Data的抽象思想是Black-Box Abstraction,也可以称为modularity。modularity可以降低复杂度,因为,modularity减少了关系发生的数量。
因为,统一视角,抽象思维,type的本质。例如:Java中的接口
Procedure is Stream View
Data is Object View

1.How I design the implementation of Data?我如何设计Data的实现? #

— Chapter 2.1.3 What Is Meant by Data?
我将用Procedures来虚拟出Data。
Data对外提供可以被感知到的是一层interface,interface的本质就是procedure。
用户就会想象,Data貌似真的存在。
其实,那些作为interface的procedure也是由更底层的procedure组成的。
例如,用procedure来模拟Pairs的存在
为了欺骗使用Pair的人,我会提供Cons/Car/Cdr这三个Procedures作为interface。
使用Pair的人会这么操纵Pair暴露的interface:cons/car/cdr:
=>(define x (cons 1 2) ;构建一个叫做x的Pair,由1和2组成
=>(car x) ;获得Pair中存储的第一个元素
1
=>(cdr x) ;获得Pair中存储的第二个元素
2

我们来看看这三个Procedures的内部究竟有没有Data的存在?

方案1:用Procedure模拟Data
(define (cons x y)
(define (dispatch m)
(cond
((= m 0) x)
((= m 1) y)
(else (error “Argument not 0 or 1 – CONS” m))))
dispatch)

(define (car z) (z 0))

(define (cdr z) (z 1))

方案2:用Procedure模拟Data,上一个方案里还用到了数字,我们这次用纯Procedure:
(define (cons x y)
(lambda (m) (m x y)))

(define (car z)
(z (lambda (p q) p)))

(define (cdr z)
(z (lambda (p q) q)))

因为,Lisp的Procedure的设计是基于Lambda演算的思想,即,Procedure就是Lambda。
所以,Data的存在,本质上是基于Lambda,更微观的看是基于Lambda的可组装能力,在Lambda演算中,这种组装变形的能力,称为:Beta规约(β-reduction)
再抽象来思考,Lambda的可组装能力来自于什么呢?
答案是,来自于,阿隆索·丘奇定义了一个规则/逻辑,也就是人脑的想象。
这个组合的逻辑来自于哪里?
答案是,来自于,人类对宇宙规律的观察,模拟出宇宙的底层规律,就是最小的元素的可组装性。
而这个逻辑是真理吗?不知道,无法证明,所以说,第一公理只能靠相信。
所以说,计算机科学的整座大厦都悬在Lambda演算所定义出的这个逻辑想象之上。

Data并不“真的”存在,我们能感受到的Data是它的interface(色),而它的本体却是“空”,所以说,色即是空。
而Data虽然是“空”但并不是什么也没有,只不过是没有我们认为的那种物质感存在感,Data的构建还是要基于其他的procedures(色),所以说:空即是色。

这里可以隐喻出,真实世界的原子并不是物质性的存在,原子也是由非物质的夸克组成,而夸克只是一种震动,可以将夸克理解成信息。而这个信息来自于哪里?来自于一种想象,那就是上帝意识的想象。这种上帝意识同样在我们每个人之中,所以这个世界本质上就是一个想象共同体。

Data隐喻着原子,Procedure隐喻着夸克。
Data隐喻着物质,Procedure隐喻着信息。

2.我为什么要如此设计构建Data的Framework,即selectors and constructors #

— Chapter 2.1.1 Example: Arithmetic Operations for Rational Numbers
因为,抽象上来看,这是一分一合。一收一放。犹如阴阳,漩涡模型的核心,旋转着涌现出丰盛的世界。
selectors:car/cdr、分、放
constructors:cons、合、收
隐喻着,Lisp的漩涡核心,Lisp的元解释器,Meta-circular Evaluator,即,Eval/Apply
Eval就是Selectors,分、放
Apply就是Constructors,合、收
所以说,抽象来看一切都是相通的。

How - 根据这个构建Data的Framework,我将如何构建有理数Rational Numbers
思路如下:
1.设计constructors,即,(make-rat )
2.设计selectors,即,(numer ) 和 (denom )

具体实现:
利用pairs的interface来构建Rational Numbers的interface。
所以,真正的编程都是在编写interface而已,即,面向interface编程。
(define (make-rat n d) (cons n d))

(define (numer x) (car x))

(define (denom x) (cdr x))
不要以为,make-rat直接代理了cons,没有添加新的逻辑,就没有意义。有人会问为什么不直接用cons来作为有理数的interface?
因为,这是一个定位问题,需要在虚空中给有理数一个位置,哪怕有理数的构建procedure中什么也没做,只要有位置,就有概念,就会有想象体的产生。
另外,有了位置,有理数就有空间来加入自己的逻辑。例如实现分子和分母自动归约:
(define (make-rat n d)
(let ((g (gcd n d)))
(cons (/ n g) (/ d g))))

3.Why I design Abstraction Barriers Framework? 我为什么要提出分层思想框架? #

这里,是分层思想的源头。SICP不愧为编程世界的“元典”。
因为,分层是一种抽象的哲学思想,它的设计思路适用于如下领域:Lisp解释器的设计,领域语言的设计(DSL),嵌入式新语言的设计,接口的设计,新类型数据的设计。面向服务的设计。

— Programs that use rational numbers—
Rational numbers in problem domain
— add-rat sub-rat mul-rat sub-rab —
Rational numbers as numerators and denominators
— make-rat number denom —
Rational numbers as pairs
— cons car cdr —
However pairs are implemented

本质上,每一层都是使用下一册提供的interface来构建自己的逻辑,再对上一层提供自己的interface。

另一个洞见是,一种新语言就是一种Data,因为,设计语言的思路和设计新Data的思路样,抽象上来看,他们就是一回事。
Data就是尊从了Black-Box Abstraction的思想。
分层思想实现了Black-Box Abstraction思想,但是不止于此。
分层思想还提供了一种Power,即,分层协作所产生的强大Power
为了解决一类特别领域的业务,设计一个针对性的语言,这就是分层思想。换句话说是针对这个领域设计了一套新类型的DATA,Data对外提供的interface就是能力。Data并不存在,Data只是定位,定位是想象的产物。

所以,之前刻意的遵守web三层架构是有问题的。表现层,业务层,数据层。
每一层都可以再分层,而不是将业务逻辑都写在一个BLOCK中。很多人为了将业务挪出来,就又都放到了数据层,但是数据层不应该放置业务逻辑,这就是概念错位。这也是我为什么不喜欢传统Web开发的原因,太过于教条。没有彰显自由意志的氛围。程序员们都是想着如何学会权威留下的规范。以学会的规范数量为资本。

4.Why I import the concept—Closure Property?我为什么要提出Closure Property这个概念? #

— Chapter 2.2 Hierarchical Data and the Closure Property
因为,要让Data成为“水”,Data能够像水一样流动,无限的自由。
另外,Data是被Procedure实现,所以Procedure的Higher-Order特质,使得Data具备了Closure Property的属性。
换句话说,水的属性不是被添加的,而是Data本来就是水,只不过才被认识到如此。在此之前,我们可能认为Data是大石头或者是盒子。

WHAT - 什么是Closure Property?
例如,自然数通过加法之后的结果还是自然数,我们就说自然数对于加法是闭合的(Closure Property)
而,自然数通过减法之后可能是负数,就不再是自然数,所以自然数对于减法就不是闭合的(Closure Property)
注意,这里不只说自然数是闭合的(Closure Property),而要基于一个行为:加法。所以,Closure Property需要Data和Procedure的配合实现。
所以,本质上是在定位Data的流动感,这个定位的概念就叫做Closure Property,或者说,Lisp中可以通过Porcedure让Data流动起来,我们的编程方向,也是要构建可以让Data流动起来的那种Procedure。
我们来看例子:Pairs经过Cons之后还是Pairs
(cons 1 2) ;这是一个Pair

(cons (cons 1 2)
(cons 3 4)) ;这还是Pair
所以,我们说Pairs对于CONS具有Closure Property

The ability to create pairs whose elements are pairs is the essence of list structure’s importance as a representational tool. We refer to this ability as the closure property of cons. In general, an operation for combining data objects satisfies the closure property if the results of combining things with that operation can themselves be combined using the same operation.

How - Closure Property的用处是什么?
自由组合出复杂机构的Data。
Closure is the key to power in any means of combination because it permits us to create hierarchical structures—structures made up of parts, which themselves are made up of parts, and so on.

注意,SICP中说到的Closure并不是函数式编程中通常说的Closure,SICP中的Closure是一个数学概念。

5.WHY - 我为什么要设计List这个概念出来? #

— Chapter 2.2.1 Representing Sequences
因为,要增加中间层,降低复杂度。这样遇到可以使用List的业务场景,就不需要每次都从Pairs开始构建。 Pairs —> List —> Business

HOW - 如何设计List的interface?
根据构建Data的思想框架framework:Constructors & Selectors
1.设计List的Constructors
我们可以利用Pairs的property构建复杂的数据结构,例如List:
(cons 1
(cons 2
(cons 3
(cons 4 nil)))
加上一层语法糖,List可以如此创建:
(list 1 2 3 4)
;等价于
(cons 1 (cons 2 (cons 3 (cons 4 nil)))

2.设计List的Selectors
我们可以直接使用car和cdr
(define one-through-four (list 1 2 3 4))

one-through-four
(1 2 3 4)

(car one-through-four)
1

(cdr one-through-four)
(2 3 4)

3.设计更有Power的,具有List特色的interface:List operations:
例如:
list-ref
(define (list-ref items n)
(if (= n 0)
(car items)
(list-ref (cdr items) (- n 1))))

(define squares (list 1 4 9 16 25))

(list-ref squares 3)
16

length
(define (length items)
(if (null? items)
0
(+ 1 (length (cdr items)))))

(define odds (list 1 3 5 7))

(length odds)
4

append
(define (append list1 list2_
(if (null? list1)
list2
(cons (car list1) (append (cdr list1) list2))))

(append squares odds)
(1 4 9 16 25 1 3 5 7)

(append odds squares)
(1 3 5 7 1 3 9 16 25)

map
Mapping over lists
目的:遍历list中的每个元素,并且将每个元素做与变量factor的乘法
传统思路如下,具象思维,头痛医头:
(define (scale-list items factor)
(if (null? items)
nil
(cons (* (car items) factor) ;这里写死了业务逻辑:乘法
(scale-list (cdr items) factor))))
高级思路,抽象思维,分离通用模式和业务接口,利用Higher-Order:
(define (map proc items)
(if (null? items)
nil
(cons (proc (car items));这里的proc就是对外开放的接口通道
(map proc (cdr items)))))

(map abs (list -10 2.5 -11.6 17)
(10 2.5 11.6 17)

(map (lambda (x) (* x x)) (list 1 2 3 4))
(1 4 9 16)
这就是Map这个概念的源头,Google的Mapping-Reduce思想就是源于此。
所以说,SICP就是计算机世界的元典。

之前的需求就可以改造成这样了:
(define (scale-list items factor)
(map (lambda (x) (* x factor))
items))

再一次,我们体会到了真正高级的Abstraction是如何思考问题的。
抽象模式,但是构建内部和外部的通道。给个性化业务以空间。
做到:古典主义(规范)和浪漫主义(超越)的平衡。
忽然醒到,J的画就是这种思路,画的主题很具象(规范),画的细很抽象(超越)。

Map一旦称为共识,Lisp解释器就可以为Map构建特殊,让Map基于硬件,加速运行。所以,规范都是快的。就像人的习惯思维不用思考就执行了。

6.有了List,我就可以构建更加复杂的数据结构了 #

— Chapter 2.2.2 Hierarchical Structures
(define x (cons (list 1 2) (list 3 4)))

(lenght x)
3

Lisp中的数据结构只用一种,就是List(要我说应该是只有Pairs),其他的数据结构都是由List组合而来。
为什么,Lisp要只抽象到List这一层就停止了?
为什么,Lisp没有更下沉,只提供Pairs,而不提供List?
答案是,这是一种平衡,即,自由度和可用性的平衡。
而,Lisp是倾向于自由的元典,所以只抽象了一层Data,即,List。
而,想其他语言对于数据的抽象,就很复杂了,因为他们的定位是更加的倾向实用性。更加的可以让用户方便的使用轮子。但缺点就是,如果一直只用这些语言,会一直被表象蒙蔽,看不到本质的源头。

7.现在的程序写的太混乱,如何能让程序变得有序,简单易懂? #

— Chapter 2.2.3 Sequences as Conventional Interfaces
现在的程序的形状太弯弯绕。如何能让程序的形状简单成一条线形。
这就是流程化编程思想的源头(这也是常江的Proc框架的思想源头,元典again!)

WHY - 程序复杂混乱的原因是什么?
因为,每个procedure的input和output都太个性化。
所以,导致procedure之间的组装都加入了个性化的逻辑,这些个性化的逻辑正是复杂性的原因。
HOW - 如何让Procedure的input和output统一呢?
启发,来自于Data概念引出的Closure Property,如果我们让Procedure和Data配合起来,都满足Closure Property,那么,写程序,就会想搭积木/LEGO积木一样,简单明了。因为Procedure称为了统一的链接上下游的标准LEGO块。
而之前的构建,就像是在用橡皮泥进行搭建,每个链接都要花费大量的思考。因为Procedure的链接不统一。
这,正是抽象出Data这个想象概念的作用。
想象出一个不同维度的中间层,让协作变得简单。

我们看例子:求一棵树结构的每个叶子节点的平方后的和
1.传统的思路:以数据为中心,围着数据构建procedure进行操作。这种感觉就像是自己在厨房做菜一样,围着食材进行操作。
(define (sum-odd-squares tree)
(cond
((null? tree) 0)
((not (pair? tree))
(if (odd? tree) (square tree) 0))
(else (+ (sum-odd-squares (car tree))
(sum-odd-squares (cdr tree))))))
2.流程化的思路: 以流程为中心,先构建流程体系,让Data进入流水线。这种感觉就像是食品加工工厂。同样的需求流程化改造后如下:
| emumerate:tree leaves |—>| filter:odd? |—>| map:square |—>| accumulate: +,0 |

(define (sum-odd-squares tree)
(accumulate + 0
(map square
(filter odd?
(enumerate-tree tree)))))
这样以来,tree作为食材(Data)流经了食品加工体系(procedures)
这样就抽象出了一个层,业务宏观层。而之前的方案是没有逻辑分层的,只有一层。分层可以降低复杂性,对于Abstraction的理解是不是又加深一步?
让抽象无处不在吧!
但也不能无节制的抽象,要找到一种平衡。抽象的本质就是分类。一类事就是同一个level的概念。

3.模块的实现:下面我们将各个模块的细节实现吧:这种感觉即是建设加工体系
这让我想起一个故事,一个村子要引入水源,方案1是用桶拎,方案2是铺设水管。
如何选,要根据业务来定,并不能什么需求都大动干戈的铺设水管。
模块1:enumerate-tree,枚举tree,将tree的元素搞成线性list)
(define (enumerate-tree tree)
(cond
((null? tree) nil)
((not (pair? tree)) (list tree))
(else (append (enumerate-tree (car tree))
(enumerate-tree (cdr tree))))))

(enumerate-tree (list 1 (list 2 (list 3 4)) 5))
(1 2 3 4 5)

** 模块2:filter,按照特定逻辑过滤元素 **
(define (filter predicate sequence)
(cond
((null? sequence) nil)
((predicate (car sequence))
(cons (car sequence)
(filter predicate (cdr sequence))))
(else (filter predicate (cdr sequence)))))

(filter odd? (list 1 2 3 4 5)
(1 3 5)
突然有了一个领悟,要允许自己看不懂代码,要用直觉去感受代码的意思,要想象的去理解。要虚看,要会猜。有点像用英语的感觉。

模块3:map,可以复用之前存在的map, 放
(define (map proc items)
(if (null? items)
nil
(cons (proc (car items));这里的proc就是对外开放的接口通道
(map proc (cdr items)))))

(map square (list 1 2 3 4 5))
(1 4 9 16 25)

模块4:accumulate,聚合,收
(define (accumulate op initial sequence)
(if (null? sequence)
inital
(op (car sequence)
(accumulate op initial (cdr sequence)))))

(accumulate + 0 (list 1 2 3 4 5))
15

(accumulate * 1 (list 1 2 3 4 5))
120

(accumulate cons nil (list 1 2 3 4 5))
(1 2 3 4 5)

这里有个领悟,对于我自己,构建体系就是养成好习惯,每一个模块就是一个习惯。数据就是我们的生命本身。如果想改变命运,要看的长远去构建体系(修正习惯)而不是做眼前的自我评判。所以,以后不要评判自己为什么总是懒惰,为什么总是浪费时间,而是要耐心的养成好习惯。
评判自己然后改正,就是用水桶拎水,见效快,但是效益递减:对数曲线。
构建新的习惯体系,就是建立水管系统,见效慢,但是效益递增:指数曲线。
所以,工欲善其事,必先利其器。
看得长远些,做时间的朋友。这也是我为什么投资自己重构底层概念系统的思路。
《掌控习惯》这本书就是在说这个事,要关注体系,不要关注目的。

另一个启发,要让自己的生活流程简单化。这样就不用总想该干啥。一段时间就干一个事,要干的事的类别要少,要抽象。
例如啃SICP的流程:
Data:每个章节
体系:Read—>Write—>Exercise

7.Why I Need design A Picture Language?我为什么要设计一个图像语言? #

— Chapter 2.2.4 Example:A Picture Language
因为,要构建一种特别的图像。如下:
(img) (img)
另外,我们可以用这个实际的需求串联之前讨论过的概念,包括:
1.Data Abstraction
2.Closure Property
3.Higher-order Procedures

因为,这类图像的特殊之处是,从一个基本图像开始,通过组装而成。所以,具有逻辑性,可以用简单的语言来描述,所以,我们可以通过设计一门新语言来描述它。

所以,设计思想还是要遵从framework如下:
1.The language’s primitives: painter
2.Means of combination: beside, below, flip-vert, flip-horiz
3.Means of abstraction: as Scheme Procedures

1.Painter #

In this picture language is that there is only one kind of element, called a painter. A painter draws an image that is shifted and scaled to fit within a designated parallelogram-shaped frame.

painter是一个procedure,目的是画出一个基础图像,如下:
(img)
当,frame变化时候,painter根据frame,自动的进行扭曲来适应frame,如下:
(img) (img) (img)
WHY-为什么要设计Frame的概念?
因为,要遵循分层思想,将图像分离成三个部分:
1.基础图像的绘制:painter
2.基础图像的画框:frame
3.多个画框的组合:operations
分层之后,降低了每一个层的复杂度。
如果没有frame的独立概念,那么要表达扭曲的图像,则要增加painter的逻辑复杂性。painter的参数就不只frame一个了。
例如,frame没有变化,只是更改基础图像,我就就会看到下面效果:
(img) (img)
(img) (img)

HOW - 如何用procedure来实现painter?
(define (segments->painter segment-list)
(lambda (frame)
(for-each
(lambda (segment)
(draw-line
((frame-coord-map frame) (start-segment segment))
((frame-coord-map frame) (end-segment segment))))
segment-list)))

2.Frame #

HOW - 如何用procedure实现frame?
frame的本质是一个四边形,是一个静态的名词,所以我们可以用Data来表示frame,我们可以用三个坐标vector来描述一个矩形:初始点坐标origin,第一条边的坐标edge1和第二条边的坐标edge2。如下:
(img)
Data的LEVEL1的interface实现要符合constructor和selectors的framework。
方案1:use list
;1.constructor
(define (make-frame origin edge1 edge2)
(list origin edge1 edge2))

;2.selectors
(define (origin-frame frame)
(list-ref frame 0)

(define (edge1-frame frame)
(list-ref frame 1)

(define (edge2-frame frame)
(list-ref fame 2)

方案2:use cons
;1.constructor
(define (make-frame origin edge1 edge2)
(cons origin (cons edge1 edge2)))

;2.selectors
(define (origin-frame frame)
(car frame)

(define (edge1-frame frame)
(car (cdr frame))

(define (edge2-frame frame)
(cdr (cdr frame))

HOW - 如何设计frame的LEVEL2的interface?
上面我们实现了frame的基础interface,接下来为了要让frame对外提供特殊的服务,我们需要构建LEVEL2的interface。
frame可以提供什么服务呢?
WHY - 还记得我们为什么要设计frame吗?
为了,要让painter绘画的时候有个参考坐标的画框。
所以,frame提供的服务,就是提供给painter,告诉painter你之前的坐标在frame中的新坐标是什么。
所以,frame提供的服务,就是接收一个坐标vector,返回一个经过frame映射之后的新坐标vector,并且这个新坐标一定在frame之中。
实现思路如下:
对于一个painter提供的新坐标vector v=(x, y)
经过frame的转化后:Origin(Frame) + x·Edge1(Frame) + y·Edge2(Frame)
这里约定,painter提供的坐标是基于标准的1乘1的frame

具体实现如下:
(define (frame-coord-map frame)
(lambda (v)
(add-vert
(origin-frame frame)
(add-vect (scale-vect (xcor-vect v)
(edge1-frame frame))
(add-vect (scale-vect (xcor-vect v)
(edge2-frame frame))

这里又根据抽象思想,分离出了一个新的Data概念:坐标Vector,而不是让frame-coord-map来承担所有的复杂性。
将复杂性剥离出来,是编程高手的体现。
复杂性的剥离,需要用抽象的视角,来定位哪些概念可以独立存在,成为一个Data或者Procedure。动词则抽象为Procedure,名词则抽象为Data。

所以我们还需要来构建Vector
1.Implementation of Level1-Interface
;1.constructor
(define (make-vect x y)
(cons x y))

;2.selector
(define (xcor-vect v)
(car v)

(define (ycor-vect v)
(cdr v)

2.Implementation of Level2-interface: add-vect, sub-vect, scale-vect
思路如下:
add-vect: (x1, y1) + (x2, y2) = (x1 + x2, y1 + y2)
sub-vect: (x1, y1) - (x2, y2) = (x1 - x2, y1 - y2)
scale-vect: s·(x, y) = (sx, sy)
具体实现如下:
(define (add-vect v1 v2)
(make-vect
(+ (xcor-vect v1) (xcor-vect v2))
(+ (ycor-vect v1) (ycor-vect v2))))

(define (sub-vect v1 v2)
(make-vect
(- (xcor-vect v1) (xcor-vect v2))
(- (ycor-vect v1) (ycor-vect v2))))

(define (scale-vect s v)
(make-vect
(* s (xcor-vect v))
(* s (ycor-vect v))))

3.Operations of picture language #

我们实现了The language’s Primitives: Painter/Frame
下一步就要实现It’s means of combination了
如何将基础的图像组合,
这里就是picture language的Level 2 的interface了:beside, below, flip-vert, flip-horiz

忽然有个感悟,感觉通了,对于抽象思想,分层思想:
Level 1 interface = primitives expressions
Level 2 interface = means of combination
Level 3 interface = means of abstraction
抽象就是定位,思考分与合的平衡,抽离出想象的位置。

我们先想象一下如果这些procedure已经实现后的效果,最后再去实现这些procedure,
flip-vert反转后再beside左右并列—(define wave2 (beside wave (flip-vert wave)))—的效果如下:
(img)

上下并列—(define wave4 (below wave2 wave2))—的效果如下:
(img)

我们还可以使用递归来进行组合:
效果如下:
(img) (img)
实现如下:
(define (right-split painter n)
(if (= n 0)
painter
(let ((smaller (right-split painter (- n 1))))
(beside painter (below smaller smaller)))))

(define (corner-split painter n)
(if (= n 0)
painter
(let ((up (up-split painter (- n 1)))
(right (right-split painter (- n 1))))
(let ((top-left (beside up up))
(bottom-right (below right right))
(corner (corner-split painter (- n 1))))
(beside (below painter top-left)
(below bottom-right corner))))))

还记得最初的需求吗,我们可以画出那种四周递归效果
效果如下:
(img)
基于刚才实现的corner-split,我们只需将四个corner-split拼接,实现如下:
(define (square-limit painter n)
(let ((quarter (corner-split painter n)))
(let ((half (beside (flip-horiz quarter) quarter)))
(below (flip-vert half) half))))

所以,当我们让the picture language嵌入lisp,它就可以继承lisp的能力。
包括刚才看到的procedure的组合能力,Data的closure property,procedure的抽象能力define。
接下来还有procedure的Higher-order能力
Higher-Order的本质目的是通过入参组合多个procedure,输出一个新的procedure。这也是遵从着Abstraction思想,Black-Box Abstraction.
但是,这里的输入procedure因为是变量,所以可变,也就是,本质上这个Black-Box有了通道/接口。

所以,Higher-Order正是Means of Abstraction的一种牛逼体现。

我们来看一个使用Higher-Order的需求:构建一个模式,即,四个frame,对外提供的接口为四个可以控制图像方向的procedure,分别是tl tr bl br
具体实现如下:
(define (square-of-four tl tr bl br)
(lambda (painter)
(let
((top (beside (tl painter) (tr painter)))
(bottom (beside (bl painter) (br painter))))
(below bottom top))))

这样一来,flipped-pairs可以用上的square-of-four来实现,如下:
(define flipped-pairs painter

(let ((combine4 (square-of-four identity flip-vert identity flip-vert)))
    (combine4 painter)))

这样,square-limit可以这样实现:
(define (square-limit painter n)
(let ((combin4 (square-of-four flip-horiz identity rotate180 flip-vert)))
(combine4
(corner-split
painter n)))
对比一下之前square-limit的实现,感受一下Higher-Order的精髓:模型思维
(define (square-limit painter n)

(let ((quarter (corner-split painter n)))
(let ((half (beside (flip-horiz quarter) quarter)))
(below (flip-vert half) half))))

Higher-Order思维:
1.定位思维
2.模型思维
3.抽象思维
以上都是一回事

HOW - implementation of Operations: flip-vert , beside, etc #

回过头来,我们现在要真正的来实现这些Operation了,包括:beside,below,flip-vert,flip-horiz
分一下类:
操纵单个painter的扭曲类:flip-vert, flip-horiz, rotate90
组合多个painter的组合类:beside, below

HOW - implementation of flip-xxx
flip-xxx类的Operation的目的是要操纵单个的Painter,使其发生位置的扭曲。而不是内容的变化。所以,只需要操纵frame即可。
也就是说,Operations的抓手interface是frame.
Operations—>Frame—>Painter
我们将这个操纵Painter的行为,抽象成一个概念:transform-painter。
也就是说,这些Operations在抽象上的概念定位就是在transform-painter.
实现的思路就是通过创建一个新的frame来影响painter。
还记得吗?定义frame需要三个坐标:origin, end of edge1, end of edge2
这里要提醒一下,新坐标是要相对于painter的默认frame的,painter的默认frame的坐标为:
origin: (0, 0)
edge1: (1, 0)
edge2:(0, 1)
具体实现如下:
(define (transfor-painter painter origin corner1 corner2)
(lambda (frame)
(let ((m (frame-coord-map frame)))
(let ((new-origin (m origin)))
(painter
(make-frame new-origin
(sub-vect (m corner1) new-origin)
(sub-vect (m corner2) new-origin)))))))
刚才我们定义了operation的统一的模式:操纵painter,下面我们要来看如何实现具体的operation。这里体现的还是Higher-Oder思想
flip-vert:反转
(define (flip-vert painter)
(transform-painter painter
(make-vect 0.0 1.0) ;new origin
(make-vect 1.0 1.0) ;new end of edge1
(make-vect 0.0 0.0) ;new end of edge2

我们还可以定义出一个新的操作:逆时针旋转90度,rotate90,只需要通过定义frame的三个坐标。
(define (rotate90 painter)
(transform-painter painter
(make-vect 1.0 0.0)
(make-vect 1.0 1.0)
(make-vect 0.0 0.0)))

**HOW - implementation of beside
上面,我们实现的是单个的painter的操纵,下面我们要来看如何组合Painter。
beside(左右组合)的具体实现:
(define (beside painter1 painter2)
(let ((split-point (make-vect 0.5 0.0)))
(let (
(paint-left
(transform-painter painter1
(make-vect 0.0 0.0)
split-point
(make-vect 0.0 1.0)))
(paint-right
(transform-painter painter2
split-point
(make-vect 1.0 0.0)
(make-vect 0.5 1.0))))
(lambda (frame)
(paint-left frame)
(paint-right frame)))))

同理,我们可以来实现below(上下组合),具体实现如下:
(define (below painter1 painter2)
(let ((split-point (make-vect 0.0 0.5)))
(let (
(paint-up
(transform-painter painter1
(make-vect 0.0 0.0)
(make-vect 1.0 0.0)
split-point)))
(paint-down
(transform-painter painter2
split-point
(make-vect 1.0 0.5)
(make-vect 0.5 1))))
(lambda (frame)
(paint-up frame)
(paint-down frame)))))

总结Summary #

体会一下抽象思维,一开始我们先假设具体的operation已经实现,例如假设beside,flip-vert已经实现,在此想象的基础上,我们在讨论如何使用Higher-Order的组合例如:corner-split,square-limit。这就是抽象思维,忽略细节,只进行形而上的思考,这样做的好处是,宏观思维,整体掌控,系统性思考。
最后,我们再把欠下的债实现,进入形而下的细节实现,例如:flip-vert的实现。即便是具体实现flip-vert ,我们也要使用我们的思维武器:抽象,Higher-Order,首先,思考operation能否定位出抽象模式,例如:transform-painter。
即,定位:不变的模式和易变的业务。
这种定位思维,就是编程的核心能力。
定位思维=模型思维=抽象思维
形象化一点的描述:Black-Box Abstraction,漩涡模型,interface模型,分层模型

这里总结一下分层模型,called stratified design.
分层模型思维,适用于设计Data,设计Language,
Each level is constructed by combining parts that are regarded as primitive at that level, and the parts constructed at each level are used as primitives at the next level.

后面我们还会设计一种集成电路语言,目的是针对digital-circuit design,在这个新语言中的primitive将是and-gates , or-gate
通过combination将会构建出cpu,memory system。

分层设计思想Stratified design的真正意义:放松、慢、甩鞭子的隐喻
让Power可以在层与层之间继承,积聚复杂度。复杂度就是力量。
编程的目的就是构建复杂Build Abstraction,但是因为脑力的限制,编程的要解决的核心问题又是如何控制复杂Control Complexity。
即想要复杂,又担心过于复杂。
所以,分层思想就现实了他的价值,
分解:分解多层,控制每一层的复杂度。
合并:层与层之间的连接,让复杂度可以累加。
这就像太极拳,每一个动作都是放松,但是多个动作的力量可以累加,最终像甩鞭子一样爆发。
所以,分层思想所能爆发出来的最终复杂度是指数级的,太极拳的发力也是指数级的。
这也是放松流动的价值:每一件事都做好,每件事之间都有继承性,最终会爆发出意想不到的效果。
关注积累(放松),而非力量(紧张),使用好时间这个链接器。所以,要去构建体系(铺设水管)。
例如,我现在构建的这层认知(啃SICP)未来的应用层(做项目)会用到。

让每一天都很简单,然后用时间将它们连接起来。关键是要有连续性,力量才能呈放大式的积累,最终一刻爆发。所以,选好一个方向,不要变,不要急,时间会让你成为指数曲线的。

self-refer自引用,closure property,构建指数的秘密。

TO BE CONTINUE…

 
0
Kudos
 
0
Kudos

Now read this

第四章.挥舞函数(4.Wielding functions)[完成]

翻译 Secrets of the JavaScript Ninja (JavaScript忍者禁术) 第四章.挥舞函数(4.Wielding functions) 本章重点: # 1.为什么匿名函数如此重要 # 2.函数中的递归 # 3.函数可以被引用后再调用 # 4.如何为函数缓存索引 # 5.利用函数的能力来实现记忆 # 6.利用函数上下文 # 7.处理参数长度 # 8.判断一个对象是否为函数 # 在上一章我们了解到函数作为自然类型的对象(first-order... Continue →