表达式及参数
def x = e 这是一个非常简单的表达式,当Scala执行这句语句的时候并不会立刻去计算/获取e的值,一直到程序需要 x 的值才会进行实际的替换(以前用过惰性加载,这个得叫惰性赋值吧)。复杂的表达式可以通过逐项替换逐渐简化成简单表达式,术语为reduction。
Scala用两种方式调用函数的参数:Call-by-value, Call-by-name。默认情况下,Scala使用Call-by-value方式,可以使用 => 来指定Call-by-name方式:
1: def loop: Int = loop //don't try this at home!
2:
3: // x: Call-by-value; y: Call-by-name
4: def constOne(x: Int, y: => Int) = x
5:
6: // if loop is Call-by-name and never used, so Scala avoids the evaluation
7: constOne(1, loop) // return 1
8:
9: // if loop is Call-by-value, when Scala try to reduce the expression, the
10: // same term reduces repeatedly to itself, hence evaluation does not terminate
11: constOne(loop, 1) // gives an infinite loop.
Call-by-value可以避免重复对参数项进行计算及赋值(evaluation),而Call-by-name可以避免对函数没有用到的参数项进行计算及赋值。前者至少且只进行一次evaluation,通常比较有效率,但有可能陷入死循环;后者则每次取用这个参数都进行一次evaluation。
条件表达式:if-else
在 Java 或 Groovy 中,if-else 只能作为单纯的控制结构,但在 Scala 中,if-else (我猜)会返回值。因此在 Scala 中不需要三元表达式 a : b ? c,直接使用 if-else 结构即可。
例程:牛顿法开平方
1: #!/bin/env scala
2: !#
3: def abs(x: Double) = if (x >= 0) x else -x
4: def square(x: Double) = x * x
5:
6: def sqrtIter(guess: Double, x: Double): Double =
7: if(isGoodEnough(guess, x)) guess else sqrtIter(improve(guess, x), x)
8:
9: def improve(guess: Double, x: Double) = (guess + x / guess) / 2
10: def isGoodEnough(guess: Double, x: Double) = abs(square(guess) - x) < 0.001
11:
12: def sqrt(x: Double) = sqrtIter(1.0, x)
13:
14: println(sqrt(25))
15: println(sqrt(0.000036))
在例程中可以看到,涉及递归的函数需要显式声明返回类型(因为逻辑上编译器无法在有限步数内推算出返回类型),对于普通的函数,一般编译器都能根据上下文进行类型推断,此时加不加显式声明就是风格问题了。铅笔书推荐不要加,Scala By Example则推荐加,在此不做深究,宗教信仰么……略过。
嵌套函数
之前的例程虽然实现了sqrt,但是却暴露出improve和isGoodEnough这两个不太可能被重用的方法,可以利用Scala嵌套函数的语法进行改进:
1: def sqrt(x: Double) = {
2: def sqrtIter(guess: Double): Double =
3: if(isGoodEnough(guess)) guess else sqrtIter(improve(guess))
4:
5: def improve(guess: Double) = (guess + x / guess) / 2
6: def isGoodEnough(guess: Double) = abs(square(guess) - x) < 0.001
7: sqrtIter(1.0)
8: }
进行嵌套后,x 在整个 block 范围内都是可见的,所以三个嵌套函数之间不需要再互相传递 x。
尾递归
1: // Greatest common divisor
2: def gcd(a: Int, b: Int): Int = if(b == 0) a else gcd(b, a % b)
3:
4: gcd(14, 21)
5: // -> gcd(21, 14)
6: // -> gcd(14, 7)
7: // -> gcd(7, 0)
8: // -> return 7
9:
10: def factorial(n: Int): Int = if(n == 0) 1 else n * factorial(n - 1)
11:
12: factorial(3)
13: // 3 * factorial(2)
14: // 3 * (2 * factorial(1))
15: // 3 * (2 * 1)
16: // return 6
考察这两个递归函数,求最大公约数 gcd 的时候,每一次递归后,表达式都保持了同样的规模;而在求阶乘 factorial 的时候,每一次递归都把表达式扩展为更长的表达式。前者即所谓的尾递归(就像一连串调用首尾相连,到最后从尾巴上跳回原始调用)。由于尾递归在每次递归过程中新的表达式(理论上)都可以覆盖前一表达式的 stack 空间,因此其空间效率较高。另外,如果一个函数的最后一个调用是其它的函数,那么理论上只需要单一的stack即可,这样的调用称为“尾调用”(Tail Call)。但由于JVM缺乏堆栈级别的重用机制,所以一般 Scala 里只有函数尾调用自身的时候才能重用栈。
练习1:优化开方函数,原始的代码在计算非常小的数时精度极差,在算非常大的数时可能死循环,为什么,试修改
练习2:将阶乘写成尾递归的形式
PS: 没想到这本书居然会讲到算法和性能的问题
PS II: 范例看得多了,慢慢的感觉就来了。
PS III: 看惯了java doc,看Scala的文档格式好吃力