在本系列的第一篇文章 《使用递归的方式去思考》中,作者并没有首先介绍 Scala 的语法,这样做有两个原因:一是因为过多的陷入语法的细节当中,会分散读者的注意力,反而忽略了对于基本概念,基本思想的理解;二是因为 Scala 语法非常简洁,拥有其他语言编程经验的程序员很容易读懂 Scala 代码。现在我们将回过头来,从基本的语法开始学习 Scala 语言。大家会发现 Scala 语言异常精炼,实现同样功能的程序,在代码量上,使用 Scala 实现通常比 Java 实现少一半或者更多。短小精悍的代码常常意味着更易懂,更易维护。本文将为大家介绍 Scala 语言的基本语法,帮助大家写出自己的第一个 Scala 程序。
学习 Scala,最方便的方式是安装一个 Scala 的 IDE(集成开发环境),Typesafe 公司开发了一款基于 Eclipse 的 IDE。该款 IDE 为 Scala 初学者提供了一个方便的功能:Worksheet。像 Python 或者 Ruby 提供的 RELP(Read-Eval-Print Loop)一样,Worksheet 允许用户输入 Scala 表达式,保存后立即得到程序运行结果,非常方便用户体验 Scala 语言的各种特性。如何安装 Scala IDE 和使用 Worksheet,请大家参考 https://class.coursera.org/progfun-002/wiki/view?page=ToolsSetup。
让我们以经典的 Hello World 程序开始,只需在 Worksheet 里输入 println("Hello World!")
保存即可,在该语句的右边就可以立刻看到程序执行结果。
Worksheet 也可以被当作一个简单的计算器,试着输入一些算式,保存。
图 1. Worksheet
当然,我们不能仅仅满足使用 Scala 来进行一些算术运算。写稍微复杂一点的程序,我们就需要定义变量和函数。Scala 为定义变量提供了两种语法。使用 val
定义常量,一经定义后,该变量名不能被重新赋值。使用 var
定义变量,可被重新赋值。在 Scala 中,鼓励使用 val
,除非你有明确的需求使用 var
。对于 Java 程序员来说,刚开始可能会觉得有违直觉,但习惯后你会发现,大多数场合下我们都不需要 var
,一个可变的变量。
清单 1. 定义变量
1
2
3
4
5
6
7
8
9
10
|
val
x
=
0
var
y
=
1
y
=
2
// 给常量赋值会出现编译错误
// x = 3
// 显式指定变量类型
val
x
1
:
Int
=
0
var
y
1
:
Int
=
0
|
仔细观察上述代码,我们会有两个发现:
定义变量时没有指定变量类型。这是否意味着 Scala 是和 Python 或者 Ruby 一样的动态类型语言呢?恰恰相反,Scala 是严格意义上的静态类型语言,由于其采用了先进的类型推断(Type Inference)技术,程序员不需要在写程序时显式指定类型,编译器会根据上下文推断出类型信息。比如变量 x
被赋值为 0,0 是一个整型,所以 x
的类型被推断出为整型。当然,Scala 语言也允许显示指定类型,如变量 x1
,y1
的定义。一般情况下,我们应尽量使用 Scala 提供的类型推断系统使代码看上去更加简洁。
另一个发现是程序语句结尾没有分号,这也是 Scala 中约定俗成的编程习惯。大多数情况下分号都是可省的,如果你需要将两条语句写在同一行,则需要用分号分开它们。
函数的定义也非常简单,使用关键字 def
,后跟函数名和参数列表,如果不是递归函数可以选择省略函数返回类型。Scala 还支持定义匿名函数,匿名函数由参数列表,箭头连接符和函数体组成。函数在 Scala 中属于一级对象,它可以作为参数传递给其他函数,可以作为另一个函数的返回值,或者赋给一个变量。在下面的示例代码中,定义的匿名函数被赋给变量 cube
。匿名函数使用起来非常方便,比如 List
对象中的一些方法需要传入一个简单的函数作为参数,我们当然可以定义一个函数,然后再传给 List
对象中的方法,但使用匿名函数,程序看上去更加简洁。
清单 2. 定义函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 定义函数
def
square(x
:
Int)
:
Int
=
x * x
// 如果不是递归函数,函数返回类型可省略
def
sum
_
of
_
square(x
:
Int, y
:
Int)
=
square(x) + square(y)
sum
_
of
_
square(
2
,
3
)
// 定义匿名函数
val
cube
=
(x
:
Int)
=
> x * x *x
cube(
3
)
// 使用匿名函数,返回列表中的正数
List(-
2
, -
1
,
0
,
1
,
2
,
3
).filter(x
=
> x >
0
)
|
让我们再来和 Java 中对应的函数定义语法比较一下。首先,函数体没有像 Java 那样放在 {}
里。Scala 中的一条语句其实是一个表达式,函数的执行过程就是对函数体内的表达式的求值过程,最后一条表达式的值就是函数的返回值。如果函数体只包含一条表达式,则可以省略 {}
。其次,没有显示的 return
语句,最后一条表达式的值会自动返回给函数的调用者。
和 Java 不同,在 Scala 中,函数内部还可以定义其他函数。比如上面的程序中,如果用户只对 sum_of_square 函数感兴趣,则我们可以将 square 函数定义为内部函数,实现细节的隐藏。
清单 3. 定义内部函数
1
2
3
4
5
|
def
sum
_
of
_
square(x
:
Int, y
:
Int)
:
Int
=
{
def
square(x
:
Int)
=
x * x
square(x) + square(y)
}
|
复杂一点的程序离不开流程控制语句,Scala 提供了用于条件判断的 if else
和表示循环的 while
。和 Java 中对应的条件判断语句不同,Scala 中的 if else
是一个表达式,根据条件的不同返回相应分支上的值。比如下面例子中求绝对值的程序,由于 Scala 中的 if else
是一个表达式,所以不用像 Java 那样显式使用 return
返回相应的值。
清单 4. 使用 if else 表达式
1
2
|
def
abs(n
:
Int)
:
Int
=
if
(n >
0
) n
else
-n
|
和 Java 一样,Scala 提供了用于循环的 while 语句,在下面的例子中,我们将借助 while 循环为整数列表求和。
清单 5. 使用 while 为列表求和
1
2
3
4
5
6
7
8
9
|
def
sum(xs
:
List[Int])
=
{
var
total
=
0
var
index
=
0
while
(index < xs.size) {
total +
=
xs(index)
index +
=
1
}
total
}
|
上述程序是习惯了 Java 或 C++ 的程序员想到的第一方案,但仔细观察会发现有几个问题:首先,使用了 var
定义变量,我们在前面说过,尽量避免使用 var
。其次,这个程序太长了,第一次拿到这个程序的人需要对着程序仔细端详一会:程序首先定义了两个变量,并将其初始化为 0
,然后在 index
小于列表长度时执行循环,在循环体中,累加列表中的元素,并将 index
加 1
,最后返回最终的累加值。直到这时,这个人才意识到这个程序是对一个数列求和。
让我们换个角度,尝试用递归的方式去思考这个问题,对一个数列的求和问题可以简化为该数列的第一个元素加上由后续元素组成的数列的和,依此类推,直到后续元素组成的数列为空返回 0。具体程序如下,使用递归,原来需要 9 行实现的程序现在只需要两行,而且程序逻辑看起来更清晰,更易懂。(关于如何使用递归的方式去思考问题,请参考作者的另外一篇文章《使用递归的方式去思考》)
清单 6. 使用递归对数列求和
1
2
3
4
|
//xs.head 返回列表里的头元素,即第一个元素
//xs.tail 返回除头元素外的剩余元素组成的列表
def
sum
1
(xs
:
List[Int])
:
Int
=
if
(xs.isEmpty)
0
else
xs.head + sum
1
(xs.tail)
|
有没有更简便的方式呢?答案是肯定的,我们可以使用列表内置的一些方法达到同样的效果:
1
|
xs.foldLeft(
0
)((x0, x) => x0 + x)
|
该方法传入一个初始值 0,一个匿名函数,该匿名函数累加列表中的每一个元素,最终返回整个列表的和。使用上面的方法,我们甚至不需要定义额外的方法,就可以完成同样的操作。事实上,List 已经为我们提供了 sum 方法,在实际应用中,我们应该使用该方法,而不是自己定义一个。作者只是希望通过上述例子,让大家意识到 Scala 虽然提供了用于循环的 while 语句,但大多数情况下,我们有其他更简便的方式能够达到同样的效果。
掌握了上面这些内容,我们已经可以利用 Scala 求解很多复杂的问题了。比如我们可以利用牛顿法定义一个函数来求解平方根。牛顿法求解平方根的基本思路如下:给定一个数 x
,可假设其平方根为任意一个正数 ( 在这里,我们选定 1 为初始的假设 ),然后比较 x
与该数的平方,如果两者足够近似(比如两者的差值小于 0.0001),则该正数即为 x
的平方根;否则重新调整假设,假设新的平方根为上次假设
与 x/ 上次假设
的和的平均数。通过下表可以看到,经过仅仅 4 次迭代,就能求解出相当精确的 2 的平方根。
表 1. 牛顿法求解 2 的平方根
假设 | 假设的平方与 2 进行比较 | 新的假设 |
---|---|---|
1 | |1 * 1 – 2| = 1 | (1 + 2/1)/2 = 1.5 |
1.5 | |1.5 * 1.5 – 2| = 0.25 | (1.5 + 2/1.5)/2 = 1.4167 |
1.4167 | |1.4167 * 1.4167 – 2| = 0.0070 | (1.4167 + 2/1.4167)/2 = 1.4142 |
1.4142 | |1.4142 * 1.4142 – 2| = 0.000038 | …… |
将上述算法转化为 Scala 程序,首先我们定义这个迭代过程,这也是该算法的核心部分,所幸这一算法非常简单,利用递归,一个 if else
表达式就能搞定。后续为两个辅助方法,让我们的程序看起来更加清晰。最后我们选定初始假设为 1
,定义出最终的 sqrt
方法。
清单 7. 使用牛顿法求解平方根
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 迭代函数,若解不满足精度,通过递归调用接续迭代
def
sqrtIter(guess
:
Double, x
:
Double)
:
Double
=
if
(isGoodEnough(guess, x))
guess
else
sqrtIter((guess + x / guess)/
2
, x)
// 判断解是否满足要求
def
isGoodEnough(guess
:
Double, x
:
Double)
=
abs(guess * guess - x)<
0.0001
// 辅助函数,求绝对值
def
abs(x
:
Double)
=
if
(x <
0
) -x
else
x
// 目标函数
def
sqrt(x
:
Double)
:
Double
=
sqrtIter(
1
, x)
// 测试代码
sqrt(
2
)
|
这段程序看起来相当优美:首先它没有使用 var
定义其他辅助变量,在程序中避免使用 var
总是一件好事情;其次它没有使用 while
循环描述整个迭代过程,取而代之的是一段非常简洁的递归,使程序逻辑上看起来更加清晰;最后它没有将整个逻辑全部塞到一个函数里,而是分散到不同的函数里,每个函数各司其职。然而这段程序也有一个显而易见的缺陷,作为用户,他们只关心 sqrt
函数,但这段程序却将其他一些辅助函数也暴露给了用户,我们在前面提到过,Scala 里可以嵌套定义函数,我们可以将这些辅助函数定义为sqrt
的内部函数,更进一步,由于内部函数可以访问其函数体外部定义的变量,我们可以去掉这些辅助函数中的 x
参数。最终的程序如下:
清单 8. 使用牛顿法求解平方根 – 使用内部函数隐藏细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 目标函数,通过将需要用到的辅助函数定义为内部函数,实现细节的隐藏
def
sqrt(x
:
Double)
:
Double
=
{
// 迭代函数,若解不满足精度,通过递归调用接续迭代
def
sqrtIter(guess
:
Double)
:
Double
=
if
(isGoodEnough(guess))
guess
else
sqrtIter((guess + x / guess) /
2
)
// 判断解是否满足要求
def
isGoodEnough(guess
:
Double)
=
abs(guess * guess - x) <
0.0001
// 辅助函数,求绝对值
def
abs(x
:
Double)
=
if
(x <
0
) -x
else
x
sqrtIter(
1
)
}
|
我们已经利用 Scala 集成开发环境提供的 Worksheet 体验了 Scala 的基本语法,在实际开发中,我们更关心如何运行 Scala 程序。在运行方式上,Scala 又一次体现出了它的灵活性。它可以被当作一种脚本语言执行,也可以像 Java 一样,作为应用程序执行。
我们可以将 Scala 表达式写在一个文件里,比如 Hello.scala。在命令行中直接输入 scala Hello.scala
就可得到程序运行结果。
清单 9. Hello.scala
1
|
println(“Hello Script!”)
|
作为应用程序执行时,我们需要在一个单例对象中定义入口函数 main
,经过编译后就可以执行该应用程序了。
清单 10. HelloWorld.scala
1
2
3
4
5
|
object
HelloWorld {
def
main(args
:
Array[String])
:
Unit
=
{
println(
"Hello World!"
)
}
}
|
Scala 还提供了一个更简便的方式,直接继承另一个对象 App,无需定义 main
方法,编译即可运行。
清单 11. HelloScala.scala
1
2
3
|
object
HelloScala
extends
App {
println(
"Hello Scala!"
)
}
|
本文为大家介绍了 Scala 的基本语法,相比 Java,Scala 的语法更加简洁,比如 Scala 的类型推断可以省略程序中绝大多数的类型声明,短小精悍的匿名函数可以方便的在函数之间传递,还有各种在 Scala 社区约定俗成的习惯,比如省略的分号以及函数体只有一条表达式时的花括号,这一切都帮助程序员写出更简洁,更优雅的程序。限于篇幅,本文只介绍了 Scala 最基本的语法,如果读者想跟进一步学习 Scala,请参考 Scala 的 官方文档及文末所附的参考资源。
掌握了这些基本的语法, 就可以用Scala 进行函数式编程,这是 Scala 最让人心动的特性之一,对于习惯了面向对象的程序员来说,学习 Scala 更多的是在学习如何使用 Scala 进行函数式编程。