随笔-341  评论-2670  文章-0  trackbacks-0

大家看到这个标题肯定会欢呼雀跃了,以为功能少的语言就容易学。其实完全不是这样的。功能少的语言如果还适用范围广,那所有的概念必定是正交的,最后就会变得跟数学一样。数学的概念很正交吧,正交的东西都特别抽象,一点都不直观的。不信?出门转左看Haskell,还有抽象代数。因此删减语言的功能是需要高超的技巧的,这跟大家想的,还有跟go那帮人想的,可以断定完全不一样。

首先,我们要知道到底为什么需要删减功能。在这里我们首先要达成一个共识——人都是很贱的。一方面在发表言论的时候光面堂皇的表示,要以需求变更和可维护性位中心;另一方面自己写代码的时候又总是不惜“后来的维护者所支付的代价代价”进行偷懒。有些时候,人就是被语言惯坏的,所以需要对功能进行删减的同时,又不降低语言的表达能力,从而让做不好的事情变得更难(完全不让别人做不好的事情是不可能的),这样大家才会倾向于写出结构好的程序。

于是,语法糖到底是不是需要被删减的对象呢?显然不是。一个好的语言,采用的概念是正交的。尽管正交的概念可以在拼接处我们需要的概念的时候保持可维护性和解耦,但是往往这么做起来却不是那么舒服的,所以需要语法糖。那如果不是语法糖,到底需要删减什么呢?

这一集我们就来讨论面向对象的语言的事情,看看有什么是可以去掉的。

在面向对象刚刚流行起来的时候,大家就在讨论什么搭积木编程啊、is-a、has-a这些概念啊、面向接口编程啊、为什么硬件的互相插就这么容易软件就不行呢,然后就开始搞什么COM啊、SOA啊这些的确让插变得更容易,但是部署起来又很麻烦的东西。到底是什么原因造成OO没有想象中那么好用呢?

之所以会想起这个问题,其实是因为最近在我们研究院的工位上出现了一个相机的三脚架,这个太三脚架用来固定一个手机干点邪恶的事情,于是大家就围绕这个事情展开了讨论——譬如说为什么手机和三脚架是正交的,中间只要一个前凸后凹的用来插的小铁块就可以搞定,而软件就不行呢?

于是我就在想,这不就是跟所谓的面向接口编程一样,只要你全部东西都用接口,那软件组合起来就很简单了吗。这样就算刚好对不上,只要写个adaptor,就可以搞定了。其实这种做法我们现在还是很常见的。举个例子,有些时候我们需要Visual C++ 2013这款全球最碉堡的C++ IDE来开发世界上最好的复杂的软件,不过自带的那个cl.exe实在是算不上最好的。那怎么办,为了用一款更好的编译器,放弃这个IDE吗?显然不是。正确的解决方法是,买intel的icc,然后换掉cl.exe,然后一切照旧。

其实那个面向接口编程就有点这个意思。有些时候一个系统大部分是你所需要的,别人又不能满足,但是刚好这个系统的一个重要部分你手上又有更好的零件可以代替。那你是选择更好的零件,还是选择大部分你需要的周边工具呢?为什么就非得二选一呢?如果大家都是面向接口编程,那你只需要跟cl.exe换成icc一样,写个adaptor就可以上了。

好了,那接口是什么?其实这并没有什么深奥的理解,接口指的就是java和C#里面的那个interface,是很直白的。不知道为什么后来传着传着这条建议就跟一些封装偶合在一起,然后各种非面向对象语言就把自己的某些部分曲解为interface,成功地把“面向接口编程”变成了一句废话。

不过在说interface之前,有一个更简单但是可以类比的例子,就是函数和lambda expression了。如果一个语言同时存在函数和lambda expression,那么其实有一个是多余的——也就是函数了。一个函数总是可以被定义为初始化的时候给了一个lambda expression的只读变量。这里并不存在什么性能问题,因为这种典型的写法,编译器往往可以识别出来,最终把它优化成一个函数。当我们把一个函数名字当成表达式用,获得一个函数指针的时候,其实这个类型跟lambda expression并没有任何区别。一个函数就只有这两种用法,因此实际上把函数去掉,留下lambda expression,整个语言根本没有发生变化。于是函数在这种情况下就属于可以删减的功能

