编程范型:工具的选择

2022-07-19 13:44:03 浏览数 (1)

这是我写的关于编程范型的文章中最后一篇。

在 《编程的未来》里面提到过,很多时候脑子里的算法还是不容易转变成代码,大部分情况下这不是你编码技巧的问题,而是编程语言的问题,或者更严格地说,是编程语言选择的问题。除了复杂性这个软件唯一的敌人,其它真正的困难,早就被数学家们解决了,如果问题和它的解决能够用数学轻松地表述出来,那计算机只是工具而已。极端地说,如果有合适的工具,那么就选择一个;如果没有,那么可以创造一个。仅此而已。

工程师的乐趣,大抵在解决实际问题上,既有解决问题的成就感,也有解决问题的过程。而为了解决问题,又需要分析问题,选择合适的工具,再来使用工具解决问题这几部分。我们对于各种设计模式和框架结构无比熟悉,却往往忘记了编程语言这个基石一般的工具。看看有多少人成天鼓吹 “语言不重要,重要的是设计,重要的是思维”,可问题的悖论在于,当没有足够的阅历和经验去驾驭各种风格的编程语言,设计也好,思维也好,只能局限于很小的一个圈子里面。

编程范型是一种很常见但是很有趣的给工具分类的维度,如果你不熟悉它,我曾经写过一篇文章简单介绍过,请先阅读。仅仅了解它对于工程师来说,是远远不够的,我们需要使用、总结,并且感受,在选择合适的编程语言以后,问题的解决一下子变得豁然开朗。

像使用 Groovy 一样使用 Groovy

这个小小的例子其实来源于我自己,在我最初学习 Groovy 的时候,并没有真正理解它。没有理解就意味着能够写出 “正确” 的代码,但是确是 Java 代码。这么说是因为 Groovy 的语法兼容 Java,而 Java 几乎是我学习计算机除了入门的 C 以外,第一次正儿八经系统学习和使用的语言。所以那时候我看问题都明显带着它的影子。

怎么才叫做 “像使用 Groovy 一样使用 Groovy” 呢?08 年的时候,在 InfoQ 的一次关于 Groovy 的交流活动里面,我举了这样一个例子(来自当时写的胶片,年代有些久远了,胶片里面有一些不合适的例子和言论请见谅)——构造一棵 DOM 树:

代码语言:javascript复制
def page = new MarkupBuilder()
page.html {
  head { title 'Hello' } 
  body { 
    a ( href:'http://…' ) { 'this is a link' } 
  }
} 

这是什么写法?很像自己实现的某种 DSL,但是很容易理解对不对。如果你使用 C 或者 Java,要实现类似的效果,需要自己定义好 head、body、a 等等各种各样的方法,而 Groovy 之类的动态语言呢?一个 “method missing” 特性就搞定了。

再举一个例子,这样解析 XML 文件,取某个节点值的例子:

代码语言:javascript复制
<freeSpace>
	<total>
	    <rank level="0">
  		    <maxSize>20000000</maxSize>
	    </rank>
		……
	</total>
</freeSpace>

解析的代码:

代码语言:javascript复制
def static node = new XmlParser().parseText(
	new File('freeSpace.xml').getText()
) 

node.total.rank.findAll{
	it.@level == "0"
}.collect{
	it.maxSize.text().toLong()
}.max()

很直观、很清晰对不对,数组、集合……全部都是对象,全部都可以使用闭包一样的写法来遍历,不需要写 for 或者 while 那样过程式的语句。

动态语言

有一种经典的学习一门新语言的方法是比较法,比如从 C 迁入 JavaScript 的工程师,就会不由自主地比较这两门语言的异同,从而快速掌握语法,和新语言的写法。但是,单纯这样训练出来的工程师,只是能写出符合 JavaScript 语法的 C 语言而已。除了满地跑的全局 function 以外,还可以从命名、代码设计上面找到很多 C 语言的影子。

学习一门新的语言,一定要选择自己不熟悉的编程范型,否则,获得的仅仅是掌握的语法和规范,枯燥而没有乐趣。如果你精通或熟悉 C 、Java、C#这样的编程语言(C 语系,且属静态语言),但是没有接触过像 Groovy、Ruby 和 Python 这样动态语言的话,那动态语言应当是一个非常有趣的领域,足以改变以往的思维方式,开阔眼界。

Lisp 是动态语言的鼻祖,动态语言是运行时能改变程序结构或变量类型的语言,它的两大特点包括:

  1. 运行时改变自身的结构甚至是函数的定义;
  2. 程序和数据形式等价。

稍微解释一下。

第一条,指的是在运行时,而非编译期,对象自身的结构是可以更改的,比如说 user 对象,里面有 name 和 age 两个属性,但是在运行期间,我要增加一个从未有过的属性 “personality”,这在静态语言里是不可能发生的。

第二条,我们都知道传统地说,程序和数据是不同的,在运行时,程序是可以反复执行的,但是却是无法修改的;而数据是可以反复修改的,却是不用来执行的——但是动态语言把这二者的区别彻底打破了,结果就是程序也可以在运行时被修改,而数据也可以取出来当做代码执行。

其实,这两条本质上说的是一回事。

