Scala编程(第4版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.1 一门按需伸缩的语言

不同大小的程序通常需要不同的编程概念。比如下面这段小程序:

这个程序首先设置好国家和首都之间的一组映射,然后修改映射,添加一个新的绑定 ("Japan"->"Tokyo"),最后将法国(France)的首都(Paris)打印出来。[2]本例中用到的表示法高级、到位并且没有多余的分号或类型标注。的确,这段代码看上去感觉像是基于一种现代的“脚本”语言,比如Perl、Python或Ruby。这些语言的一个共通点,至少就上面的示例而言,是它们各自都在语法层面支持某种“关联映射”(associative map)的结构。

关联映射非常有用,因为它们让程序精简可靠,不过有时你可能不同意这种“一体适用”(one size fits all)的哲学,因为你需要在你的程序中更为精细地控制映射结构的性质。Scala给你这种自由度,因为映射在Scala里并不是语言本身的语法,它们是通过类库实现的一种抽象,可以按需进行扩展和适配。

在上面这段程序中,得到的是默认的Map实现,不过改起来也很容易。比如说,可以指定一个特定的实现,如HashMapTreeMap,也可以通过调用par方法得到一个并行执行操作的ParMap。可以指定映射中的默认值,也可以在创建的映射中重写任何方法。不论是哪一种定制,都可以复用跟示例中一样的易用语法来访问你的映射。

这个示例展示了Scala既能让你方便地编写代码,也提供了灵活度。Scala有一组方便的语法结构帮助你快速上手,以愉悦而精简的方式编程,同时,你也会很放心,你想实现的并不会超出语言能表达的范围。可以随时根据需要裁剪你的程序,因为一切都基于类库模块,任由你选用和定制。

培育新类型

Eric Raymond首先提出了大教堂和市集的隐喻,用来描述软件开发。[3]大教堂指的是那种近乎完美的建筑,修建需要很长的时间,不过一旦建好,就很长时间不做变更。而市集则不同,每天都会有工作于其中的人们不断地对市集进行调整和扩展。在Raymond的著作里,市集用来比喻开源软件开发。Guy Steele在一次以“培育编程语言”为主题的演讲中提到,大教堂和市集的比喻也同样适用于编程语言的设计。[4]在这个意义上,Scala更像是市集而不是大教堂,其主要的设计目标就是让用Scala编程的人们可以对它进行扩展和定制。

举个例子,很多应用程序都需要一种不会溢出(overflow)或者说“从头开始”(wrap-around)的整数。Scala正好就定义了这样一个类型scala.math.BigInt。这里有一个使用该类型的方法,计算传入整数值的阶乘(factorial):[5]

现在如果你调用factorial(30),将得到

BigInt看上去像是内建的,因为可以使用整型字面量,并且对这个类型的值做*和-等操作符运算。但实际上它不过碰巧是Scala标准类库里定义的一个类而已。[6]就算没有提供这个类,Scala程序员也可以直接(比如对java.math.BigInteger做一下包装)实现。实际上,Scala的BigInt就是这么做的。

当然了,也可以直接使用Java的这个类。不过用起来并不会那么舒服,因为尽管Java也允许创建新的类型,这些类型用起来并不会给人原生支持的体验:

BigInt的实现方式很有代表性,实际上还有许多其他数值类的类型(大小数、复数、有理数、置信区间、多项式等)。某些编程语言原生地支持其中的某些数值类型。举例来说,Lisp、Haskell和Python实现了大整数;而Fortran和Python则实现了复数。不过,如果某个语言要同时实现所有这些对数值的抽象,只会让语言的实现变得大到不可控的程度。不仅如此,就算有这样的语言存在,总有某些应用会得益于语言提供的范围之外的类型。因此,试图在语言中提供一切的做法并不实际。Scala允许用户通过定义易于使用的类库来培育和定制,最终的代码让人感觉就像是语言本身支持的那样。

培育新的控制结构

从前一个示例我们可以看到,Scala允许我们添加新的类型,这些类型用起来跟内建的类型一样。像这样的扩展原则也适用于控制结构。Scala提供了一组API实现“基于actor的”并发编程模型Akka,很好地展示了这种扩展性。

随着多核处理器在未来几年的不断普及,要达到可接收的性能指标愈发要求我们在应用程序中更多地探索和发掘并行能力。通常,这意味着重写我们的代码,让计算可以分布在多个并发执行的线程上。不过很不幸,在实践中创建可靠的多线程应用程序非常难。Java的线程模型是围绕着共享内存(shared memory)和锁(locking)机制实现的,这样的模型很难推敲,尤其是在系统变得越来越大,越来越复杂的背景下。我们很难(从分析代码)确保程序没有争用状况(race condition)或死锁(deadlock),这些问题在测试阶段很有可能根本测不出来,但在生产环境却随时可能发生。按理说,更安全的做法是采用消息传递(message passing)的架构,比如Erlang采用的“actor”方式。

Java自带的基于线程的并发类库内容很丰富,Scala程序员当然也可以像其他API那样使用它。不过,Scala也提供了一个额外的类库—Akka,实现了跟Erlang类似的actor模型。

actor是可以在线程机制之上实现的并发抽象。它们通过相互发送消息来通信。一个actor可以执行两类基本操作:发消息和收消息。发的动作用感叹号表示(!),用于向某个actor发送消息。如下这个例子是向名为recipient的actor发送消息:

消息的发送是异步的,也就是说,发送消息的actor可以在发完消息后立即继续下一步操作,而不需要等到发送的消息被接收和处理。每个actor都有一个邮箱mailbox),发到该actor的消息都会在这里排队。actor通过receive代码块来处理发送到邮箱的消息:

