1.2 是什么让Scala能屈能伸
语言的伸缩性取决于很多因素,从语法细节到组件抽象都有。如果我们只能挑一个让Scala能屈能伸的方面,那就是它对面向对象和函数式编程的结合(我们作弊了,面向对象和函数式本质上是两个方面,不过它们确实是相互交织的)。
与其他结合面向对象和函数式编程的语言相比,Scala走得更远。举例来说,其他语言可能会区分对象和函数,将它们定义为不同的两个概念,但在Scala中,函数值就是对象,而函数的类型是可被子类继承的类。你可能会认为这仅仅是在纸面上更好看,但其实这对语言的伸缩性有着深远的影响。本节将概要地介绍Scala是如何做到将面向对象和函数式概念结合在一起的。
Scala是面向对象的
面向对象编程获得的成功是巨大的,从20世纪60年代中期的Simula和70年代的Smalltalk开始,直到现在,成了大多数编程语言都支持的主要特性。在某些领域,对象几乎全面占领了市场。虽然面向对象的含义并没有一个准确的定义,但是很显然,对象这个概念是深受程序员群体欢迎的。
从原理上讲,面向对象编程的动机非常简单:除最微不足道的程序之外,所有程序都需要某种结构,而形成这种结构最直截了当的方式就是将数据和操作放进某种容器里。面向对象编程的伟大概念让这类容器变得完全通用,使得这类容器既可以包含操作,也可以包含数据,并且可以以值的形式被存放在其他容器中,或者作为参数被传递给相关操作。这些容器被称为对象。Smalltalk的发明人——Alan Kay认为,通过这样的抽象,最简单的对象也像完整的计算机一样,有着相同的构造原理:它将数据和操作结合在一个形式化的接口之下。[7]所以说,对象与编程语言的伸缩性之间的关系很大:同样的技巧既适用于小程序也适用于大程序。
虽然面向对象编程已经作为主流存在了很长的时间,但是相对而言很少有编程语言按照Smalltalk的理念,将这个构思原理推到逻辑的终点。举例来说,许多语言都允许不是对象的值的存在,如Java的基本类型,或者允许不以任何对象的成员形式存在的静态字段和方法的存在。这些对面向对象编程理念的背离在一开始看上去没什么不妥,但它们倾向于让事情变得复杂,限制了伸缩的可能性。
Scala则不同,它对面向对象的实现是纯粹的:每个值都是对象,每个操作都是方法调用。举例来说,当你说1 + 2时,实际上是在调用Int类里定义的名称为+的方法。你也可以定义名称像操作符的方法,这样别人就可以用操作符表示法来使用你的API。
与其他语言相比,在组装对象方面,Scala更为高级。Scala的特质(trait)就是一个典型的例子。特质与Java的接口很像,不过特质可以有方法实现甚至是字段。[8]对象通过混入组合(mixin composition)构建,构建的过程是取出某个类的所有成员,然后加上若干特质的成员。这样一来,类的不同维度的功能特性就可以被封装在不同的特质定义中。这粗看上去有点像多重继承(multiple inheritance),细看则并不相同。与类不同,特质能够对某个未知的超类添加新的功能,这使得特质比类更为“可插拔”(pluggable)。尤其是特质成功地避开了多重继承中,当某个子类通过不同的路径继承到同一个超类时产生的“钻石继承”(diamond inheritance)问题。
Scala是函数式的
Scala不只是一门纯粹的面向对象语言,也是功能完整的函数式编程语言。函数式编程的理念,甚至比计算器还要早。这些理念早在20世纪30年代由Alonzo Church开发的lambda演算中得以建立。而第一个函数式编程语言Lisp的历史,可以追溯到20世纪50年代末期。其他函数式编程语言还包括:Scheme、SML、Erlang、Haskell、OCaml、F#等。在很长一段时间里,函数式编程都不是主流:虽然在学术界很受欢迎,但是在工业界并没有广泛使用。不过,最近几年,大家对函数式编程语言和技巧的兴趣与日俱增。
函数式编程以两大核心理念为指导。第一个核心理念是函数是一等的值。在函数式编程语言中,函数值的地位与整数、字符串等是相同的。函数可以作为参数传递给其他函数,作为返回值返回,或者被保存在变量里。还可以在函数中定义另一个函数,就像在函数中定义整数那样。也可以在定义函数时不指定名称,就像整数字面量42,让函数字面量散落在代码中。
作为一等值的函数对操作的抽象和创建新的控制结构提供了便利。这种函数概念的抽象带来了强大的表现力,可以让我们写出精简可靠的代码。这一点对于伸缩性也有很大的帮助。以ScalaTest为例,这个测试类库提供了eventually(最后)这样的结构体,接收一个函数作为入参(argument)。用法如下:
在eventually中的代码——it.next() shouldBe 3这句断言被包含在一个函数里,该函数并不会直接执行,而是会原样传入eventually。在配置好的时间内,eventually将会反复执行这个函数,直到断言成功。
函数式编程的第二个核心理念是程序中的操作应该将输入值映射成输出值,而不是当场(in place)修改数据。为了理解其中的差别,我们不妨设想一下Ruby和Java的字符串实现。在Ruby中,字符串是一个字符型的数组,字符串中的字符可以被单个替换。例如,在同一个字符串对象中,可以将分号替换为句号。而在Java和Scala中,字符串是数学意义上的字符序列。通过s.replace(';', '.')这样的表达式替换字符串中的某个字符,会交出(yield)一个全新的对象,而不是s。换句话说,Java的字符串是不可变的(immutable),而Ruby的字符串是可变的(mutable)。因此仅从字符串的实现来看,Java是函数式的,而Ruby不是。不可变数据结构是函数式编程的基石之一。Scala类库在Java API的基础上定义了更多的不可变数据类型。比如,Scala提供了不可变的列表(list)、元组(tuple)、映射(map)和集(set)等。
函数式编程的这个核心理念的另一种表述是方法不应该有副作用(side effect)。方法只能通过接收入参和返回结果这两种方式与外部环境通信。举例来说,Java的String类的replace方法便符合这个描述:它接收一个字符串(对象本身)、两个字符,交出一个新的字符串,其中所有出现的入参第一个字符都被替换成了入参的第二个字符。调用replace并没有其他的作用。像replace这样的方法被认为是指称透明(referential transparent)的,意思是对于任何给定的输入,该方法调用都可以被其结果替换,同时不会影响程序的语义。
函数式编程鼓励不可变数据结构和指称透明的方法。某些函数式编程语言甚至强制要求这些。Scala给你选择的机会。如果你愿意,则完全可以编写指令式(imperative)风格的代码,也就是用可变数据和副作用编程。不过Scala通常让你可以不必使用指令式的语法结构,因为有其他更好的函数式的替代方案可供选择。