提到动态语言不得不提 “鸭子类型”。鸭子类型(duck type)这个概念指的是一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定。理解它,也可以帮助理解动态语言:

在静态语言中,你需要定义一个 “嘎嘎叫” 的接口,让鸭子类实现自这个接口,然后在逻辑中引用实现自这个接口的对象并且调用这个嘎嘎叫的方法,虽说程序在运行时实现了多态,在编译期间编译器并不知道这个实现自嘎嘎叫接口的对象到底是什么类型的,但是它依然知道这个对象一定是实现了这个接口,一定是一个可以 “嘎嘎叫” 的对象。

但是在动态语言中,你根本不需要定义这样的接口(“不需要”,而不是 “不可以”),只需要在你的逻辑里面调用传入对象的 “嘎嘎叫” 方法就好了,不管这个对象是什么,如果这个对象可以嘎嘎叫,那么程序就顺利执行下去;反之,一个运行时异常会被抛出。

我猜想每一个习惯于静态语言的工程师在尝试使用动态语言来解决实际问题的是时候,都会度过一段挣扎期的,这段时间里,写的代码有可能不伦不类,很不自在。根据烟斗理论,学习一个新东西往往需要退步一段时间。毕竟,改变一个人的思维习惯并不容易。

元编程的进化

元编程(meta programming)的代码,说白了就是产生代码的代码,在运行的时候产生或者修改代码(执行逻辑)。我想经过前面的介绍,你到这里应该轻车熟路了。具备元编程能力的语言被称为是 “自省的”。在静态语言中,比如 Java 当中,它就是通过 “反射” 来实现的——啰嗦而且受限(例如只能调用某个类的方法,但是却不能修改该方法本身的逻辑)。

元编程在动态语言的发展过程中,有了一个化身——对象原型(prototype)。在对象原型介绍以前,有必要先了解一下 “元类”(meta class)的概念。

既然元编程的代码就是代码的代码,那么元类就该是 “类的类”,元类正是对某一个类的 “描述”。在面向对象语言里面,我们经常看到这样的关系:

对象 –> 类 –> 元类

右边都是对左边的描述:类是对对象的描述,而元类(本身也可以是对象,但是是特殊的对象)则是对类的描述。想一想 Java 里面反射的实现,就需要用到对目标类的元类的使用(Class<T>),你可以从元类里面找到目标类所有的方法和属性,即对目标类的所有描述信息。

好,有了这个概念以后,回过头来看对象原型。对象原型是如此强大,以至于面向对象的语言可以没有类!

其实这件事情发生得很自然,既然对象是我最关心的工作的个体,通过元类我可以修改对类的定义,从而作用到对象上面,那么,为什么我还需要中间的 “类” 这一层呢?于是,上面的关联关系演进成了:

对象 –> 原型

想想 JavaScript,还有相对小众、但是我很喜欢的 Io 语言,就是这样设计的。这两者本身都是 “基于原型” 的语言,也就是说,从语言本身定义上是没有 “类” 这个概念了,但是,没有类依然具备完善的对象系统,我们依然可以写出优秀的面向对象程序,而且,原型本身,也是对象。为了帮助那些写惯了类定义的程序员,我们可以 “模拟” 出一套类定义的代码来,作为对一类对象的规约和抽象。

既然它也来自元编程这个老祖宗,那么怎么理解元编程在 “对象原型” 上面的体现呢?在 Groovy 当中可以通过操纵类的 “metaClass” 属性来给这个类的所有对象增添方法;在 JavaScript 当中,则是利用 “prototype” 这个特殊的属性(我就不举例子了,不清楚的话你可以阅读以下 《JavaScript 实现继承的几种方式》,里面有利用原型链来实现的例子,相信你就豁然开朗了)。

最后我要说明的是,对于一些静态语言,虽然没法使用 “对象原型” 这样的好工具来帮助程序员,但是也拐弯抹角地引入一些其他的 workaround 达到同样的效果。AOP(Aspect Oriented Programming)就是一个例子。

我想很多人都知道 AOP 的含义,并且也写过 AOP 的代码,但是我们可以更多地从上面的故事中去类比和理解 AOP 的行为。比如你要统计某个方法的调用次数,如果使用任何一种动态语言,那么很简单,找到原型中的这个方法,使用元编程的方式,动态地在这个方法的前部增加统计方法调用次数的语句。但是如果是静态语言,那事情就变得麻烦一些,因为方法本身是不能够随便改变的(前面已经说了,静态语言中对类本身的修改是 “受限” 的),所以常见的办法包括静态织入和动态代理这两种。这两种方法严格说来都没有真正改变某个方法的行为,但是从实现的效果看起来,确是做到了 “仿佛改变了某个类方法调用的逻辑” 了。这便是元编程在静态语言施拳脚的一个体现。AOP 在网上的原理介绍和例子有很多,在这里就不展开了。

编程范型不仅仅是一个程序员手中工具划分的维度,它是一个非常值得思考和实践的魔杖,强大到足以扩展和改变一个人的思维方式。如果以后能有时间和精力,并且最重要的是要的是如果能具备足够的能力,我还想按照时间的顺序,理一理编程范型演进的过程。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

0 人点赞