这里receive代码块由若干样例(case)组成,每个样例都会用某个消息模式来查询邮箱。邮箱中的第一条消息如果匹配了任何一个样例,该样例就会被选中,对应的动作会被执行。一旦邮箱中的消息被处理完毕,actor便会暂停,等待后续的消息。

举例来说,如下是用Akka实现的一个简单的actor,它可以提供计算校验和(checksum)的服务:

这个actor首先定义了一个名为sum的局部变量,并初始化为0。然后定义了一个receive代码块用于处理消息。如果它接收到一条Data消息,它会将这个Data所包含的byte加到sum中。如果它接收到一条GetChecksum消息,它则从当前的sum计算出校验和,然后将计算结果发送给requesterrequester ! checksumrequester字段是通过GetChecksum消息发送的,通常都是发起请求的那个actor。

我们并不指望你在现在这个阶段能完全理解这个actor示例。在介绍伸缩性的时候举这个例子,重点其实是,receive代码块和信息发送(!)都并非Scala内建的操作指令。尽管receive代码块看上去和执行起来都像是内建的语法结构,它实际上只是定义在Akka类库中的一个方法。同理,尽管“!”看上去像是内建的操作符,它实际上也是Akka类库中定义的一个方法。这两个结构都不是Scala语言本身原生提供的。

这里的receive代码块和发送(!)语法跟Erlang很像,不过在Erlang中,这些结构是内建在语言级的。除此之外,Akka还实现了Erlang其他绝大部分用于并发编程的结构,比如监控和超时。总体来说,actor模型在表达并发和分布式计算方面做得非常好。尽管我们只能通过类库的方式来定义,actor用起来感觉就像是Scala语言的一部分。

通过这个示例我们可以直观地了解如何“培育”Scala让它适用于各类场景,哪怕是并发编程这样(充满挑战)的特定领域。当然了,要做到这一点,我们需要优秀的架构师和程序员,不过关键在于这是可行的:可以用Scala设计和实现各式各样的抽象,应用于完全不同的领域,同时用起来仍像是语言原生支持的一样。