2.2.4 例子:一个图片的语言

2.2.4 例子:一个图片的语言

这部分表示了一个简单的语言,这个语言用来画图。它演示了数据抽象和闭包的威力,
也以一个必要的方式,显式地显示出高阶程序。语言被设计成,使它容易实验,
结合图2.9中的这样的模式。这模式是重复的图片的组合。这个图片可以拉伸与缩放。
在这种语言中,数据对象能被组合,能被表示为程序而不是列表结构。正如cons,它能满足闭包属性,
允许我们很容易地随意的构建复杂的列表结构,在这种语言中的操作,也满足闭包属性,允许我们
很容易地随意的构建复杂的模式。

图2.9 用图片语言生成的设计

* 图画语言

在1.1部分中,当我们开始编程的研究时,我们强调了通过聚焦于语言的原生对象
组合的方法和抽象的方法,来描述一个语言的重要性。在这里,我们遵循这个框架。

这个图画语言的优雅的部分是它仅有一类元素。叫做小画笔。一个小画笔画一个图画,
这个图画能被拉伸和缩放来适合一个特定形状的帧。例如,这有一个原生的小画笔。
我们能叫它波浪。这波浪能画一个粗糙的线条绘制,如图2.10所示。图画的实际形状依赖于帧。
在图2.10中的所有的四幅图都是由同一个小画笔生成的,但是是对应于四个不同的帧的。
小画笔能变得更加复杂一些。原生的小画笔叫做rogers,它画了Mit创建者的画像。如图2.11.
在图2.11中的四个画像使用了与图2.10相应的帧。

为了组合图画,我们能使用各种操作来从给定的小画笔来组成新的小画笔。例如,
beside(并排)操作是输入两个小画笔,生成一个新的组合的小画笔。它画图,在帧的左半边
画第一个图,右半边画第二个图。相似的是below操作,是输入两个小画笔,生成一个新的组合的小画笔。
它把第一图画在第二个图的下边。 一些操作转换一个单独的小画笔生成一个新的小画笔。
例如flip-vert,输入的是一个小画笔,生成一个新的。它画图是上下翻转的,flip-horiz生成了一个
新的小画笔,画原图为左右翻转的。


i     /     \                    ----------------------    
|    /       \                  /                    /
i\   \       /                 /                    /
| \- -\     /--\              /                    /
i\  /\          \            /                    /
| \/  \      \   \          /                    /
i     /   /\  \            /____________________/
|____/___/__\__\______
      
          |
          i
          i              ------------------------------
          i              i
          i              i
-----------              i

图2.10 波浪的小画笔生成的图画,相应的是4个不同的帧。帧用点线来表示,不是图画的一部分。

图2.12 显示了一个小画笔叫做wave4的图,是由wave开始的两个阶段的组合而成。
(define wave2 (beside wave (flip-vert wave)))
(define wave4 (below wave2 wave2))

以这种方式构建复杂的图画时,我们显式地显示了这个事实,它是在语言的组合的方法
之下,小画笔是闭合的。两个小画笔的并排操作和上下并排的操作,结果还是一个小画笔。
因此,我们在制作更复杂的小画笔时,能使用它作为一个元素。正如使用cons,构建列表结构,
我们的数据的闭合的属性在组合的方法之下,对创建复杂的结构,仅使用很少的操作的能力
是很关键的。

扫描二维码关注公众号,回复: 2635509 查看本文章

一旦我们能组合了小画笔,我们要能够抽象组合小画笔的经典的模式。
我们将实现小画笔操作以scheme程序。这意味着在这个图画的语言中,
我们不需要一个特别的抽象机制。因为组合的方法是一个普通的scheme程序,
我们能自动拥有用小画笔的操作来做任何事的能力,就像我们在使用程序一样。
例如,我们能抽象在wave4中的模式,如下:

(define ()
   (let ((painter2 (beside painter (flip-vert painter))))
        (below painter2 painter2)   
   )
)

并且定义wave4作为这个模式的一个实例:

(define wave4 (flipped-pairs wave))

我们也能定义递归的操作。这是一个让小画笔分开,向右分支的操作。
如图2.13和 图2.14。

(define (right-split paiter n)
   (if (= n 0)
        painter
       (let ((smaller (right-split painter (- n 1))))
            (beside painter (below smaller smaller))
       )
   )
)

 _______________________       ____________________________
