使用Scheme模拟类和对象

a. 类和对象

函数定义可以解释为一个类,并且函数调用可以扮演对象的角色。换句话说,lambda表达式可以被视为类,而闭包可以被视为对象。

下面定义一个point类,lambda表达式将作为point类的实例对象句柄返回。这个对象句柄实际上是一个调度程序,它在给定message参数作为输入的情况下返回匹配的方法。

(define (point x y)
  (letrec ((getx (lambda () x))
           (gety (lambda () y))
           (add  (lambda (p)
                    (point
                       (+ x (send 'getx p))
                       (+ y (send 'gety p)))))
           (type-of (lambda () 'point)))
    (lambda (message)
      (cond ((eq? message 'getx) getx)
            ((eq? message 'gety) gety)
            ((eq? message 'add)  add)
            ((eq? message 'type-of) type-of)
            (else (error #f "Message not understood"))))))

add方法中,我们使用send函数向对象发送消息。send函数仅查找方法,并使用apply来调用方法。

(define (send message obj . par)
  (let ((method (obj message)))
    (apply method par)))

b. 类的通用模式

一个类通常包含:构造参数、实例变量、方法和self方法。方法除了通过上面的letrec定义外,还可以通过define来简化定义。

(define (class-name construction-parameters)
 (let ((instance-var init-value)
        ...)

   (define (method parameter-list)
     method-body)

   ...

   (define (self message)
     (cond ((eqv? message selector) method)
       ...

       (else (error #f "Undefined message" message))))

   self))

我们再实现一个实例化对象的函数,顺便给send函数增加一点错误处理能力。

(define (new-instance class . parameters)
  (apply class parameters))

(define (send message object . args)
  (let ((method (object message)))
    (cond ((procedure? method) (apply method args))
          (else (error #f "Error in method lookup " method)))))

c. 类的示例

现在我们重新写一下point

(define (point x y)
 (let ((x x) 
       (y y))

   (define (getx) x)

   (define (gety) y)

   (define (add p)
    (point
     (+ x (send 'getx p))
     (+ y (send 'gety p))))

   (define (type-of) 'point)

   (define (self message)
     (cond ((eqv? message 'getx) getx)
           ((eqv? message 'gety) gety)
           ((eqv? message 'add)  add)
           ((eqv? message 'type-of) type-of)
       (else (error #f "Undefined message" message))))

   self))

下面我们模拟一个场景,其中我们创建了两个点,并将它们绑定到变量pq,再将pq的和绑定到变量p+q。最后我们通过send发送getxgety消息来检查结果是否符合预期。

1> (define p (new-instance point 2 3))

2> (send 'getx p)
2

3> (define q (new-instance point 4 5))

4> (define p+q (send 'add p q))

5> (send 'getx p+q)
6

6> (send 'gety p+q)
8

d. 继承

上面我们已经在Scheme中简单的模拟了类和对象,继承是面向对象中更高级的概念。

我们先将对象简单的分成两部分super(super part)和self(subclass part)。基类的对象是上半部分,
我们将其绑定到super,分派器dispatch作为下半部分仍然绑定到self

(define (class-name parameters)
 (let ((super (new-part super-class-name some-parameters))
       (self 'nil))
   (let ((instance-variable init-value)
         ...)

     (define (method parameter-list)
       method-body)
     ...

     (define (dispatch message)
       (cond ((eqv? message 'selector) method)
             ...
             (else (method-lookup super message))))

     (set! self dispatch))

   self))

下面将实现一个大多数面向对象语言中都有的基础类object。所有对象可以通过继承形成一个super链,objectsuper为空,作为整个super派发链的终结。

(define (object)
  (let ((super '())
        (self 'nil))

   (define (dispatch message)
       '())

   (set! self dispatch)
   self))

我们再添加new-instancenew-partsendmethod-lookup这几个函数来对面向对象做更完善的支持。new-part用于构造对象的部件,而new-instance用于构造具体类型的对象,这里暂时看上去长得一样。

(define (new-instance class . parameters)
  (apply class parameters))

(define (new-part class . parameters)
  (apply class parameters))

(define (method-lookup object selector)
 (cond ((procedure? object) (object selector))
       (else
         (error #f "Inappropriate object in method-lookup: "
                 object))))

(define (send message object . args)
 (let ((method (method-lookup object message)))
  (cond ((procedure? method) (apply method args))
        ((null? method)
         (error #f "Message not understood: " message))
        (else
         (error #f "Inappropriate result of method lookup: "
                 method)))))

e. 继承的示例

我们借用c小节的示例point类型,在此基础上,我们通过继承来派生出一个带颜色的点color-point类型。

(define (color-point x y color)
 (let ((super (new-part point x y))
       (self 'nil))
   (let ((color color))

     (define (get-color)
       color)

     (define (type-of) 'color-point)

     (define (dispatch message)
       (cond ((eqv? message 'get-color) get-color)
             ((eqv? message 'type-of) type-of)
             (else (method-lookup super message))))

     (set! self dispatch))

   self))

测试下我们的颜色点,并将两个颜色点相加(注意,两个颜色点相加后不是颜色点,只是普通的点)

1> (define cp (new-instance color-point 5 6 'red))

2> (send 'get-color cp)
red

3> (send 'getx cp)
5

4> (send 'gety cp)
6

5> (define cp-1 (send 'add cp (new-instance color-point 1 2 'green))) 

6> (send 'getx cp-1)
6

7> (send 'gety cp-1)
8

8> (send 'type-of cp-1)
point

9> (send 'get-color cp-1)
Undefined message get-color

f. self解释

继承的模拟涉及将对象部分聚合为整体对象。为了将整个对象绑定在一起,self所有部分的(对象句柄)必须指向最专门的对象部分。

图中展示了我们想要实现的目标。左侧的绿色层次结构显示了现在的情况,其中self每个级别均指向当前对象部分。右侧的黄色层次结构显示了我们希望建立的情况。

self-super.gif

self必须指向最顶层的对象部分,如果不是这样,就根本无法从“非顶层对象部分”访问“顶层对象部分”

g. 虚拟方法示例

现在展示虚拟方法的效果。我们将定义一个基类x,一个子类y(y继承自x)。在这2个对象中,我们都将看到一个额外的方法set-self!,该方法负责将self更改为适当的对象。注意!使用xy类的程序员对set-self!不感兴趣,所以set-self!方法是对象的内部事务。

(define (x)
 (let ((super (new-part object))
       (self 'nil))

   (let ((x-state 1))

     (define (get-state) x-state)

     (define (res)
       (send 'get-state self))

     (define (set-self! object-part)
         (set! self object-part)
         (send 'set-self! super object-part))

     (define (self message)
         (cond ((eqv? message 'get-state) get-state)
               ((eqv? message 'res) res)
               ((eqv? message 'set-self!) set-self!)
               (else (method-lookup super message))))

      self)))
(define (y)
 (let ((super (new-part x))
       (self 'nil))

   (let ((y-state 2))

     (define (get-state) y-state)

     (define (set-self! object-part)
         (set! self object-part)
         (send 'set-self! super object-part))

     (define (self message)
         (cond ((eqv? message 'get-state) get-state)
               ((eqv? message 'set-self!) set-self!)
               (else (method-lookup super message))))

      self)))

下面是一个小示例,它可以解释self的效果。将res消息发送到y对象b会得到值2,表明该res方法调用了y对象的get-state(不是xget-state)。yres方法是从x继承而来。

1> (define a (new-instance x))

2> (define b (new-instance y))

3> (send 'res a)
1

4> (send 'res b)
2

为了得到上面示例的结果,我们对new-instance函数还需要做一些小小的修改。我们在new-instance中调用一个virtual-operations的函数,该函数将set-self!消息发送到对象,这将依次激活所有级别对象的set-self!方法。

(define (new-instance class . parameters)
 (let ((instance (apply class parameters)))
   (virtual-operations instance)
   instance))

(define (virtual-operations object)
  (send 'set-self! object object))

h. 面向对象的一些思考

这里只是在Scheme中对面向对象的一个简单模拟。在此基础上,我们还可以实现一个更完善的面向对象系统。例如:

  1. 通过槽(Slot)来管理属性和方法(槽是Key/Value对的列表)
  2. 所有的动作(actions)都是消息
  3. 对象与对象之间只能通过消息来交互
  4. 基于原型链(Prototypes)的方式实现继承(当对象收到一条消息时,它会寻找一个匹配的槽,如果找不到,则查找将首先在其原型中递归地继续进行)
  5. 多重继承只需要将原型添加到对象的原型链中即可(当响应消息时,查找机制对原型链进行深度优先搜索)
  6. 对象的继承和实例化可以都通过复制(clone)的方式进行

i. 参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341

推荐阅读更多精彩内容