那class和interface呢?跟上面的讨论类似,我主张class也是属于可以删减的功能之一,而且删减了的话,程序员会因为人类的本性而写出更好的代码。把class删掉其实并没有什么区别,我能想到的唯一的区别也就是class本身从此再也不是一个类型,而是一个函数了。这有关系吗?完全没有,你用interface就行了。

class和interface的典型区别就是,interface所有的函数都是virtual的,而且没有局部变量。class并不是所有的函数都是virtual的——java的函数默认virtual但是可以改,C++和C#则默认不virtual但是可以改。就算你把所有的class的函数都改成virtual,那你也会因此留下一些状态变量。这有什么问题呢?假设C++编译器是一个接口,而Visual C++和周边的工具则是依赖于这个class所创造出来的东西。如果你想把cl.exe替换成icc,实际上只要new一个新的icc就可以了。而如果C++编译器是一个class的话,你就不能替换了——就算class所有的函数都是virtual的,你也不可能给出一个规格相同而实现不同的icc——因为你已经被class所声明的构造函数、析构函数以及写好的一些状态变量(成员变量)所绑架了

那我们可以想到的一个迫使大家都写出倾向于比以前更可以组合的程序,要怎么改造语言才可以呢?其实很简单,只需要不把class的名字看成一个类型,而把他看成一个函数就可以了。class本身有多个构造函数,其实也就是这个意思。这样的话,所有原本要用到class的东西,我们就会去定义一个接口了。而且这个接口往往会是最小化的,因为完全没有必要去声明一些用不到的函数。

于是跟去掉函数而留下匿名函数(也就是lambda expression)类似,我们也可以去掉class而留下匿名class的。Java有匿名class,所以我们完全不会感到这个概念有多么的陌生。于是我们可以来检查一下,这样会不会让我们丧失什么表达方法。

首先,是关于类的继承。我们有四种方法来使用类的继承。

1、类似于C#的Control继承出Button。这完全是接口规格的继承。我们继承出一个Button,不是为了让他去实现一个Control,而是因为Button比Control多出了一些新东西,而且直接体现在成员函数上面。因此在这个框架下,我们需要做的是IControl继承出IButton

2、类似于C#的TextReader继承出StreamReader。StreamReader并不是为了给TextReader添加新功能,而是为了给TextReader指定一个来源——Stream。因此这更类似于接口和实现的区别。因此在这个框架下,我们需要的是用CreateStreamReader函数来创建一个ITextReader

3、类似于C#的XmlNode继承出XmlElement。这纯粹是数据的继承关系。我们之所以这么做,不是因为class的用法是设计来这么用的,而是因为C++、Java或者C#并没有别的办法可以让我们来表达这些东西。在C里面我们可以用一个union加上一个enum来做,而且大家基本上都会这么做,所以我们可以看到这实际上是为了拿到一个tag来让我们知道如何解释那篇内存。但是C语言的这种做法只有大脑永远保持清醒的人可以使用,而且我们可以看到在函数式语言里面,Haskell、F#和Scala都有自己的一种独有的强类型的union。因此在这个框架下,我们需要做的是让struct可以继承,并且提供一个Nullable<T>(C#也可以写成T?)的类型——等价于指向struct的引用——来让我们表达“这里是一个关于数据的union:XmlNode,他只可能是XmlElement、XmlText、XmlCData等有限几种可能”。这完全不关class的事情。

4、在Base里面留几个纯虚函数,让Derived继承自Base并且填补他们充当回调使用——卧槽都知道是回调了为什么还要用class?设计模式帮我们准备好了Template Method Pattern,我们完全可以把这几个回调写在一个interface里面,让Base的构造函数接受这个interface,效果完全没有区别。

