如果任意
Java 下一代语言都适合作为您使用的下一代语言,那么您会如何选择?Neal Ford 调查了会对这个重要决定产生影响的各个因素,从而撰写了
Java 下一代 文章系列。
我是一位失败的诗人。或许每一位小说家都愿意先写诗,发现自己不是这块料之后,再尝试短篇小说,因为短篇小说的要求之高仅次于诗歌的题材。再次失败之后,就只能开始写长篇小说了。
并非所有作家(甚至是伟大作家)都擅长写各种体裁的文章。类似地,程序员使用某些编程语言时要比使用其他语言更自如一些。有些开发人员天生就是
C 程序员,而有些则更钟情于 Lisp,还有一些则特别信赖 Perl。没有任何一种语言能够满足所有开发人员的偏好,这个事实很好地说明了为何会有如此之多的计算机语言存在。Java
下一代的含义是没有哪一种语言能够独领风骚,因为没有任何一种语言能让所有人都感到满意。
Java 语言似乎是一个反例。但 Java 的统治地位源自一些独特的情形,Bruce
Tate 在他的著作 超越 Java 中为这种情形提供了一种极好的描述,将它描述为一场 “完美风暴”。Java
于 20 世纪 90 年代中期问世,问世之初,它在接受程度方面受到了严峻考验。它的速度比当时流行的编译语言要慢。它对内存要求极高(当时内存价格还暂时处在高位)。而且它并不特别适合当时主流的客户端/服务器开发方式。Java
惟一的两点可取之处是它的相对易用(通过像垃圾收集之类的工具实现)和 Applets,这在当时是独一无二的。如果形势保持不变,Java
可能无法生存下来。
但 Java 与当时新的 World Wide Web 属于绝配,尤其在
Servlet API 开始变得流行之后。突然之间,服务器端的开发模型让 Java 的众多劣势不再明显。这些因素(硬件、Web
和范式)结合在一起,就构成了 Tate 所说的完美风暴:开发人员需要新工具来完成 Web 编程,服务器端的
Java 缓解了内存约束,与此同时,一种用于构建健壮 Web 应用程序的简化模型出现了。借助天时地利,以及大型公司
(Sun) 的支持,Java 成为了软件行业中的主导力量。
其他语言不见得也能遇到这一系列的巧合。我们已经进入了一个多计算机语言的时代,这种势头还在继续增长。尝试找出能与
Java 具有相同影响力的下一代语言注定会以失败告终。在调查要采用的下一代 Java 语言时,应当重点关注能与您产生共鸣的方面,而不是追求最大的流行程度。
多范式语言
很多现代语言都支持几种编程范式:面向对象、元编程、函数式、过程等。在 Java
下一代语言中,Groovy 和 Scala 是多范式的。Groovy 是一种面向对象的语言,可以通过库进行功能性扩展。Scala
是一种面向对象与函数式兼而有之的混合语言,强调函数式编程偏好,比如不变性与懒惰。
多范式语言的功能非常强大,支持混合与匹配范式,以便无缝地解决问题。在 Java
8 版本之前,很多开发人员都对 Java 中存在的限制感到恼火。像 Groovy 这样的语言提供了更多工具,包括元编程与函数式构建。
尽管它们功能强大,但多范式语言在大项目上对开发人员的纪律要求更多。因为语言支持众多抽象与理念,独立的开发小组可以在库中创建明显不同的变体。例如,代码重用趋向于面向对象体系中的结构体,而在函数式体系中,则趋向于复合函数与高阶函数。在设计公司的
Customer API 时,您必须确定最佳方式,确保团队中的每个人都同意(和坚持)这一点。很多从 Java
转到 Ruby 的开发人员都遇到了这个问题,因为 Ruby 是一种多范式语言。C++ 是另一种多范式语言,它给很多试图强行(通常情况下为无意)跨越过程与面向对象的项目带来困扰。
解决方案之一是依靠工程规范来确保项目中的所有开发人员都朝着同一目标努力。很多开发人员担心使用元编程会对核心类造成过多修改。例如,有些测试库会在
Object 中添加方法,以便支持范围更广的断言。单元测试支持复杂扩展的精确理解,这减轻了大家对于未知副作用的恐惧。
包括 Clojure 在内的有些语言主要支持一种范式,同时,出于实用目的,它们也支持其他范式,这强加了更多的原则。Clojure
毫无疑问是针对 JVM 的一种函数式 Lisp。您可以从底层平台与类和方法进行交互(如果愿意的话,还可以创建自己的类和方法),但
Clojure 首先要支持的仍然是强函数式的范式,比如不变性与懒惰。
杀手锏:函数式编程
对于大多数开发人员而言,完全支持函数式编程是未来语言最重要的特性。我在几篇系列文章中谈到过
Java 下一代语言关于函数式方面的内容。函数式范式有效性的关键在于能够在一个较高的抽象层面上表达理念。
在 "内存化和函数式协同" 一文中,我将命令式的 indexOfAny()
方法(来自 Apache Commons StringUtils 库)转化成了 Clojure,生成了一个更短、更简单但更加通用的函数。Clojure
的可读性优势仍然很突出,但对于非 Lisp 开发人员,Clojure 看起来有点奇怪。Scala 被设计为对
Java 开发人员最具可读性。同一个 indexOfAny() 方法被转换为 Scala 而非 Clojure,如清单
1 所示。
清单 1. 一种 Scala indexOfAny() 实现
def indexOfAny(input : Seq[Char], searchChars : Seq[Char]) : Option[Int] = { def indexedInput = (0 until input.length).zip(input) val result = for (char <- searchChars; pair <- indexedInput; if (char == pair._2)) yield (pair._1) if (result.isEmpty) None else Some(result.head) } |
indexOfAny 方法的用途是在第一个参数中返回第二个参数中传递的任意字符的索引位置。在
清单 1 中,我首先基于输入字符串的长度构建了一个数字的顺序列表,使用它作为 indexedInput
参数的值。然后,我使用 Scala 中内置的 zip() 函数将两个列表连接在一起。例如,如果我的输入字符串为
zabycdxx,那么 indexedInput 中的结果应该是 Vector((0,z), (1,a),
(2,b), (3,y), (4,c), (5,d), (6,x), (7,x))。
有了 indexedInput 集合后,我使用 for 推导式代替原始版本中的嵌套循环。首先,我通过
searchChars 进行搜索;我检查 indexedInput 第二部分中(使用 Scala 速写形式
pair._2)每个字符是否存在,然后返回与 pair._1 匹配的索引部分。yield() 函数为返回列表生成值。
在 Scala 中,返回 Option 而非可能的 null 十分常见,因此如果没有结果存在我就返回
None,否则就返回 Some。原始的 indexOfAny() 方法只返回首个匹配字符的索引,因此我只返回结果中的第一个元素(result.head)。在
Clojure 版本中,我返回了一个包含所有匹配的列表。将它转换为具有相同功能的 Scala 版本也很容易,如清单
2 所示。
清单 2. indexOfAny 返回所有匹配项
def lazyIndexOfAny(input : Seq[Char], searchChars : Seq[Char]) : Seq[Int] = { def indexedInput = (0 until input.length).zip(input) for (char <- searchChars; pair <- indexedInput; if (char == pair._2)) yield (pair._1) } |
在 清单 2 中,返回的是一个匹配列表而不仅仅是第一个匹配项。例如,lazyIndexOfAny("zzabyycdxx",
"by") 的结果是 Vector(3, 4, 5),它与输入字符串中每个目标字符的索引相匹配。
函数式编程语言让您能够使用更为强大的构造块在更高的抽象层次上进行工作,比如优先于循环的
map()。在不用关心底层的代码细节时,您就可以更加清晰地专注于关联度更高的问题。
函数式金字塔
计算机语言类型的存在一般沿着两条轴线:强对弱和动态对静态,如图 1 中所示。
图 1. 语言分类特征
强类型的变量 “知道” 它们的类型,支持反射与实例检查,而且它们会保持这种认知。弱类型的语言不太了解它们的指向。例如,C
是一种静态的弱类型语言:C 中的变量实际上是一个能够以各种方式解释的字节集合,这让全世界的 C 开发人员又爱又恨。
Java 是静态的强类型语言。在声明变量时,必须指定变量类型,有时候还要反复声明。Java
逐步引入了类型推理,但它在类型的简洁度方面明显不如任意一种 Java 下一代语言。Scala、C# 和
F# 也是静态的强类型语言,但它们使用了类型推理,省去了很多麻烦。很多时候,语言可以通过识别类型来减少冗余。
自从编程语言的早期时代开始,这些区别就一直存在。然而,一个新的因素打破了这种平衡:函数式编程。
正如我在 "函数式编码风格" 一文中所说的那样,函数式编程语言的设计理念与命令式编程语言截然不同。命令式语言尝试让状态变化变得简单,并为此提供了许多功能。函数式语言尝试最大限度地减少可变状态,并构建了更多通用的机制。但函数式
语言没有确定一个类型系统,图 2 中表明了这一点。
图 2. 函数式编程语言
函数式编程语言依赖于(而且有时候强调)不变性。语言之间的关键区别现在不是动态和静态,而是命令式和函数式,后者会让软件的构建方式变得大不相同。
在 2006 年的一篇博客中,我意外地让 多语言编程 这个术语再次流行起来,并给它赋予了新的意义:利用现代运行时来创建对语言而非平台进行混合和匹配的应用程序。这种重新定义基于以下事实:Java
与 .NET 平台支持的语言超过了 200 种,而且增加了没有 “一种真正的语言” 能够解决所有问题的怀疑。借助现代托管的运行时,您可以在字节代码层次上自由地混合和匹配语言,并对特定的任务使用最适合的语言。
当我发表该博客文章后,我同事 Ola Bini 紧接着发表了一篇 后续文章
来讨论他的多语言金字塔。该金字塔如图 3 所示,它揭示了开发人员在多语言环境中构建应用程序的可能方式。
图 3. Bini 的金字塔
在 Bini 的倒金字塔中,他建议在最底层使用更加静态的语言,因为此时可靠性是优先级最高的。接下来,他建议在应用层使用更加动态的语言,使用较简单的语法来构建像用户界面这样的内容。最后,在金字塔的顶端是特定领域的语言
(DSL),开发人员使用它们来简洁地封装重要的领域知识和工作流。通常,DSL 是使用动态语言来实现的,目的是利用它们的某些功能。Bini
的目标是夯实底部,并在接近顶部的地方提供更多的灵活性。
Bini 的金字塔是对我原来博文的精彩补充。但在中间的这些年里,环境发生了变化。现在我相信,在更多的情况下,类型是开发人员的一种偏好,影响他们关注更重要的特性:函数式和命令式。图
4 中显示了我的新函数金字塔。
图 4. 函数金字塔
我们渴求的灵活性并非来自于静态类型,而是来自在底部融入函数式概念。如果所有起关键作用的核心
API(比如数据访问与集成)能够假定不变性,那么所有这些代码就会变得简单的多。当然,无处不在的不变性会改变我们构建数据库和其他基础架构的方式,但结果会获得更好的核心稳定性。
基于函数核心,使用命令式语言处理工作流、业务规则、UI 和系统的其他部分,其中开发人员的易用性处于优先位置。在原来的金字塔中,DSL
位于顶部,为相同的目的而服务。然而,我仍然相信 DSL 将会穿透系统的所有层次,直达底部。您可以在像 Scala
(函数式、静态强类型)和 Clojure(函数式、动态强类型)这样的语言中轻松编写 DSL,以准确的方式捕捉重要理念,这就是对上述观点的一个强有力的证明。
构建符合这种金字塔模型的应用程序代表着一种重大改变,但其中的含义令人神往。要查看可能性,可以了解
Datomic(一种商业产品)的架构。Datomic 是一种函数式数据库,可以精确地记录下每次变化。更新不会破坏数据:它会创建数据库的一个新版本。您可以回滚数据库,查看过去的快照。因为始终能够查看历史,诸如连续软件交付这样的工作(它依赖于将数据库按时间前后滚动的能力)就变得微不足道。测试应用程序的多个版本也变得很轻松,因为您可以直接同步模式与代码改动。Datomic
是使用 Clojure 构建的,并在架构层次上使用了函数式结构。这其中蕴含的含义令人感到吃惊。
结束语
本文是 Java 下一代 文章系列的最后一篇文章。我希望本文章系列激发了您深入研究我在前
15 篇文章中谈及的语言和理念的兴趣。自从我 18 个月前开始撰写此文章系列以来,编程语言的大环境已经发生了改变。Java
8 是 Java 下一代语言中强有力的竞争者之一,它最终添加了在未来数年内将统领语言的函数式编程元素。所有四种
Java 下一代语言(Groovy、Scala、Clojure 和 Java 8)都拥有强大的社区和不断增长的用户群,并不断进行创新。无论您选择哪位竞争者(或它们的组合),JVM
语言的前景看起来都是一片光明。
|