2.1 Scala语言概述
本节首先对计算模型的理论研究历史和编程范式的发展进行了简要的概述,然后介绍了Scala语言的发展背景及基本特性,最后详细介绍了Scala的安装方法和各种运行方式。
2.1.1 计算机的缘起
从20世纪30年代开始,一些数学家开始研究如何设计一台拥有无穷计算能力的超级机器,来帮人类自动完成一些计算问题。
数学家阿隆佐·邱奇(Alonzo Church)提出了“λ演算”的概念,这是一套用于研究函数定义、函数应用和递归的形式系统。λ演算被视为最小的通用程序设计语言,它包括一条变换规则(变量替换)和一个函数定义方式。λ演算的通用性就体现在,任何一个可计算函数都能用这种形式来表达和求值。λ演算强调的是变换规则的运用,而非实现它们的具体机器。可以认为这是一种更接近软件而非硬件的方式。它是一个数理逻辑形式系统,使用变量代入和置换来研究基于函数定义和应用的计算。
英国数学家阿兰·图灵(Alan Turing)采用了完全不同的设计思路,提出了一种全新的抽象计算模型,即将人们使用纸笔进行数学运算的过程进行抽象,由一个虚拟的机器替代人们进行数学运算。所谓的图灵机就是指一个抽象的机器,如图2-1所示,它有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色。有一个机器头在纸带上移来移去。机器头有一组内部状态,还有一些固定的程序。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后结合自己的内部状态查找程序表,根据程序输出信息到纸带方格上,并转换自己的内部状态,然后进行移动。这种理论计算模型后来被称为“图灵机”,它是现代计算机的鼻祖。现有理论已经证明,λ 演算和图灵机的计算能力是等价的。
图2-1 图灵机示意模型
如果说图灵奠定的是计算机的理论基础,那么冯·诺依曼(John Von Neumann)则是将图灵的理论物化成为实际的物理实体,成为了计算机体系结构的奠基者。1945年6月,冯·诺依曼提出了在数字计算机内部的存储器中存放程序的概念,这是所有现代计算机的范式,被称为“冯·诺依曼结构”,按这一结构建造的计算机被称为“存储程序计算机”,又称为“通用计算机”。冯·诺依曼计算机主要由运算器、控制器、存储器和输入输出设备组成,它的特点是,程序以二进制代码的形式存放在存储器中,所有的指令都是由操作码和地址码组成,指令在其存储过程中按照执行的顺序,以运算器和控制器作为计算机结构的中心等。从第一台冯·诺依曼计算机诞生到今天已经过去很多年了,计算机的技术与性能也都发生了巨大的变化,但是,计算机主流体系结构依然是冯·诺依曼结构。
2.1.2 编程范式
编程范式是指计算机编程的基本风格或典范模式。常见的编程范式主要包括命令式编程和函数式编程。面向对象编程就属于命令式编程,比如C++、Java等。
命令式语言是植根于冯·诺依曼体系的,一个命令式程序就是一个冯·诺依曼机的指令序列,给机器提供一条又一条的命令序列让其原封不动地执行。函数式编程,又称泛函编程,它将计算机的计算视为数学上的函数计算,并且避免状态以及可变数据。函数编程语言最重要的基础是λ演算,而且λ演算的函数可以接受函数当作输入和输出,因此,λ演算对函数式编程特别是Lisp语言有着巨大的影响。典型的函数式语言包括Haskell、Erlang和Lisp等。
从理论上说,函数式语言并不是通过冯·诺依曼机运行的,而是通过 λ 演算来运行的,但是,由于现代计算机都是采用冯·诺依曼结构,所以,函数式程序还是会被编译成冯·诺依曼机的指令来执行。
一个很自然的问题是,既然已经有了命令式编程,为什么还需要函数式编程呢?为什么在C++、Java 等命令式编程流行了很多年以后,近些年函数式编程会迅速升温呢?这个问题的答案需要从CPU制造技术的变化说起。从20世纪80年代至今,CPU的制造工艺不断提升,晶体管数量不断增加,运行频率不断提高,如图2-2所示。在30多年里,CPU的处理速度已经从10MHz提高到3.6GHz。但是,CPU的制造工艺不可能无限提升,单个CPU内集成的晶体管数量不可能无限增加,因此,从2005年以来,计算机计算能力的增长已经不依赖CPU主频的增长,而是依赖于CPU核数的增多, CPU开始从单核发展到双核,再到四核甚至更多核数。
对于命令式编程而言,由于涉及多线程之间的状态共享,就需要引入锁机制实现并发控制。而函数式编程则不会在多个线程之间共享状态,不会造成资源争用,也就不需要用锁机制来保护可变状态,自然也就不会出现死锁,这样可以更好地实现并行处理。因此,函数式编程能够更好地利用多个处理器(核)提供的并行处理能力,所以,函数式编程开始受到更多的关注。此外,由于函数式语言是面向数学的抽象,更接近人的语言,而不是机器语言,因此,函数式语言的代码会更加简洁,也更容易被理解。
图2-2 1970~2010年CPU的工艺参数及性能变化
2.1.3 Scala简介
编程语言的流行主要归功于其技术上的优势以及其对某种时代需求的适应性。例如,Java 的流行主要归功于其跨平台特性和互联网应用的广泛需求。从2010年前后开始,随着物联网及大数据等技术的发展,编程语言面对的是高并发性、异构性以及快速开发等应用场景,这些场景使得函数式编程可以大施拳脚。但传统的面向对象编程的统治地位远没有结束,因此,能够将两者结合起来的混合式编程范式是适应当前需求的最好解决方案。Scala语言正是在这一背景下开始流行起来的。
Scala是由瑞士洛桑联邦理工学院(EPFL)的Martin Odersky教授,于2001年基于Funnel的工作开始设计的。Scala是一门类Java的多范式语言,它整合了面向对象编程和函数式编程的最佳特性。具体而言:
Scala运行于Java虚拟机(JVM)之上,并且兼容现有的Java程序,可以与Java类进行互操作,包括调用Java方法、创建Java对象、继承Java类和实现Java接口。
Scala是一门纯粹的面向对象的语言。在Scala语言中,每个值都是对象,每个操作都是方法调用。对象的数据类型以及行为由类和特质描述。类抽象机制的扩展有两种途径,一种途径是子类继承,另一种途径是灵活的混入(Mixin)机制,这两种途径能避免多重继承的诸多问题。
Scala也是一门函数式语言。在Scala语言中,每个函数都是一个对象,并且和其他类型(如整数、字符串等)的值处于同一地位。Scala提供了轻量级的语法用以定义匿名函数,同时支持高阶函数,允许嵌套多层函数,并支持柯里化(Currying)。
Scala 语言的名称来自于 Scalable,意为“可伸展的语言”。Scala 的“可伸展性”归功于其集成了面向对象和函数式语言的优点。Scala的一种常用方式是通过解释器键入单行表达式来即时运行并观察结果,因此,对于某些应用来说,Scala就像是一种脚本语言,它的语法简单,在变量类型自动推断机制下,无需时刻关注变量的类型,但却保留了强制静态类型的诸多优势。同时,Scala编写的程序也可以编译打包发布,其生成的代码与Java是一样的。已经有许多大型的业务系统或框架采用Scala作为首选的开发语言,包括Spark、Twitter和LinkedIn等。
2.1.4 Scala的安装
Scala于2004年1月公开发布1.0版本,目前仍处于快速发展阶段,每隔几个月就有新的版本发布。Spark从2.0版本开始都采用Scala2.11编译,因为本教材使用的Spark版本是2.1.0,其对应的Scala版本是2.11.8,所以,本教材中的Scala选用2017年4月发布的2.11.8版本。
Scala运行在Java虚拟机(JVM)之上,因此只要安装相应的Java虚拟机,所有的操作系统都可以运行Scala程序,包括Window、Linux、Unix、Mac OS等。本教材后续的Spark操作都是在Linux系统下进行的,因此,这里以Linux系统(选用Ubuntu发行版)为例,简要介绍Scala的安装及环境配置。
首先,需要安装Linux系统,具体安装方法请参见教材官网的“实验指南”栏目的“Linux系统的安装”。此外,在安装Scala之前,请确保本机Linux系统中已经安装了Java 7或以上版本的JDK (Scala 2.12需要Java 8支持),具体安装方法请参考教材官网的“实验指南”栏目的“Linux系统中Java的安装”。
然后,下载相应的二进制包完成 Scala 的安装。以2.11.8版本为例,通过官方网站(http://www.scala-lang.org/download/2.11.8.html)下载Linux系统对应的安装压缩包“scala-2.11.8.tgz”,或者也可以直接从教材官网的“下载专区”的“软件”目录中下载“scala-2.11.8.tgz”;在Linux系统中下载和解压缩文件的具体方法,可以参考本教材官网的“实验指南”栏目的“Linux系统中下载安装文件和解压缩方法”。下载后,将“scala-2.11.8.tgz”解压到Linux系统的本地文件夹下,如/usr/local,这时会在该目录下生成一个新的文件夹“scala-2.11.8”,编译器及各种库文件即位于该文件夹下,为以后启动Scala方便,可以把“scala-2.11.8”重命名为“scala”,并建议将其下的“bin”目录添加到PATH环境变量。在终端(即Linux Shell环境)中运行“scala -version”,查看是否正确安装,如果已经正确安装,则会显示Scala编译器的版本信息。
2.1.5 HelloWorld
开始学习Scala的最简单的方法是使用Scala REPL(REPL是Read、Eval、Print、Loop的缩写)。与Python及MATLAB等语言的解释器一样,Scala REPL是一个运行Scala表达式和程序的交互式Shell,在REPL里输入一个表达式,它将计算这个表达式并打印结果值。在Linux系统中打开一个终端(可以使用组合键Ctrl+Alt+T),输入如下命令启动Scala REPL:
$ cd /usr/local/scala #这是Scala的安装目录
$ ./bin/scala
正常启动后,终端将出现“scala>”提示符,此时即可输入Scala表达式,默认情况下一行代表一个表达式或语句,按Enter键后scala即运行该语句或表达式并提示结果,如果一条语句需要占用多行,只需要在一行以一个不能合法作为语句结尾的字符结束(比如句点或未封闭的括号与引号中间的字符),则REPL会自动在下一行以“|”开头,提示用户继续输入。图2-3所示是几个输入的示例,其中,关于函数show()的定义语句占用了多行。
图2-3 在Scala REPL中一条语句占用多行的效果
直接在解释器里编写程序,一次只能运行一行。如果想运行多行,可以用脚本的方式运行,只需要将多行程序保存为文本文件,然后在Linux Shell中用“scala 文件名”这种命令形式来运行即可,或者在Scala解释器终端用“:load 文件名”的形式装载执行。下面请在Linux系统中打开一个终端,在“/usr/local/scala/”目录下创建一个mycode子目录,在mycode目录下,使用文本编辑器(比如vim)创建一个代码文件Test.scala(文本编辑器vim的使用方法可以参见教材官网的“实验指南”栏目的“Linux系统中vim编辑器的安装和使用方法”)。Test.scala的内容如下:
//代码文件为/usr/local/scala/mycode/Test.scala
println("This is the first line")
println("This is the second line")
println("This is the third line")
然后,可以在Scala REPL中执行如下命令运行该代码文件:
scala> :load /usr/local/scala/mycode/Test.scala
Loading /usr/local/scala/mycode/Test.scala…
This is the first line
This is the second line
This is the third line
除了使用Scala解释器运行Scala程序之外,还可以通过编译打包的方式运行Scala程序。
首先,在Linux系统中创建一个代码文件HelloWorld.scala,内容如下:
//代码文件为/usr/local/scala/mycode/HelloWorld.scala
object HelloWorld {
def main(args: Array[String]) {
println("Hello, world!");}
}
使用如下命令对代码文件HelloWorld.scala进行编译:
$ cd /usr/local/scala/mycode
$ scalac HelloWorld.scala
编译成功后,将会生成“HelloWorld$.class”及“HelloWorld.class”两个文件,其文件名即为程序中用object关键字定义的对象名,后一个文件即是可以在Java虚拟机上运行的字节码文件。然后可以使用如下命令运行该程序:
$ scala -classpath .HelloWorld
注意,上面命令中一定要加入“-classpath .”,否则可能会出现“No such file or class on classpath:HelloWorld”错误。可以看出,这与编译和运行Java的“Hello World”程序非常类似,事实上,Scala的编译和执行模型与Java是等效的,上面编译后的字节码文件也可以直接用Java执行,命令如下:
$ java -classpath .:/usr/local/scala/lib/scala-library.jar HelloWorld
其中,scala-library.jar为Scala类库。
关于上面的Scala源文件HelloWorld.scala,这里对照Java做几点简要说明。
(1)HelloWorld是用关键字object定义的单例对象(Singleton Object),它提供了一个main方法用作应用程序的入口点。这里要注意Scala的main方法与Java的main方法之间的区别,Java中是用静态方法(public static void main(String[] args)),而Scala没有提供静态方法,改为使用单例对象方法。每一个独立应用程序都必须有一个定义在单例对象中的main方法。
(2)尽管对象的名字HelloWorld与源文件名称HelloWorld.scala一致,但是对于Scala而言,这并不是必须的。实际上,可以任意命名源文件,比如这里可以把源文件命名为 Test.scala,源文件里面的单例对象名称使用HelloWorld。可以看出,在这个方面,Scala和Java是不同的,按照Java的命名要求,这里的文件名称就只能起名为 HelloWorld.scala,也就是文件名称必须和文件中定义的类(class)名称保持一致。虽然 Scala 没有要求文件名和单例对象名称一致,但是,这里仍然推荐像在Java里那样按照所包含的类名来命名文件,这样程序员可以通过查看文件名的方式方便地找到类。
(3)Scala 是大小写敏感的语言,采用分号“;”分隔语句,但与 Java 不同的是,一行 Scala程序的最后一条语句末尾的分号是可以省略的。在 HelloWorld.scala 中,第三行末尾的分号是可以省略的。