因此我们可以看到,干掉class留下匿名class,根本不会对语言的表达能力产生影响。而且这让我们可以把所有需要的依赖都从class转成interface。interface是很好adapt的。还是用Visual C++来举例子。我们知道cl.exe和icc都可以装,那gcc呢?cl.exe和icc是兼容的,而gcc完全是另一套。我们只需要简单地adapt一下(尽管有可能不那么简单,但总比完全不能做强多了),就可以让VC++使用gcc了。class和interface的关系也是类似的。如果class A依赖于class B,那这个依赖是绑死的。尽管class A我们很欣赏,但是由于class B实现得太傻比从而导致我们必须放弃class A这种事情简直是不能接受的。如果class A依赖于interface IB,就算他的缺省实现CreateB()函数我们不喜欢,我们可以自己实现一个CreateMyB(),从而吧我们自己的IB实现给class A,这样我们又可以提供更好的B的同时不需要放弃我们很需要的A了。

不过其实每次CreateA(CreateMyB())这种事情来得到一个IA的实现也是很蠢得,优点化神奇为腐朽的意思。不过这里就是IoC——Inverse of Control出场的地方了。这完全是另一个话题,而且Java和C#的一些类库(包括我的GacUI)已经深入的研究了IoC、正确使用了它并且发挥得淋漓尽致。这就是另一个话题了。如何用好interface,跟class是否必须是类型,没什么关系。

但是这样做还有一个小问题。假设我们在写一个UI库,定义了IControl并且有一个函数返回了一个IControl的实现,那我们在开发IButton和他的实现的时候,要如何利用IControl的实现呢?本质上来说,其实我们只需要创造一个IControl的实现x,然后把IButton里面所有原本属于IControl的函数都重定向到这个x上面去,就等价于继承了。不过这个写起来就很痛苦了,因此我们需要一个语法糖来解决它,传说中的Mixin就可以上场了。不知道Mixin?这种东西跟prototype很接近但是实际上他不是prototype,所以类似的想法经常在javascript和ruby等动态语言里面出现。相信大家也不会陌生。

上面基本上论证了把class换成匿名class的可能性(完全可能),和他对语言表达能力的影响(毫无影响),以及他对系统设计的好处(更容易通过人类的人性的弱点来引导我们写出比现在更加容易解耦的系统)。尽管这不是银弹,但显然比现在的做法要强多了。最重要的是,因为class不是一个类型,所以你没办法从IX强转成XImpl了,于是我们只能够设计出不需要知道到底谁实现了IX的算法,可靠性迅速提高。如果IY继承自IX的话,那IX可以强转成IY就类似于COM的QueryInterface一样,从“查看到底是谁实现的”升华到了“查看这个IX是否具有IY所描述的功能”,不仅B格提高了,而且会让你整个软件的质量都得到提高。

因此把class换成匿名class,让原本正确使用OO的人更容易避免无意识的偷懒,让原本不能正确使用OO的人迅速掌握如何正确使用OO,封死了一大堆因为偷懒而破坏质量的后门,具有相当的社会意义(哈哈哈哈哈哈哈哈)。

我之所以写这篇文章是为了告诉大家,通过删减语言的功能来让语言变得更好完全是可能的。但这并不意味着你能通过你自己的口味、偷懒的习惯、B格、因为智商低而学不会等各种奇怪的理由来衡量一个语言的功能是否应该被删除。只有冗余的东西在他带来危害的时候,我们应该果断删除它(譬如在有interface前提下的class)。而且通常我们为了避免正交的概念所本质上所不可避免的增加理解难度所带来的问题,我们还需要相应的往语言里面加入语法糖或者新的结构(匿名class、强类型union等)。让语言变得更简单从来不是我们的目标,让语言变得更好用才是。而且一个语言不容易学会的话,我们有各种方法可以解决——譬如说增加常见情况下可以解决问题的语法糖、免费分享知识、通过努力提高自己的智商(虽然有一部分人会因此感到绝望不过反正社会上有那么多职业何必非得跟死程死磕)等等有效做法。

于是在我自己设计的脚本里面,我打算全面实践这个想法。

posted on 2013-10-19 05:51 陈梓瀚(vczh) 阅读(9394) 评论(17)  编辑 收藏 引用 所属分类: 启示