|           |           |      | 向上 |向上 |              |
|           |  向右分支 |      | 分支 |分支 |角分支n-1     |
|           |  n-1      |      | n-1  |n-1  |              |
|           |___________|      |______|_____|______________|
|本体       |           |      |            | 向右分支n-1  |
|           | 向右分支  |      |  本体      |______________|
|           |   n-1     |      |            | 向右分支n-1  |
|___________|___________|      |____________|______________|

   向右分支 n                       角分支n

图2.13 向右分支和角分支的递归计划

我们能介绍一下平衡的模式,通过向上分支,正如它向右分支。(见练习2.44和图2.13和图2.14)

(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)))    
       )

  )

)

通过适当地把corner-split放置四个副本,我们得到一个叫square-limit的模式,
它能应用到图2.9中的wave和roger.

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

练习2.44
定义一个up-split 使用corner-split.与right-split相似,除了交换below和beside的角色。

* 高阶操作
除了抽象组合小画笔的模式,我们能工作在更高的层次上,抽象组合的小画笔的操作的模式。也就是我们能
视小画笔的操作为元素,来控制并且能写出这些元素的组合的方法。程序是以小画笔的操作为参数,创建
新的小画笔的操作。

例如,flipped-pairs和square-limit这两个程序都安排了一个小画笔图画的四个副本在一个方框的模式中。
它们的不同仅是如何放置副本。抽象小画笔的组合的这个模式的一个方法是用如下的程序,它带有四个小画笔
的操作,生成一个小画笔的操作。 输入的小画笔带有一个参数。输出的小画笔它以那四个操作转换了
一个小画笔,在一个方框中安排了结果。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 ((combine4 (square-of-four flip-horiz identity rotate180 flip-vert)))
             (combine4 (corner-split painter n)))
)

练习2.45
right-split和up-split能被表达为一个通用的split的一个实例。定义一个split程序,
有如下的解释属性。

(define right-split (split beside below))
(define    up-split (split below beside))

来生成right-split和up-split程序,作为一个已经定义的程序的相同的行为。


* 帧
在我们能显示如何实现小画笔和它们的组合的方法之前,我们必须先考虑帧。
一个帧能用三个向量来描述。一个是原始向量,另两个是边向量。
原始向量指定的是帧的原点到平面上某个绝对的原点的距离。边向量指定的是帧的
顶点到帧的原点的距离。如果两个边是垂直的,帧是矩形的,否则是一个普通的菱形。

图2.15显示了一个帧和它的相关的向量。根据数据抽象,我们不需要指定帧如何被表示,
而是说这有一个组装子make-frame,它有三个向量为参数生成一个帧。相应的有三个选择子,
origin-frame,edge1-frame和edge2-frame(见练习2.47)
      /  \
     /    \
     \    /
      \  /
       \/
        \
         -\
           -\
             O
图2.15  用三个向量描述的一个帧。一个原点和两个边

为了指定图像,我们将使用在单位框中的坐标。(0<=x,y<=1)
在任何一个帧中,我们关联一个帧坐标映射,它被用来拉伸和缩放图像来适应帧。
映射转换了单位的方框为帧,通过映射向量v=(x,y)到向量和

Origin(Frame)+x*Edge1(Frame)+y*Edge2(Frame)

例如,(0,0)被映射到帧的原点,(1,1)映射到原点的对角线处的顶点上。
(0.5,0.5)映射到帧的中心位置。我们能创建一个帧的坐标映射,使用如下的程序:

(define (frame-coord-map frame)
  (lambda (v) (add-vect (orgin-frame frame)
                        (add-vect
                          (scale-vect (xcor-vect v) (edge1-frame frame))
                          (scale-vect (xcor-vect v) (edge2-frame frame)))
              )
  )
)

注意应用frame-coord-map映射到一个帧,返回一个程序,这个程序输入一个向量,
返回一个向量。如果输入向量是一个单位方框,返回向量在帧中。例如:

((frame-coord-map a-frame) (make-vect 0 0))

返回的向量如下

(origin-frame a-frame)

练习2.46
一个二维的向量v,从原点指向一个点,这个点能被表示为一个数对,
这个数对由x坐标和y坐标组成。通过使用一个组装子make-vect和相应的
选择子xcor-vect和ycor-vect,来实现向量的数据抽象。使用的你的组装子和
选择子,实现程序add-vect,sub-vect,scale-vect,它们执行的操作是
向量加法,向量减法,向量的数乘。

