Scala如何改变了我的编程风格:从命令式到函数式
51CTO编辑推荐:Scala编程语言专题
【51CTO快译】编者前言:这篇文章最初写于2008年底,作者Bill Venners一方面是美国著名开发网站Artima的总编,另一方面也是一位十分关注Scala语言的Java程序员。在这几个月间的Scala创始人Martin Odersky访谈系列中,与Martin对话的正是Bill Venners。这篇文章虽然已经完成了半年有余,但对于还不很熟悉Scala语言的Java程序员而言,仍然是一篇非常实用的Scala语言简介。以下是译文:
每次我学习一门新的语言,我都会学到某些编程方面的东西。比如说,当我以一个C++程序员的身份学习Java的时候,Java的接口构造教会我来自纯粹的抽象基类的多重继承的价值。尽管在C++里面这种编程风格是有可能的,但在我使用C++的日子里,我却没有考虑用这种方式进行多重继承,而我在C++设计中也不怎么使用抽象基类。然而,一旦我开始进行Java编程,我就开始一直使用这种风格了。学习Java―尤其是它的接口构造―改变了我OO设计的方法。
51CTO编辑推荐:充分利用面向对象语言的接口特性 | 面向对象的思维过程
我学习Scala编程的时候也发生了类似的情况。在过去的两年里,我有相当多的时间是用Scala工作的,Scala是Java平台上的一种新的静态类型语言,它融合了面向对象编程和函数型程序设计的概念。Scala能让我写出几乎跟Ruby和Python一样简洁的代码。在Scala我可以跟在Java里面一样方便地调用Java库,包括我已有的Java库。考虑到Scala是静态类型的,我可以享受到诸多静态类型的好处,诸如将文档作为类型,IDE代码自动完成,动态代码重构(deterministic refactoring)以及执行速度等(Scala程序的执行速度跟Java的一样快)。但Scala还让我以简洁和类型安全的方式获得某些通常是动态语言的好处,例如在已有类上增加新方法的能力,或者将类型传递给没有共同继承关系的方法。
Scala是怎样改变了我对编程的看法的呢?一句话:我学会了欣赏函数化的风格。函数化的编程风格强调不可变对象、变量可被初始化但不能重新赋值(Java中的最终变量)、数据结构转换,以及方法和控制的构造,最终产生一个没有副作用的结果。这个领域的另一端是命令式的风格,以可变对象、变量可被重新赋值(Java里的正常变量)、在数据结构中索引、以及带副作用的方法和控制构造为特征。
尽管Scala经常被吹捧为函数型编程语言,当它并不仅仅是函数型的。Scala同时支持函数式和命令式两种风格。如果你自己选择要这么做的话,你可以以Java的编程方式进行Scala编程,那种风格主要是命令式的。这样有助于Scala的学习曲线变缓,但随着对Scala越来越熟悉,你就会发现自己会更喜欢函数式的。我就是这样。为什么?因为我发现函数型风格往往要比命令式风格的代码更简洁,且更不易出错。函数式风格的代码通常层次更高,这使得它编写起来更快,阅读也更为容易。举个例子,看看下面这段确定一个字符串是否包含大写字符的Java代码。
boolean nameHasUpperCase = false; // 这是Java for (int i = 0; i < name.length(); ++i) { if (Character.isUpperCase(name.charAt(i))) { nameHasUpperCase = true; break; } } |
这里的命令式风格是很明显的,因为nameHasUpperCase变量被重新赋值会给loop循环带来副作用,loop是通过字符串中的字母索引进行迭代的。在Java你还可以以更为简洁的方式得到相同的结果,像下面这样:
boolean nameHasUpperCase = !name.toLowerCase().equals(name); |
这一行Java代码展现出一种更为函数化的风格,因为它转换不可变数据:name这个字符串被转换为另外一个全部字母都是小写的字符串,然后值被转换为布尔结果。此外,nameHasUpperCase这个变量被初始化了,但仅限于这一小块代码里,而没有被重新赋值。如果该变量为最终值的话,它的函数化就会更为清晰。
在Scala里面,你可以写出跟以上两个例子类似的代码,不过更为理想的编写方式是像下面这样的:
val nameHasUpperCase = name.exists(_.isUpperCase) |
nameHasUpperCase变量被定义为 val,即可被初始化但不能被重新赋值的变量(类似于Java里面的最终变量)。甚至于尽管本例中并无显式的类型标注,Scala的类型推断机制也会给nameHasUpperCase赋予Boolean类型。exists 方法在对象集合中迭代,并依次将每个元素传递给函数对象。在这里, name字符串被视为字符集合,因此exists会把字符串的每一个字符都传递给该函数。_.isUpperCase的语法是Scala里的一种函数显式声明(function literal),是一种编写少量代码就可以到处传递和调用的速写方式。下划线代表该函数的唯一参数。因此你可以把下划线视为每次该函数被调用时待填的空白。如果exists 方法发现该函数因被传递的字符中的其中一个而返回true―比如说,其中一个字符是大写的―而返回true。否则就返回false。
尽管最后的这个单行代码对于某些不熟悉Scala的人来说像是天书,只要你了解了Scala,你就能一眼看出代码的目的。相反,其他的两个版本却要费上一点功夫去研究一下。另外需要注意的一点不同是命令式例子中潜在的偏移错误,因为你必须显式地指出迭代的上标。在函数化的版本里这种错误不会产生,在这种方式下,函数化版本相对而言不易出错。
最后,我想指出的是我转向Scala的时候并没有“彻底函数化”。尽管我已经发现通常大部分情况下函数化风格的代码来得更为简洁、明晰,更不易出错,我还发现有时候命令式风格也可带来更为清晰和简洁的代码。在那种情况下,我就会使用命令式的。Scala允许我方便地应用函数式和命令式的风格,结合使用此二者,我就能找到写出清晰代码的最佳方式。
函数式编程和命令式编程简介
什么是函数式编程?(参考资料:《征服RIA:基于JavaScript的Web客户端开发》第8章JavaScript函数对象)
在数学领域,函数是一种关系,这种关系使一个集合里的每一个元素对应到另一个集合里的唯一元素。函数是将唯一的输出值赋予每一输入的"法则"。这一"法则"可以用函数表达式、数学关系,或者一个将输入值与输出值对应列出的简单表格来表示。函数最重要的性质是其决定性,即同一输入总是对应同一输出(注意,反之未必成立)。从这种视角,可以将函数看做"机器"或者"黑盒",它将有效的输入值变换为唯一的输出值。通常将输入值称做函数的参数,将输出值称做函数的值。
《Why Functional Programming Matters》的作者John Hughes 说明了模块化是成功编程的关键,而函数编程可以极大地改进模块化。在函数编程中,编程人员有一个天然框架用来开发更精练的、更小的、更简单的和更一般化的模块,然后将它们组合在一起。函数式编程的基本特点是:
丰富的数据类型;
函数是运算元;
在函数内保存数据;
函数内的运算对函数外无副作用。
函数式编程只描述在程序输入上执行的操作,不必使用临时变量保存中间结果。重点是捕捉"是什么以及为什么",而不是"如何做"。与将重点放在执行连续命令上的过程性编程相比,函数式编程的重点是函数的定义而不是状态机(State Machine)的实现。是一种强调表达式的计算而非命令的执行的一种编程风格。表达式是用函数结合基本值构成的,它类似于用参数调用函数(函数式的优美的说明可见《Functional Programming For The Rest of Us》)。
什么是命令式编程?(参考资料:维基百科)
命令式编程,是种描述电脑所需作出的行为的编程典范。几乎所有电脑的硬体工作都是命令式的;几乎所有电脑的硬体都是设计来执行机器码,使用命令式的风格来写的。较高阶的命令式编程语言使用变数和更复杂的语句,但仍依从相同的典范。食谱和行动清单,虽非电脑程式,但与命令式编程有相似的风格:每步都是指令,有形的世界控制情况。因为命令式编程的基础观念,不但概念上比较熟悉,而且较容易具体表现于硬体,所以大部分的编程语言都是命令式的。