评论:
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能[未登录] 2013-10-19 06:00 | me
不是一般的赞!  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-19 06:29 | DiryBoy
沙发  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-19 08:47 | whl
开头一段里提到的正交是什么意思?能不能给个示例说明?  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-19 08:52 | whl
就是可组合在一起的意思?不知道有没有理解错。  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-19 09:11 | 陈梓瀚(vczh)
@whl
就是互相之间完全没有重叠,譬如说dynamic dispatch和concept mapping  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-19 20:23 | crabshai
看完我有个疑问,接口里是不能有变量,那么想在接口里定义一个事件,在C++里该怎么做比较好?声明一个方法来获取事件对象么?  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能[未登录] 2013-10-19 21:09 | 陈梓瀚(vczh)
@crabshai
纯虚类,而且实现必须使用虚拟继承。事件你看我的gacui就知道那是纯虚类里的一个变量,不是用来覆盖的函数。我觉得这样做最好。  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-19 23:14 | FTS
>在C里面我们可以用一个union加上一个enum来做,而且大家基本上都会这么做,>所以我们可以看到这实际上是为了拿到一个tag来让我们知道如何解释那篇内存。>但是C语言的这种做法只有大脑永远保持清醒的人可以使用,而且我们可以看到在函>数式语言里面,Haskell、F#和Scala都有自己的一种独有的强类型的union。


=, = 楼主,你明显提一下Algebraic data type...给点keyword不行么  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能[未登录] 2013-10-19 23:47 | 陈梓瀚(vczh)
@FTS
我不是很喜欢这些术语啊哈哈哈  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-22 18:13 | haskell
4、在Base里面留几个纯虚函数...
这点不太懂,如果有多个Derived怎么办呢?  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-22 18:22 | haskell
是不是使用接口IBase,IDerived1,IDerived2...,在IBase中提供一个以IDerivedx为参数的create函数,然后再调用IBase的各个功能  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-22 19:29 | haskell
看了一上午,太多不懂,总想着C++怎么实现,通常用抽象类实现接口,这两者不是一码事,lz说的是广义上的接口?用无状态类实现?如IControl就是一个抽象接口,IButton是一个具体接口,就是在IButton中实现所有功能。ITextReader是一个泛型接口,类似STL的容器。  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-24 22:57 | 陈梓瀚(vczh)
@haskell
只要这些多个base之间不覆盖相同的函数那还是没问题的。如果覆盖了相同的函数,因为这里的接口的语义在C++里面是虚拟继承的,所以这种情况在C++里面你会发现他要求你【分别选中每一个函数到底要继承谁的实现】

如果是多个derived的话,那每个derived都是个函数,还是一样的道理  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-10-24 22:58 | 陈梓瀚(vczh)
@haskell
接口上面没有状态,但是接口的实现有时候是免不了有状态的,所以其实跟状态没什么关系。只是接口很好地避免你必须依赖于【一组固定形式的状态】来继承。  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-11-10 21:20 | 幻の上帝
@陈梓瀚(vczh)
正交比“互相没有重叠”更严格。
正交隐含的多出来的一点是自由组合后没有冗余。形式一点的说法是两者作为操作数笛卡儿×一下……出来的结果可以两两都不等价。这里等价的选取看需求。


继承?能吃么?
interface真吊死在面向对象上了?图样图森破。concept就不能算interface?


  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-11-11 03:57 | 陈梓瀚(vczh)
@幻の上帝
不能,因为interface是类型,concept不是。  回复  更多评论
  
# re: 如何设计一门语言(十一)&mdash;&mdash;删减语言的功能 2013-12-13 00:32 | Klyress
@whl
@陈梓瀚(vczh)
我来给lz补充一下他的程序语言空间理论(逃

类比于线性赋范空间中的正交概念,语言中的正交也应该放到整个语言构成的功能集合中。也就是说语言的"正交"必须满足这些要求:
1.语言所提供的所有的"功能"中必须可以抽取出有限的n个功能,他们两两正交。
2.语言所提供的所有"功能"必须可以由有限个正交的功能加以组合来表达。
3.1中抽取出来的n个功能,每个都不可能由其他的功能所表达。
比如赋值操作和判断操作是正交的,你不可能通过光赋值就写出一个判断来。
然后比如if, goto和赋值就能组成一个图灵完备的语言空间。所以if, goto和赋值可以看作是图灵完备空间的正交基(...)。然后如果你有cmp和jmp,那么if可以由这两者实现,那么com, jmp和mov就构成了一套新的正交基(...)。

希望没让你更糊涂...  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理