(x1,y1)+(x1,y2)=(x1+x2,y1+y2)
(x1,y1)-(x1,y2)=(x1-x2,y1-y2)
s*(x,y)=(s*x,s*y)

练习2.47
对于帧,这有两个可能的组装子。

(define (make-frame origin edge1 edge2)
  (list origin edge1 edge2)
)

(define (make-frame origin edge1 edge2)
  (cons origin (cons edge1 edge2))
)

为了生成帧的实现,对任何一个组装子,提供一些合适的选择子。


* 小画笔
一个小画笔被表示为一个程序,给一个帧作为参数,画一个特定的图像,以拉伸
和缩放来适合帧。也就是说,如果p是一个小画笔,f是一个帧,那么,我们生成
p的图像,在f中。那么通过调用p,以f为参数。

原生的小画笔是如何被实现的细节依赖于图像系统的特定的特性和被画的图像的类型。
例如,假定我们有一个程序,这个程序是draw-line,它负责在屏幕上的两个特定的点之间,
画一条线。那么我们能为了线的绘制创建一个小画笔,例如,在图2.10中的wave小画笔,
这个小画笔有线段的列表,如下所示:

(define (segment->painter segment-list)
  (lambda (frame)
    (for-each
        (lambda (segment)
                (draw-line ((frame-coord-map frame) (start-segment segment))
                           ((frame-corrd-map frame) (end-segment segment))))
        segment-list
    )
  )
)

线段们被给出,以相对于单位方框的坐标方式。对于列表中的任何一个线段,
小画笔用帧坐标映射来转换线段的终点,并在已转换后的点之间画线。

在图画的语言中,表示小画笔为程序,体现了一种强有力的抽象隔离能力。
我们基于一系列的图形能力,能创建和混合各个原生的小画笔。它们的实现细节
并不被关注。任何程序能作为一个小画笔,提供一个帧作为参数,在帧上画些什么。


练习2.48
在平面上,一个有向的线段能被表示成向量的一个数对。一个向量是从原点到线段
起点,另一个向量是从原点到线段的终点。使用你的练习2.46中的向量的表示法,
定义一个线段的表示法,它有一个组装子make-segment,选择子 start-segment和
 end-segment。

练习2.49
使用segments-painter来定义如下的原生的小画笔。

a.画指定的帧的外框的小画笔。

b.以*为元素,画帧的两条对角线。

c.以菱形块为元素,画帧的对边的中点的连线。

d. 波浪小画笔


* 转换和组合的小画笔
在小画笔上的操作,(例如,上下对称或者是并排)通过创建一个小画笔,这个小画笔调用
原有的小画笔,并且对应着从参数帧推导出新的帧,来生效的。
因此,例如,flip-vert不是必须知道一个小画笔是如何对称的,它仅必须如何把一个帧
上下翻。对称的小画笔仅使用原有的小画笔,但是是在翻转后的帧中。

小画笔的操作基于程序transform-painter,这个程序以一个小画笔和如何转换一个帧的信息
作为参数,生成一个新的小画笔。一个转换的小画笔,当调用一个帧时,它转换帧和在转换的帧上
调用原有的小画笔。transform-painter的参数是点(表示成向量),这些点指定了新帧的角:
当映射帧时,第一个点指定了新帧的原点,其它两个指定了边向量的终点。因此,
有单位方框的参数指定了一个包括原有帧的帧。

(define (transform-painter painter orgin 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)))    
               )
          )
  )
)

这有怎么上下对称小画笔的图像的方法:

(define (flip-vert painter)
   (transform-painter painter
                      (make-vect 0.0 1.0)
                      (make-vect 1.0 1.0)
                      (make-vect 0.0 0.0)))

使用transform-painter, 我们能很容易地定义新的转换。例如,
我们能定义一个小画笔,它能把它的图像收缩到给定的帧的右上角中。

(define (shrink-to-upper-right painter)
        (transform-painter painter
                           (make-vect 0.5 0.5)
                           (make-vect 1.0 0.5)
                           (make-vect 0.5 1.0)))

其它的转换能旋转图像,你计数钟一样,旋转90度。

