第3. 一切皆对象和面向对象的理论基础

老庄是反对一切皆对象的,而TrustNo1在javaeye的一篇帖子上说:

第一,我可以很负责的说,OO的,70年代成型,80年代在理论基础上就给人毙掉。从这种意义上说不是OO死不死的问题,而是OO还活着么?当然理论基础给人毙掉,不是说没有用。

我先说面向对象的理论基础的问题,至于一切皆对象稍后再表。

所谓面向对象的理论基础其实是没有的,原因很简单,面向对象根本就不是一种计算模型。在第一次软件危机的那个时代,对与计算机的非数值计算应用的讨论以及对于可计算性问题的研究和发展,大抵确立了几种主流的计算模型:递归函数类,图灵机,Lambda演算,Horn子句,Post系统等等。

其中递归函数类是可计算性问题的数学解释;图灵机是图灵解决可计算问题的时候所设计的装置,其后成为计算机的装置模型,与图灵机相关的形式语言和自动机成为了命令式语言的理论基础;lambda演算成为了函数式语言的理论基础;Horn子句是prolog这类逻辑语言的理论基础。但是我们惊讶的发现,面向对象没有计算模型的理论基础,换而言之,面向对象根本就不是从可计算性的研究上发展过来的,那么面向对象的理论基础的价值本身就不大。

所以我很奇怪的一个问题就是TrustNo1所谓的面向对象在80年代理论基础上给人毙掉的说法是从何而来的?既然面向对象本质上不是一种计算模型,那么它大抵上只能归结为一种应用技术,应用技术自然可以从各种不同的领域里得到相似的应用,那么毙掉的理论基础所指的又是什么呢?甚怪之。

既然面向对象不是一个计算模型,那么我们可以从不同的角度推断出OO的各种形态,老庄已经出给了从ADT引出OO的问题以及例子,我就不罗嗦了,我给一个从Functional Programming出来的例子,其实就是SICP里的Data as Procedure。

(define (make-user name age sex)
  (define (dispatch message)
     (cond ((eq
? message 'getName) name)
           ((eq? message 'getAge) age)
           ((eq? message 'getSex) sex))
           (else (error 'messageNotUnderstand))))
  dispatch)

然后我们就可以

(define vincent (make-user 'Vincent 24 'Male))
(vincent 
'getName)

自然的,如果我调用

(vincent 'sayHi)

会得到一个messageNotUnderstand的runtime错误,这就是一个很自然dyanmic type的对象封装,最早的面向对象系统Smalltalk和CLOS基本上都是这个路子,于是有一个问题,为什么最早的面向对象系统都是dyanmic type?这里就跟lambda演算有关了。

lambda演算这个计算模型根本的一个假设就是,对于任何一个定义良好的数学函数,我都可以使用lambda抽象来表述他的求值,因此无论是什么东西你能够构造lambda抽象的话,我就能计算。这个地方东西很多,大家可以找找lambda演算相关的资料,这里我说三件事(由于lambda太难输入,我用scheme程序代替,然后由于alpha变化,beta规约和eta规约我也用scheme伪码来模拟。)

第一个是数值的表述,其实这个很简单,不考虑丘奇代数的系统的话,我们可以把数值表示成单值函数:

(define one (lambda (x) 1))

这个东西无论给什么x都返回1,然后根据lambda演算里的alpha变换,这个lambda抽象等价于数值1。因此,对于所有的数值,我们可以按lambda演算处理。

第二个是bool的表达,也就是如何逻辑进行lambda抽象,下面我直接给出了,缺省认为大家都看了SICP,对Scheme也颇有心得。

(define true-new (lambda (x y) x)) ;;;这个函数也叫select-first
(define 
false-new (lambda (x y) x));;;这个函数也叫select-second

(define 
if-new (lambda (conditon if-true if-false) (condition if-true if-false)))

然后我就可以做一个测试

(if-new true-new 3 4)
3

(
if-new false-new 3 4)
4

因此,对于所有bool我们可以按lambda演算来处理

第三个是自定义类型,这里我们还是先看一个Lisp里的例子,序对。

(define (cons a b) (lambda (dispath) (dispatch a b)))
(define (car list) (list select
-first))
(define (cdr list) (list select
-second))

这里依旧是high-order,我们来测试

(define list1 (cons 1 2))
(car list1)
1
(cdr list1)
2

这里大家自己用beta规约算一下,就发现的确是这样的。这里我们又可以看到,在lambda演算里,根本没有数据或者类型。有的永远各种各样的lambda抽象而已(目前已经有了带类型的lambda演算)。这里说一句题外话,SICP里的Data as Procedure其实就是在说这个问题,不过他没明说,而是用了一种特殊的用法而引出了消息传递风格,我觉得这里SICP有些顾此失彼,对于data as procedure我总结的一般形式是

(define (construct-function value1 value2 value3 value4valuen) (lambda (dispatch) (dispatch value1 value2 value3 value4valuen)))
(define (select
-function1 data) (data select-first))
(define (select
-function2 data) (data select-second))
.
(define (select
-functionn data) (data select-last))

综上所述,我们看到在lambda演算里,一切都是lambda抽象,然后对于不同的lambda抽象使用alpha变换,beta规约和eta规约,表述各种不同计算。看,在面向对象之前就已经有了一切皆某某的完美的计算理论存在了。而且回顾一下:

(define (make-user name age sex)
  (define (dispatch message)
     (cond ((eq
? message 'getName) name)
           ((eq? message 'getAge) age)
           ((eq? message 'getSex) sex))
           (else (error 'messageNotUnderstand))))
  dispatch)

(define vincent (make
-user 'Vincent 24 'Male))
(vincent 
'getName)

我们有理由说,对象其实就是一个lambda抽象,所以一切皆对象不过是一切皆lambda抽象的演化,这也是为什么SICP里把面向对象称作一种“方便的界面”而不是一种抽象的方法的原因了。那么对象和lambda抽象又什么区别呢?

嘿嘿,熟悉FP的人都应该知道了,就是Side-Effect,副作用。对象允许对其内部状态进行修改,那么这个东西就破化了eta规约的前提条件,也就是说允许修改内部状态的东西,已经不是一个可以进行lambda计算的lambda抽象了。因此暂且给一个别的名字吧,就叫对象吧.....因此我认为,对象很大程度上是一种带有副作用的lambda抽象。

我在有一篇blog上写了Say sorry to object-oriented,里面给了一只用对象作分支的例子,这里就不重复了,有兴趣大家可以去看一下(刚才好像在JavaEye上看到一个说法,说Everything is Object是Smalltalk的广告语,唉,Smalltalk里的的的确确everything is object啊。)

这里我们来总结一下,面向对象作为一种“方便的界面”主要解决了一个局部化和模块化的问题,这是从lambda演算和函数编程的角度来看面向对象技术。(taowen在他的一篇blog上说,面向对象局部化了Side-Effect,我深以为然),这个东西我个人认为更加接近面向对象本来的意思,而不是由ADT里发展出来的带类型的对象系统的那个意思。因此老庄不以为然的面向对象类型系统,我也不以为然。但是面向对象作为lambda抽象的界面,我觉得还是很不错的。这一点在Smalltalk和Ruby里都有不错的体现。