问题导读
1.Scala 如何定义匿名函数?
2.任何函数是否都有返回值?
3.Scala 中的 if else是一个表达式,与Java有什么不同?
Hello World 让我们以经典的 Hello World 程序开始,只需在 Worksheet 里输入 println("Hello World!")保存即可,在该语句的右边就可以立刻看到程序执行结果。
Worksheet 也可以被当作一个简单的计算器,试着输入一些算式,保存。
图 1. Worksheet
变量和函数 当然,我们不能仅仅满足使用 Scala 来进行一些算术运算。写稍微复杂一点的程序,我们就需要定义变量和函数。Scala 为定义变量提供了两种语法。使用 val定义常量,一经定义后,该变量名不能被重新赋值。使用 var定义变量,可被重新赋值。在 Scala 中,鼓励使用 val,除非你有明确的需求使用 var。对于 Java 程序员来说,刚开始可能会觉得有违直觉,但习惯后你会发现,大多数场合下我们都不需要 var,一个可变的变量。
清单 1. 定义变量
val x = 0
var y = 1
y = 2
// 给常量赋值会出现编译错误
// x = 3
// 显式指定变量类型
val x1: Int = 0
var y1: Int = 0 复制代码
仔细观察上述代码,我们会有两个发现:
定义变量时没有指定变量类型。这是否意味着 Scala 是和 Python 或者 Ruby 一样的动态类型语言呢?恰恰相反,Scala 是严格意义上的静态类型语言,由于其采用了先进的类型推断(
Type Inference )技术,程序员不需要在写程序时显式指定类型,编译器会根据上下文推断出类型信息。比如变量 x被赋值为 0,0 是一个整型,所以 x的类型被推断出为整型。当然,Scala 语言也允许显示指定类型,如变量 x1,y1的定义。一般情况下,我们应尽量使用 Scala 提供的类型推断系统使代码看上去更加简洁。
另一个发现是程序语句结尾没有分号,这也是 Scala 中约定俗成的编程习惯。大多数情况下分号都是可省的,如果你需要将两条语句写在同一行,则需要用分号分开它们。
函数的定义也非常简单,使用关键字 def,后跟函数名和参数列表,如果不是递归函数可以选择省略函数返回类型。Scala 还支持定义匿名函数,匿名函数由参数列表,箭头连接符和函数体组成。函数在 Scala 中属于一级对象,它可以作为参数传递给其他函数,可以作为另一个函数的返回值,或者赋给一个变量。在下面的示例代码中,定义的匿名函数被赋给变量 cube。匿名函数使用起来非常方便,比如 List对象中的一些方法需要传入一个简单的函数作为参数,我们当然可以定义一个函数,然后再传给 List对象中的方法,但使用匿名函数,程序看上去更加简洁。
清单 2. 定义函数
// 定义函数
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. 定义内部函数
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 表达式
def abs(n: Int): Int =
if (n > 0) n else -n 复制代码
和 Java 一样,Scala 提供了用于循环的 while 语句,在下面的例子中,我们将借助 while 循环为整数列表求和。
清单 5. 使用 while 为列表求和
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. 使用递归对数列求和
//xs.head 返回列表里的头元素,即第一个元素
//xs.tail 返回除头元素外的剩余元素组成的列表
def sum1(xs: List[Int]): Int =
if (xs.isEmpty) 0 else xs.head + sum1(xs.tail) 复制代码
有没有更简便的方式呢?答案是肯定的,我们可以使用列表内置的一些方法达到同样的效果:
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.4167 * 1.4167 - 2| = 0.0070
(1.4167 + 2/1.4167)/2 = 1.4142
|1.4142 * 1.4142 - 2| = 0.000038
将上述算法转化为 Scala 程序,首先我们定义这个迭代过程,这也是该算法的核心部分,所幸这一算法非常简单,利用递归,一个 if else表达式就能搞定。后续为两个辅助方法,让我们的程序看起来更加清晰。最后我们选定初始假设为 1,定义出最终的 sqrt方法。
清单 7. 使用牛顿法求解平方根
// 迭代函数,若解不满足精度,通过递归调用接续迭代
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. 使用牛顿法求解平方根 - 使用内部函数隐藏细节
// 目标函数,通过将需要用到的辅助函数定义为内部函数,实现细节的隐藏
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中箭头连接符( =>)新手比较疑惑的,这里明白其中含义之一:是存在匿名函数中。
Scala 还支持定义匿名函数,匿名函数由参数列表,箭头连接符( =>)和函数体组成
// 定义匿名函数
val cube = (x: Int) => x * x *x
函数在 Scala 中属于一级对象,它可以作为参数传递给其他函数,可以作为另一个函数的返回值,或者赋给一个变量。
没有显示的 return语句,最后一条表达式的值会自动返回给函数的调用者。
由于 Scala 中的 if else是一个表达式,所以不用像 Java 那样显式使用 return返回相应的值。