(define (rotate90 painter)
        (transform-painter painter
                           (make-vect 1.0 0.0)
                           (make-vect 1.0 1.0)
                           (make-vect 0.0 0.0))
)

或者聚焦图于帧的中心区:

(define (squash-inwards painter)
         (transform-painter painter
                            (make-vect 0.0 0.0)
                            (make-vect 0.65 0.35)
                            (make-vect 0.35 0.65))
)

帧转换对于组合两个或者多个小画笔的定义方法也是很重要的。
程序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))))

)

注意小画笔如何进行数据抽象,特别是小画笔的表示法为程序,让beside(并排操作)
容易实现。beside(并排操作)程序不需要知道关于组件性的小画笔的细节,除了知道
任何一个小画笔将在给定的帧上要画点什么。


练习2.50
定义一个转换flip-horiz,它让小画笔水平方向上对称,还有定义转换,它旋转小画笔像计数钟
一样的,旋转180和270度。

练习2.51
为了小画笔定义上下并排的操作。below(上下并排的)以两个小画笔为参数。作为结果的小画笔,
给定一个帧,画第一个小画笔在帧的底部,画第二个小画笔在帧的顶部。以两种不同的方式定义below,
第一种是写一个与上述的beside相似的程序,另一个是使用beside和从练习2.50中得到的合适的旋转操作。


* 健壮性设计的语言的层次

图画语言练习了一些关键性的思想,这些思想是我们已经介绍了的关于程序和数据的抽象。基础的数据抽象,
小画笔使用程序抽象来实现,它让语言能以一种统一的方式,来处理不同的基础的绘画能力。
组合的方式满足了闭包属性,它允许我们很容易地构建复杂的设计。最后,对于抽象程序的所有的工具
对于我们组合小画笔的抽象的方法来说都是有用的。

我们也获得了一些关于语言和设计的重要的思想。这是分层设计的方法。它的观念是一个复杂的系统,
应该被结构化为一些层次的序列,这些层次的序列用语言的序列来描述。也就是一层用一种语言来描述。
任何一个层被组装,组合的部分被认为是它那个层的原生的内容,在任何一个层中组成的部分在下一个层中
被视为原生的部分。在一个分层的设计的任何一个层中使用的语言都有原生对象,组合的方法,并且有适合细节的那个层的抽象的方法。

分层设计遍及复杂系统的工程实践。例如,在计算机工程中,电阻器和晶体管被组合(在一个模拟电路的语言中被描述)来生成例如与门,或门之类的部分,对于数据电路的设计来说,它们形成了语言的原生部分。
这些部分组合来构建处理器,总线结构,内存系统,它们进一步的组合起来,形成了计算机,使用适合的语言
形成了计算机的架构。计算机被组合起来,形成了分布式系统,使用适合的语言来描述网络的内部连接,等等。

作为分层的一个微型的例子,我们的图画语言使用了原生的元素(原生的小画笔),这个小画笔是用一个指定了
点和线来提供线段的列表的语言来创建的。图画语言的我们的描述的主体聚焦于组合这些原生的对象,使用几何的
组合例如beside(并排)和 below(上下并排)。我们也工作在一个更高的层次,视beside和below为原生的对象,在一个语言中操纵它们,例如d square-of-four能捕捉到组合的几何组合子的共同的模式。

分层设计有利于使得程序健壮,也就是它使得在规范中的很小的修改,在程序中,将对应的需要很小的修改。
例如,假定我们要修改基于图2.9中的wave小画笔的图画。我们能工作在最低层的修改wave元素的细节性的外观;我们能工作在中间层,来修改corner-split来复制wave;我们能工作在最高层来修改square-limit如何修改角的四个副本。总之,在一个分层设计的任何一层都提供了一个不同的词汇表来表达系统的特征,有不同的类型的能力来修改它。

练习2.52

对图2。9中的wave的四角限制 做一些修改,以工作在如上描述的不同的层次的方式,特别是:

a.对练习2。49中的原生的波浪小画笔加上一些线段(例如加上一个笑脸)
b.用corner-split修改组装的模式,(例如,仅使用up-split或者是right-spit中的一个,来代替使用两个)
c.修改square-limit的版本,使用square-of-four,为了以一种不同的模式来形成角。(例如你能让这个人看起来从框的四角向外扩展。)

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/81448045