FreedomCY 2013-04-03
本文是ThoughtWorks公司架构师Neal Ford在IBM developerWorks系列文章java.next中的第一篇,其基于Groovy,Scala和Clojure,讲述了多语言编程的重要性,并对静态类型与动态类型,函数式编程与命令式编程进行了比较。
在我与Martin Fowler曾经合作呈现的一次主题演讲中,他作出了一个有洞察性的观点:
Java的遗产将是平台,而不是程序设计语言。
Java技术的原始工程师们作出了一个明智的决定,就是将编程语言与运行时环境分开,最终这使得超过200种语言能够运行在Java平台上。这种架构对于该平台的长期活力是至关重要的,因为计算机程序设计语言的寿命一般都是比较短。从2008年开始,由Oracle主办一年一度的JVM语言峰会为JVM上其它的语言实现与Java平台工程师进行开放式合作提供了机遇。
欢迎来到Java.next专栏系列,在本系列的文章中,我将讲述三种现代JVM语言--Groovy,Scala和Clojure--它提供了范式,设计选择与舒适因子之间一种有趣的混合。在此我不会花时间去深入介绍每种语言;在它们各自的站点上都有这类深度介绍。但这些语言社区的站点--它们主要目的是为了传布这些语言--都缺乏客观的信息,或者是该语言不适用的例子。在本系列的文章中我将进行独立地比较,以填补上述空白。这些文章将概述Java.next程序设计语言,以及学习它们的好处。
超越Java
Java程序设计语言达到卓越的程度就是,按Bruce Tate在他的Beyond Java一书中的说法,完美风暴:Web应用的兴起,已有Web技术由于种种原因不能适应需求,企业级多层应用开发的兴起,这些因素共同造就了Java的卓越。Tate也指出这场风暴是一系列独一无二的事件,曾经没有其它语言使用相同的途径达到相同的卓越程序。
Java语言已经证明其在功能方面的强大灵活性,但它的语法与固有范式则存在着长期已知的局限性。尽管一些承诺过的变化即将引入到该语言中,但Java语法却不能很容易地支持一些重要的未来语法特性,例如函数式编程中的某些特性。但如果你试图去找到一种语言去替代Java,那么你就找错了。
多语言编程
多语言编程--在2006年的一篇博客中我使这个术语重焕活力并重新流行起来--是基于这样的一种认识:没有一种编程语言能够解决每个问题。有些语言拥有某些内建的特性,使其能够更好地适应特定的问题。例如,由于Swing十分复杂,开发者们发现很难编写Java中的Swing UI,因为它要求事先声明类型,为UI动作定义烦人的匿名内部类,还有其它的麻烦事儿。使用更适合构建UI的语言,如Groovy中的SwingBuilder工具,去构建Swing应用会美妙得多。
运行在JVM上的程序设计语言大量增多,这大大激发了多语言编程理念,因为你可以混用编号语言,并可使用最佳匹配的语言,但同时却维护着相同的底层字节码和类库。例如,SwingBuilder并不是要替代Swing;它只是搭建在已有的Swing API之上。当然,在相当长的时间内,开发者们还是将在JVM之外混合使用编程语言--例如,为特定目的而使用SQL和JavaScript--但在JVM的世界内,混合编程将变得更为流行。ThoughtWorks中的许多项目就合用着多种编程语言,而所有由ThoughtWorks Studios开发的工具则都要使用混合语言。
即便Java仍是你主要的开发语言,学习一下其它语言是如何工作的会让你将它们纳入你的未来战略中。Java仍将是JVM生态系统中的重要组成部分,但最终它更多是作为该平台的汇编语言--或是由于纯粹的性能原因,或是在应对特殊需求时才会用到它。
编程语言的进化
当上世纪八十年代我还在大学时,我们使用着一种称作Pecan Pascal的开发环境。它独一无二的特性就是能使相同的Pascal代码既可运行在Apple II上,又可以运行在IBM PC上。Pecan的工程师们为了实现这一目的使用了一种称作"字节码"的神秘之物。开发者们将他们的Pascal代码编译成"字节码",该"字节码"则运行在为各个平台编写的原生"虚拟机"上。那是一段可怕的经历!最终程序慢的出奇,即便只是一个简单的类赋值。当时的硬件无法应对这一挑战。
Pecan Pascal之后的十年,Sun发布了使用相同架构的Java,它受限也受利于上世纪九十年代的硬件环境。Java还加入了其它的对开发者友好的特性,如自动的垃圾收集。由于曾经使用过像C++之样的语言,现在我再也不想使用没有垃圾收集功能的语言去编码了。我宁愿花时间在更高抽象层次上去思考复杂的业务问题,而不是像内存管理这样的复杂管道问题。
计算机语言通常没有很长寿命的原因之一就是语言和平台设计的创新速度。由于我们的平台变得更为强大,它们可以处理更多的额外工作。例如,Groovy的内存化(memoization)特性(2010年加入)会缓存函数调用的结果。不需要手工编写缓存代码,那样会引入潜在的缺陷,你仅仅只是需要调用memoize方法而以,如清单1所示:
清单1. 在Groovy中内存化函数
def static sum = { number -> factorsOf(number).inject(0, {i, j -> i + j}) } def static sumOfFactors = sum.memoize()
在清单1中,sumOfFactors方法返回的结果会被自动缓存。你还可以使用方法memoizeAtLeast()和memoizeAtMost()去定制缓存行为。Clojure也含有内存化特性,在Scala中也有略有实现。像内存化这样存在于下一代编程语言(以及某些Java框架)中的高级特性也将逐渐地进入到Java语言中。Java的下一个版本中将加入高阶函数(higher-order function),这使得内存化更容易被实现。通过研究下一代Java语言,你就可以先睹Java的未来特性为快了。
Groovy,Scala和Clojure
Groovy是二十一世纪的Java语法--浓缩咖啡取代了传统咖啡。Groovy的设计目标是更新并消除Java语法中的障碍,同时还要支持Java语言中的主要编程范式。因此,Groovy要"知晓"诸如JavaBean,它会简化对属性的访问。Groovy会以很快的速度纳入新特性,包括函数式编程中的重要特性,这些特性我将在本系列的后续篇章中着重描述。Groovy仍然主要是面向对象的命令式语言。Groovy区别于Java的两个基本不同点:它是动态而非静态的;它是的元编程能力要好得多。
Scala从骨子里就是为了利用JVM而进行设计的,但是它的语法则是完全被重新设计过了。Scala是强静态类型语言--它的类型要求比Java还严格,但造成的麻烦却很少--它支持面向对象和函数式范式,但更偏好于后者。例如,Scala更喜欢val声明,这会生成不可变变量(类似于在Java中将变量声明为final)赋给var,而var将创建更为大家所熟悉的可变变量。通过对这两种范式的深度支持,Scala为你可能想要的(面向对象的命令式编程)与你所应该想要的(函数式编程)之间架起了一座桥梁。
Clojure是最激进的,它的语法是从其它语言中分离出来,被认为是Lisp的方言。Clojure是强动态类型语言(就像Groovy),它反映了一种义无反顾的设计决策。虽然Clojure允许你与遗留的Java程序进行全面而深度的交互,但是它并不试图构建一座桥梁去连接面向对象范式。例如,Clojure是函数式编程的铁杆,也支持面向对象以允许与该种范式进行互操作。尽管它支持面对对象程序员所习惯的全部特性,如多态--但,是以函数式风格,而非面向对象风格进行实现的。设计Clojure时遵循了一组核心的工程原则,如软件事务内存(Software Transactional Memory),这是为了迎合新功能而打破了旧有的编程范式。
编程范式
除语法之外,这些语言之间的最有趣的不同之处就是类型及其内在的编程范式:函数式或命令式。
静态类型 vs. 动态类型
编程语言中的静态类型要求显式的类型声明,例如Java中的int x;声明语句。动态类型语言并不要求在声明时提供类型信息。此处所考虑的语言都是强类型语言,意即程序在赋值之后能够反射出类型。
Java的类型系统广受诟病之处就是其静态类型有太多不便,且又没有提供足够的益处。例如,在当前的有限的类型推导出现之前,Java要求开发者在赋值语句两边要重复地声明类型。Scala的类型比Java的更为静态,但在日常使用中所遇到的不便要少得多,因为它大量使用了类型推导。
初看Groovy,它似乎有一种行为能够衔接静态与动态之间的隔阂。考虑如清单2所示的简单对象集合工厂:
清单2. Groovy集合工厂
class CollectionFactory { def List getCollection(description) { if (description == "Array-like") new ArrayList() else if (description == "Stack-like") new Stack() } }
清单2中的类表现为一个工厂类,基于传入的description参数,该工厂返回List接口的两种实现--ArrayList或Stack--之一。对于Java开发者,上述代码确保了返回值能够符合约定。然后,清单3中的两个单元测试揭示了一种复杂性:
清单3. Groovy中的集合类型测试
@Test void test_search() { List l = f.getCollection("Stack-like") assertTrue l instanceof java.util.Stack l.push("foo") assertThat l.size(), is(1) def r = l.search("foo") } @Test(expected=groovy.lang.MissingMethodException.class) void verify_that_typing_does_not_help() { List l = f.getCollection("Array-like") assertTrue l instanceof java.util.ArrayList l.add("foo") assertThat l.size(), is(1) def r = l.search("foo") }
在清单3中的第一个单元测试中,使用前述的工厂类获得一个Stack对象,并验证它是否确实是Stack对象,然后再执行栈操作,例如push(),size()和search()。然而,在第二个单元测试中,我必须声明一个期望的异常MissingMethodException才能确保该测试能够通过。当我获取一个Array-like的集合,并将它赋给List类型的变量时,我能够验证返回的类型确为一个List对象。但是,当我试图调用search()方法时将触发异常,因为ArrayList并不包含search()方法。因此,这种声明无法在编译时确保方法的调用是正确的。
虽然这看起来像是一个缺陷,但这种行为却是恰当的。Groovy中的类型只是确保赋值语句的有效性。例如,在清单3中,如果返回的实例未实现List接口,将会触发一个运行时异常GroovyCastException。鉴于此,可以肯定Groovy能够与Clojure同跻身于强动态类型语言家族。
然而,Groovy最新的一些变化使得它的静态与动态之间的隔阂变得扫地清。Groovy 2.0加入了注解@TypeChecked,该注解可使你特别地对类或方法决定进行严格的类型检查。清单4例证该注解的使用:
清单4. 使用注解的类型检查
@TypeChecked @Test void type_checking() { def f = new CollectionFactory() List l = f.getCollection("Stack-like") l.add("foo") def r = l.pop() assertEquals r, "foo" }
在清单4中,我加入了注解@TypeChecked,它同时对赋值及随后的方法调用进行了验证。例如,清单5中的代码将不能通过编译:
清单5. 防止无效方法调用的类型检查
@TypeChecked @Test void invalid_type() { def f = new CollectionFactory() Stack s = (Stack) f.getCollection("Stack-like") s.add("foo") def result = s.search("foo") }
在清单5中,我必须对集合工厂返回的对象进行强制类型转换,这样才能允许我调用Stack类中的search()方法。但这种方式会产生一些局限性:当使类型静态化之后,Groovy的很多动态特性将无法工作。然而,上述救命证明了Groovy将继续进行改进,以弥合静态性与动态性之间的分歧。
所有这些语言都有十分强大的元编程功能,所以更为严苛的类型化可以在事后再添加进来。例如,已有多个分支项目将选择性类型(selective type)引入到Clojure中。但一般认为选择性类型是可选的,它不是类型系统的一部分;它只是一个类型验证系统。
命令式 vs. 函数式
另一个主要的比较维度就是命令式与函数式。命令式编程注重于单步执行的结构,在许多情况下,它是模仿了早期底层硬件的有益结构。函数式编程则注重将函数作为第一等的结构体,以试图将状态传递与可变性降低到最小。
Groovy在很大程度上是受Java的启发,它在根本上仍然是命令式语言。但从一开始,Groovy就加入了许多函数式命令的特性,并且以后还会加入更多的此类特性。
Scala则弥合了这两种编程范式,它同时支持这两种范式。在更偏向(也更鼓励)函数式编程的同时,Scala依然支持面向对象和命令式编程。因此,为了恰当地使用Scala,就要求团队要受到良好的培训,以确保你不会混用和随意地选择编程范式,在多范式编程语言中,这一直都是一个危险。
Clojure是铁杆的函数式编程语言。它也支持面向对象特性,使得它能够很容易地与其它JVM语言进行交互,它并不试图去弥合这两种范式之间的隔阂。相反,Clojure这种义无反顾的决策使它的设计者所考虑的语句成为很好的工程学实践。这些决策具有深远的影响,它使Clojure能够以开创性的方法去解决Java世界中一些挥之不去的问题(如并发)。
在学习这些新语言时所要求的许多思想上的转变就是源自于命令式与函数式之间的巨大差别,而这也正是本系列文章所要探索的最有价值的领域之一。
结论
开发者们正生活在一个多语言编程快速发展的世界中,在这种环境中,要求使用多种不同的语言去解决问题。学习高效地利用新语言可以帮助你决定哪种方法是合适的。即便你无法离开Java,它也会逐步地将下一代JVM语言中的特性纳入到Java中;现在看看这些新特性,就会使你在潜移默化之中掌握到未来的Java语言。