Jade Dungeon

函数

内建控制结构

if表达式

标题里叫它「表达式」,所以是有返回值的。对于返回的类型,如果不同条件的类型不一样, 那么表达式的类型是公共的超类:

if (x > 0) "positive" else -1

上面表达式的类型是StringInt的共同超类Any

if表达式返回执行分支的结果:

var filename = "default.txt"
if (!args.isEmpty)
	filename = args(0)

更加函数式地写法,去掉变量:

val filename = 
	if (!args.isEmpty) args(0)
	else "default.txt"

在没有副作用的情况下,用变量的目的就是为了存个值。而表达式就是算值的, 所以直接拿表达式来用把变量省掉了:

println(if (!args.isEmpty) args(0) else "default.txt")

如果没有else部分,返回的类型可能是Unit(相当于Java中的void),写作()

if (x > 0) 1

相当于:

if (x > 0) 1 else ()

while循环

  • Scala中while循环用得不多,因为它不是表达式,没有返回值。

用do-while算最大公约数:

def gcdLoop(x: Long, y: Long): Long = {
	var a = x; var b = y
	while (a != 0) {
		val temp = a; a = b % a; b = temp
	}
	b
}

用while-do读取文件:

var line = ""
do {
	line = readline()
		println("Read: " + line)
} while (line != "")

while循环与Unit

注意这里没有叫它「表达式」。原因是它不会有返回值(类型为Unit,写作「()」), 所以不是表达式。

scala> def greet() { println("HI") }
greet: ()Unit

scala> greet() == ()
<console>:9: warning: comparing values of types Unit and Unit using `==' will always yield true
              greet() == ()
                      ^
HI
res1: Boolean = true

scala> () == ""
<console>:8: warning: comparing values of types Unit and java.lang.String using `==' will always yield false
              () == ""
                 ^
res2: Boolean = false

scala> () != ""
<console>:8: warning: comparing values of types Unit and java.lang.String using `!=' will always yield true
              () != ""
                 ^
res3: Boolean = true

注意上面的警告信息:UnitUnit进行相等运算永远为true;与String相等运算 永远是false;与String不相等运算永远为true

现在说到重点了:Scala中var赋值操作也是Unit,而不是和Java一样返回变量值。 所以下面这种Java中一直用到的写法在Scala中是会出问题的:

var line = ""
while ((line = readline()) != "") {  // always true !!!
		println("Read: " + line)
} 

while循环与函数式风格

由于while没有返回值,所以常常被函数式语言舍弃。例如对于同样一个求最大公约数的 函数,对比一下指令式与函数式的区别。

指令式,用循环:

def gcdLoop(x: Long, y: Long): Long = {
	var a = x; var b = y
	while (a != 0) {
		val temp = a; a = b % a; b = temp
	}
	b
}

函数式,用递归:

def gcd(x: Long, y: Long): Long = if (y == 0) x else gcd(x, x % y)

while循环是没有返回值的,那就一定要用副作用:不是更新var就是写I/O之类的, 不然浪费电么?

for表达式

Scale中的for表达式是遍历集合类的强大工具,包括了过滤与构造新集合的功能。但是在 Scala提供的for表达式中没有提供breakcontinue来退出循环,这是为了更加贴合 函数式风格的需要。如果真的需要要break时,可以有以下几个方案:

  • 使用Boolean变量控制某块代码是否执行。
  • 使用嵌套函数,这样可以从函数当中return
  • 使用Breaks对象中的break方法。

Breaks对象中的break方法的例子:

import scala.util.control.Breaks_

breakable {
	for (...) {
		...
		if (...) break;
		...
	}
}

范围(range)

使用tountil可以按范围生成整数的范围:

<start-int> [to|until] <end-int> [by increment]

函数to用来抽象一个序列:

scala> 1 to 10
res0: scala.collection.immutable.Range.Inclusive = Range 1 to 10

scala> l.toList
res3: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

until生成的区间不包括上限:

scala> (1 until 10).toList
res5: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)

可以指定步长:

scala> (1 to 10 by 3).toList
res8: List[Int] = List(1, 4, 7, 10)

scala> (10 to 1 by -3).toList
res9: List[Int] = List(10, 7, 4, 1)

可以适用长整形:

scala> (1L to 10L by 3).toList
res10: List[Long] = List(1, 4, 7, 10)

可以适用浮点与双精度型,步长可以不是整数:

scala> (1.1f to 10.3f by 3.1f).toList
res11: List[Float] = List(1.1, 4.2, 7.2999997)

scala> (1.1 to 10.3 by 3.1).toList
res12: List[Double] = List(1.1, 4.2, 7.300000000000001)

可以适用字符型:

scala> ('a' to 'g' by 3).toList
res13: List[Char] = List(a, d, g)

可以适用其他数据类型:

scala> (BigInt(1) to BigInt(10) by 3).toList
res14: List[BigInt] = List(1, 4, 7, 10)

scala> (BigDecimal(1.1) to BigDecimal(10.3) by 3.1).toList
res15: List[scala.math.BigDecimal] = List(1.1, 4.2, 7.3)

生成器(generator)

生成器语法item <- collection,对应每步遍历时集合中对应的项:

// Array[file]
val fileList = (new java.io.File(".")).listFiles

for (file <- fileList) println(file)

指定次数:

scala> for (i <- 1 to 4) print(i + " ")
1 2 3 4

scala> for (i <- 1 until 4) print(i + " ")
1 2 3 

不推荐的风格,遍历时还要考虑下标是从0还是从1开始,会不会越界:

for (i <-0 to filesList.length -1)
	println(fileList(i))

可以用分号隔开多个生成器:

scala> for (i <-1 to 3; j <- 1 to 3) print(i + "-" + j + ", ")
1-1, 1-2, 1-3, 2-1, 2-2, 2-3, 3-1, 3-2, 3-3, 

for是表达式

for表达式之所以被称为「表达式」是因为它能产生有用的值。值的类型类型取决于 <-子句的集合。

过滤器(filter)

只要处理以.scala结尾的文件。如果用Java的思路,代码是这样的:

for (file <- fileList)
	if ( file.getName.endsWith(".scala") )
		println(file)

Scala可以做得更好,给for循环加上选择器:

for (file <- fileList if file.getName.endsWith(".scala"))
		println(file)

而且不止可以加一个,当然多个语句之间要用分号分隔:

for (
	file <- fileList 
	if file.isFile;
	if file.getName.endsWith(".scala")
) println(file)

for循环嵌套

多个for循环与多个过滤器可以相互嵌套:

def fileLines(file: java.io.File) = 
	scala.io.Source.fromFile(file).getlines.toList

def grep(pattern: String) =
	for {
		file <- filesList
		if file.getName.endWith(".scala")
		line <- fileLines(file)
		if line.trim.matches(pattern)
	} println(file + ": " + line.trim)

grep(".*gcd.*")

就像上面的代码那样,花括号可以代替小括号。但是替换的目的是什么呢?就是为了好看。 这么多行用花括号看起来像是一种语言级的结构一样。

花括号代替小括号

在前面提到过的Scala的断句原则时提到,在小括号里被认为是一条语句, 所以多个for嵌套时要加上分号分隔;花括号里可以放多条,根据换行可以正确断句。

(流间)变量绑定(mid-stream)

请注意前面的代码段中重复出现的表达式line.trim。这不是个可忽略的计算, 因此你或许想每次只算一遍。通过用等号=把结果绑定到新变量可以做到这点。 绑定的变量被当作val引入和使用,不过不用带关键字val

def grep(pattern: String) =
	for {
		file <- filesList if file.getName.endWith(".scala")
		line <- fileLines(file)
		trimmed = line.trim if line.trim.matches(pattern)
	} println(file + ": " + trimmed)

注意在上面代码中<-代表的容器迭代操作与=所代表的赋值操作的区别。

制造新集合

到现在为止所有的例子都只是对枚举值进行操作然后就放过,除此之外,你还可以通过 yield关键字来表示要创建一个容器去记住每一次的迭代。

for表达式在每次执行的时候都会制造一个值,当for表达式完成的时候,结果将是一个 包含了所有产生的值的集合。结果集合的类型基于枚举子句处理的集合类型。

对于for-yield表达式的语法是这样的:

for {子句} yield {循环体}

比如,下面的函数鉴别出.scala文件并保存在数组里:

def scalaFiles = 
	for { 
		file <- filesHere if file.getName.endsWith(".scala") 
	} yield file

for表达式在每次执行的时候都会制造一个值,本例中是file。本例中结果为 Array[File],因为filesHere是数组并且产生的表达式类型是File

再来一个取每行长度的例子:

val forLineLengths =
	for {
		file <- filesList if file.getName.endWith(".scala")
		line <- fileLines(file)
		trimmed = line.trim if line.trim.matches(".*for.*")
	} yield trimmed.length

过滤Option成员

scala> val dogBreeds  =  Some("a") :: None :: Some("b") :: None :: Some("c") :: Nil
dogBreeds: List[Option[String]] = List(Some(a), None, Some(b), None, Some(c))

// 第一次迭代还是每一个成员
scala> for {breedOption <- dogBreeds} yield breedOption
res6: List[Option[String]] = List(Some(a), None, Some(b), None, Some(c))

// 二次迭代过滤掉None,
// breed <- breedOption相斗于一个迭代,可以过滤掉空的Option(也就是None)
// 因为Option类型也一种容器(只能有一个成员),
scala> for {breedOption <- dogBreeds; breed <- breedOption} yield breed
res7: List[String] = List(a, b, c)

// 把大写的成员作为流间变量
scala> for {breedOption <- dogBreeds; breed <- breedOption; upcasedBreed = breed.toUpperCase} yield upcasedBreed
res8: List[String] = List(A, B, C)

// 用另一种写法:
scala> for {Some(breed) <- dogBreeds; upcasedBreed = breed.toUpperCase()} yield upcasedBreed
res10: List[String] = List(A, B, C)

try表达式与异常

抛出异常

throw new IllegalArgumentException

throw也是有结果类型的表达式,而且还可以转换成任何类型。所以可以写在赋值语句里。 没有异常就是表达式的值,有异常了得到Nothing

val half = 
	if ( n % 2 == 0 ) n/2
	else throw new RuntimeException("n must be even")

捕获异常

捕获异常的语法选择catch子句的形式。这样设计的原因是为了与Scala很重要的部分: 模式匹配(pattern matching)保持一致。模式匹配是一种很强大的特征, 将在稍后概述并在另外的章节中详述。

import java.io.FileReader 
import java.io.FileNotFoundException 
import java.io.IOException 

try { 
	val f = new FileReader("input.txt") // Use and close file 
} catch { 
	case ex: FileNotFoundException => // Handle missing file 
	case ex: IOException => // Handle other I/O error 
}

如果要匹配所有的异常类型,用case _

还有一点要注意顺序,Java里会提示后面的异常已经被前面捕获,而Scala里不会。

方法的throws声明

与Java的一个差别是Scala里不需要你捕获检查异常(checked exception)或把它们声明在 throws子句中。如果你愿意,可以用@throws标注声明一个throws子句,但这不是 必需的(详见「Scala结合Java」一章的「注解」一节)。@throws来说明:

  import java.io._
  class Reader(fname: String) {
    private val in =
      new BufferedReader(new FileReader(fname))
 
    @throws(classOf[IOException])
    def read() = in.read()
  }

finally子句

没有啥要特别说明的:

import java.io.FileReader

val file = openFile()
try { 
	// ... do something ...
} finally { 
	file.close()
}

try-cache-finally产生的值

和其它大多数Scala控制结构一样,try-catch-finally也产生值。

下面的例子尝试拆分URL,但如果URL格式错误就使用缺省值。结果是,如果没有异常抛出, 则对应于try子句;如果抛出异常并被捕获,则对应于相应的catch子句。如果异常被抛出 但没被捕获,表达式就没有返回值。由finally子句计算得到的值,如果有的话,被抛弃。

通常finally子句做一些清理类型的工作如关闭文件;他们不应该改变在主函数体或try的 catch子句中计算的值。

import java.net.URL 
import java.net.MalformedURLException 

def urlFor(path: String) = 
	try { new URL(path) } 
	catch { 
		case e: MalformedURLException => 
			new URL("http://www.scalalang.org") 
}

而下面的两个例子一个第一个值为2,第二个值为1:

scala> def f(): Int = try { return 1 } finally { return 2 }
f: ()Int

scala> f()
res1: Int = 2

scala> def g(): Int = try { 1 } finally { 2 }
g: ()Int

scala> g()
res2: Int = 1

finally中的返回值会覆盖所有的结果,因此通常最好还是避免从finally子句中返回值。 最好是把finally子句当作确保某些副作用,如关闭打开的文件。

match表达式

Scala的匹配表达式允许你在许多可选项(alternative)中做选择,就好象其它语言中的 switch语句。通常说来match表达式可以让你使用任意的模式(pattern)做选择, 后面会有专门的篇幅介绍。通用的模式可以稍等再说。目前,只要考虑使用match在 若干可选项中做选择。

下面的例子里的脚本从参数列表读入食物名然后打印食物配料。match表达式检查参数列表 的第一个参数firstArg。如果是字串"salt"就打印"pepper";如果是"chips", 就打印"salsa",如此递推。缺省情况用下划线_说明,这是常用在Scala里作为占位符 表示完全不清楚的值的通配符。

val firstArg = if (args.length > 0) args(0) else "" 

firstArg match { 
	case "salt"  => println("pepper") 
	case "chips" => println("salsa") 
	case "eggs"  => println("bacon") 
	case _       => println("huh?") 
}

与Java的switch语句比,匹配表达式还有一些重要的差别:

  • 任何种类的常量或其他东西都能用作Scala里的case,不像Java里只能整形和枚举常量。
  • 没有break,而且也不会从项转到下一项。
  • match表达式也能产生值。
val firstArg = if (args.length > 0) args(0) else "" 

val friend = firstArg match { 
	case "salt"  => "pepper"
	case "chips" => "salsa" 
	case "eggs"  => "bacon" 
	case _       => "huh?"
}
println(friend)

不要用break和contine

break和continue与函数字面量结合得不好, 而且有效利用函数字面量可以让代码写得更加简短。

最简单的方式是用if替换每个every和用布尔变量替换每个break。 布尔变量指代是否包含它的while循环应该继续。 比如说,假设你正搜索一个参数列表去查找以.scala结尾但不以连号开头的字串。 Java里你可以——如果你很喜欢while循环,break和continue——如此写:

  int i = 0;                // This is Java
  boolean foundIt = false;
  while (i < args.length) {
    if (args[i].startsWith("-")) {
      i = i + 1;
      continue;
    }
    if (args[i].endsWith(".scala")) {
      foundIt = true;
      break;
    }
    i = i + 1;
  }

如果要字面直译成Scala的代码,代之以执行一个if然后continue,你可以写一个if环绕 while余下的全部内容。要去掉break,你可以增加一个布尔变量提示是否继续做下去, 不过在这里你可以复用foundIt,基本就是这样:

  var i = 0
  var foundIt = false

  while (i < args.length && !foundIt) {
    if (!args(i).startsWith("-")) {
      if (args(i).endsWith(".scala"))
        foundIt = true
    }
    i = i + 1
  }

这个版本与原来的Java代码非常像。所有的主要段落仍然存在并保持原顺序。 有两个可重新赋值的变量及一个while循环。循环内有个i是否小于args.length的测试, 然后检查"-",然后检查".scala"。

如果要去掉代码里的var,可以尝试的一种方式是用递归函数重写循环。比方说, 可以定义带一个整数值做输入的searchFrom函数向前搜索,并返回想要的参数的索引。 采用这种技巧的代码看上去会像这样:

  def searchFrom(i: Int): Int =
    if (i >= args.length) -1
    else if (args(i).startsWith("-")) searchFrom(i + 1) 
    else if (args(i).endsWith(".scala")) i
    else searchFrom(i + 1)

  val i = searchFrom(0)

每个continue都被带有i + 1做参数的递归调用替换掉,有效地跳转到下一个整数。 用递归替代了循环的编程风格更易于理解。

Scala编译器不会实际对上面展示的代码生成递归函数。因为所有的递归调用都在尾调用: tail-call位置,编译器会产生出与while循环类似的代码。

每个递归调用将被实现为回到函数开始位置的跳转。尾调用优化将在后面用另外篇幅讨论。

变量作用域

大括号通常引入了一个新的范围,所以任何定义在打括号里的东西在括号之后就脱离了范围 。这条规则有几个例外,因为在Scala里有时候你可以用大括号代替小括号。表达式语法的 替代品是这种使用大括号例子的其中之一。

本地变量:local variable。对于它们被定义的函数来说是「本地」的。每次函数被调用的 时候,一整套全新的本地变量将被使用。一旦变量被定义了,你就不可以在同一个范围内 定义同样的名字。比如,下面的脚本不会被编译通过:

  val a = 1
  val a = 2 // Does not compile
  println(a)

然而,你可以在一个内部范围内定义与外部范围里名称相同的变量(注意之在Java里不行) 。下列脚本将编译通过并可以运行。内部变量被说成是遮蔽(shadow)了同名的外部变量, 因为在内部范围内外部变量变得不可见了:

  val a = 1;
  {
    val a = 2 // Compiles just fine
    println(a)
  }
  println(a)

在解释器里看上去像是遮蔽的东西:

  scala> val a = 1
  a: Int = 1

  scala> val a = 2
  a: Int = 2

  scala> println(a)
  2

在理论上,解释器在每次你输入新的语句时都创建了一个新的嵌套范围。因此, 你可以把之前解释的代码虚拟化认为是

  val a = 1;
  {
    val a = 2;
    {
      println(a)
    }
  }

重构指令式风格的代码

通过指令式风格输出乘法表:

  def printMultiTable() {

    var i = 1
    // only i in scope here

    while (i <= 10) {

      var j = 1
      // both i and j in scope here

      while (j <= 10) {

        val prod = (i * j).toString
        // i, j, and prod in scope here

        var k = prod.length
        // i, j, prod, and k in scope here

        while (k < 4) {
          print(" ")
          k += 1
        }

        print(prod)
        j += 1
      }

      // i and j still in scope; prod and k out of scope

      println()
      i += 1
    }

    // i still in scope; j, prod, and k out of scope
  }

代码在两个方面显示出了指令式风格。

首先,调用printMultiTable有副作用:在标准输出上打印乘法表。在函数式风格中, 我们重构了函数,让它把乘法表作为字串返回。由于函数不再执行打印,我们把它重命名为 multiTable。正如前面提到过的,没有副作用的函数的一个优点是它们很容易进行 单元测试。要测试printMultiTable,你需要重定义printprintln从而能够检查 输出的正确性。测试multiTable就简单多了,只要检查结果即可。

  // Returns a row as a sequence
  def makeRowSeq(row: Int) =
    for (col <- 1 to 10) yield {
      val prod = (row * col).toString
      val padding = " " * (4 - prod.length)
      padding + prod
    }

  // Returns a row as a string
  def makeRow(row: Int) = makeRowSeq(row).mkString

  // Returns table as a string with one row per line
  def multiTable() = {

    val tableSeq = // a sequence of row strings
      for (row <- 1 to 10)
      yield makeRow(row)

    tableSeq.mkString("\n")
  }

printMultiTable里另一个揭露其指令式风格的信号来自于它的while循环和var。 与之相对,multiTable函数使用了val,for表达式,帮助函数:helper function, 并调用了mkString。

我们提炼出两个帮助函数makeRowmakeRowSeq,使代码容易阅读。

函数makeRowSeq使用for表达式从1到10枚举列数。这个for函数体计算行和列的乘积, 决定乘积前占位的空格,并生成由占位空格,乘积字串叠加成的结果。for表达式的结果是 一个包含了这些生成字串作为元素的序列(scala.Seq的某个子类)。

另一个帮助函数makeRow仅仅调用了makeRowSeq返回结果的mkString函数。 叠加序列中的字串把它们作为一个字串返回。

multiTable方法首先使用一个for表达式的结果初始化tableSeq,这个for表达式从1到10 枚举行数,对每行调用makeRow获得该行的字串。因为字串前缀yield关键字, 所以表达式的结果就是行字串的序列。现在仅剩下的工作就是把字串序列转变为单一字串。 mkString的调用完成这个工作,并且由于我们传递进去"\n",因此每个字串结尾插入了 换行符。

函数与闭包

没有返回值的函数

类型Unit表示没有回返值

类型Unit表示没有回返值:

scala> def log(d: Double): Unit = println(f"Got value $d%.2f")
log: (d: Double)Unit

函数最后语句没有返回值

有些情况下可以推导出函数是没有返回值的(比如println本身没有返回值), 所以Unit类型声明可以省略:

scala> def log(d: Double) = println(f"Got value $d%.2f")
log: (d: Double)Unit

不用等号表示没有返回值

虽然Scala会把最后一个语句的返回值推导为函数的函数值, 但也可以不用等号来表示没有返回值,但是这种风格官方不推荐:

scala> def log(d: Double) { println(f"Got value $d%.2f") }
log: (d: Double)Unit

关于函数的参数

没有参数的函数

用空的括号表示没有参数的函数,调用时有没有括号都可以:

scala> def hi(): String = "hi"
hi: ()String

scala> hi()
res1: String = hi

scala> hi
res0: String = hi

但是如果定义时没有括号,那调用时也不能加括号:

scala> def hi:String = "hi"
hi: String

scala> hi()
<console>:9: error: not enough arguments for method apply: (index: Int)Char in class StringOps.
Unspecified value parameter index.
              hi()
                ^

scala> hi
res2: String = hi

把代码块作为实参

对于一个函数:

scala> def formatEuro(amt: Double) = f"$amt%.2f"
formatEuro: (amt: Double)String

可以直接把实参传给它:

scala> formatEuro(3.4645)
res4: String = 3.46

也可以把一段代码作为为实参,程序自动算出代码的结果再作为参数传给函数:

scala> formatEuro { val rate = 1.32; 2.35 + 0.7123 + rate * 5.32 }
res5: String = 10.08

应用序与正则序

  • 应用序,又名「按值调用」
  • 正则序,又名「按名调用」

Scala的参数默认为应用序的,通过加上=>可以声明为正则序:

def fun01(a: String, b: String) = a + b        // 应用序

def fun01(a: String, b: => String) = a + b     // 正则序

带名参数

可以在调用时指定的每个实参的参数名,这样就不用按定义函数时参数表的顺序来传参数:

scala> def greet(prefix: String, name: String) = s"$prefix $name"
greet: (prefix: String, name: String)String

scala> greet(name = "Brown", prefix = "Mr")
res2: String = Mr Brown

参数默认值与带名参数

定义参数有默认值的参数:

def decorate(str: String, left: String = "[", right: String = "]") = 
	left + str + right

调用时:

scala> decorate("hello")
res0: String = [hello]

scala> decorate("hello","<",">")
res1: String = <hello>
注意顺序

结合前面介绍的带名参数,传入实参不一定要按参数表的顺序,可以指定对应的形参名:

scala> decorate("hello", right = ">")
res2: String = [hello>
全部默认要加上括号

如果所有的实参都用默认的,那也要带上括号:

scala> def sayHello(msg: String = "Hello, World") { println(msg) }
sayHello: (msg: String)Unit

scala> sayHello("Hello, User")
Hello, User

scala> sayHello()
Hello, World

scala> sayHello
<console>:13: error: missing argument list for method sayHello
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writing `sayHello _` or `sayHello(_)` instead of `sayHello`.
       sayHello
       ^

变长参数

Scala允许你指明函数的最后一个参数可以是重复的。这可以允许客户向函数传入可变长度 参数列表。想要标注一个重复参数,在参数的类型之后放一个星号。例如:

def sum(args: Int*) = {
  var res = 0
  for (arg <- args) res += arg
  res
}

传入多个参数:

scala> val s = sum (1,4,9,16,25)
s: Int = 55

函数内部,重复参数的类型是声明参数类型的数组。因此,echo函数里被声明为类型 String*的args的类型实际上是Array[String]

但是不能把序列(列表或数组)传入:

scala> val s = sum (1 to 5)
<console>:8: error: type mismatch;
 found   : scala.collection.immutable.Range.Inclusive
 required: Int
       val s = sum (1 to 5)
                      ^

把序列转为多个参数的方法是序列实参后添加:_*符号(这三个之间还可以有空格), 像这样:

scala> val s = sum(1 to 5: _*)
s: Int = 15

在递归定义中我们会用到这样的语法:

def recursiveSum(args: Int*): Int = {
  if (args.length ==0) 0
  else args.head + recursiveSum(args.tail: _*)
}

注意:

  • 转换列表为可变参数时,数天的类型只能通过类型推导得到,如:args: _*, Scala不允许在这时指定类型(如:args: String*)。
  • 如果使用变长参数且参数类型为Object的Java方法,如PrintStreamprintfMessageFormat.format时,要手工对基本类型进行转换。如:
import java.text.MessageFormat
val str = MessageFormat.format("The answer to {0} is {1}", "everything",
		42.asInstanceOf[AnyRef])

str: String = The answer to everything is 42

函数与高阶函数

方法(method)

方法是被定义为某个对象成员的函数,这是最常用的形式。如下面这个工具检查文件中超过 指定长度的行:

  import scala.io.Source

  object LongLines {

    def processFile(filename: String, width: Int) {
      val source = Source.fromFile(filename)
      for (line <- source.getLines) 
        processLine(filename, width, line)
    }

    private def processLine(filename: String,
        width: Int, line: String) {

      if (line.length > width)
        println(filename +": "+ line.trim)
    }
  }

再定义了一个application以后,就可以在shell中调用它了,把第一个命令行参数当作 行长度,并把后续的参数解释为文件名:

  object FindLongLines {
    def main(args: Array[String]) {
      val width = args(0).toInt
      for (arg <- args.drop(1))
        LongLines.processFile(arg, width)
    } 
  } 

调用,查找一行长度超过45个字符的行:

  $ scala FindLongLines 45 LongLines.scala
  LongLines.scala: def processFile(filename: String, width: Int) {

本地函数

Java里通过定义private方法来限制访问。在Scala里还可以把方法定义在另一个函数里 来限制只有所在的代码块能访问:

  def processFile(filename: String, width: Int) {

    def processLine(filename: String, width: Int, line: String) {
      if (line.length > width) print(filename +": "+ line)
    }

    val source = Source.fromFile(filename)
    for (line <- source.getLines) processLine(filename, width, line)

  }

还可以省掉filenamewidth这两个参数的传递:

  import scala.io.Source

  object LongLines {
  
    def processFile(filename: String, width: Int) {
      def processLine(line: String) {
        if (line.length > width) print(filename +": "+ line)
      }    

      val source = Source.fromFile(filename)
      for (line <- source.getLines) processLine(line)
    }

  }

头等函数与函数字面量(literal)

Scala拥有头等函数(first-class function),除了定义函数与调用函数外, 还可以写成没有名字的函数字面量(literal)。

函数字面量直接作为一段文本被编译进一个类中,等到运行时被实例化为函数值 (function value)。

任何函数值都是某个扩展了scala包的FunctionN特质之一的类的实例,如Function0 是没有参数的函数,Function1是有一个参数的函数等等。每个FunctionN特质有一个 apply方法用来调用函数。

简单例子:

(x: Int) => x + 1

=>指明这个函数把左边的东西转变成右边的东西。所以,这是一个把任何整数x映射为 x + 1的函数。

函数值是对象,所以如果你愿意可以把它们存入变量。它们也是函数,所以可以用括号调用 它们。以下是这两种动作的例子:

  scala> var increase = (x: Int) => x + 1
  increase: (Int) => Int = <function>

  scala> increase(10)
  res0: Int = 11

本例中,因为increasevar,你可以在之后重新赋给它不同的函数值。

  scala> increase = (x: Int) => x + 9999
  increase: (Int) => Int = <function>

  scala> increase(10)
  res2: Int = 10009

如果你想在函数文本中包括超过一个语句,用大括号包住函数体,一行放一个语句, 就组成了一个代码块。与方法一样,当函数值被调用时,所有的语句将被执行, 而函数的返回值就是最后一行产生的那个表达式。

  scala> increase = (x: Int) => {
       |   println("We")
       |   println("are")
       |   println("here!")
       |   x + 1
       | }
  increase: (Int) => Int = <function>

  scala> increase(10)
  We
  are
  here!
  res4: Int = 11

许多Scala库都提供了结合函数字面量的机制。例如,所有的集合类都能用到foreach 方法和filter方法:

  scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
  someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

  scala> someNumbers.foreach((x: Int) => println(x))
  -11
  -10
  -5
  0
  5
  10

函数字面量的短格式

Scala提供了许多方法去除冗余信息并把函数文本写得更简短。

一种让函数文本更简短的方式是去除参数类型:

  scala> someNumbers.filter((x) => x > 0)
  res7: List[Int] = List(5, 10)

根据someNumbers编译器知道x一定是整数,因为它看到你立刻使用了这个函数过滤 整数列表(暗示)。这被称为目标类型化(target typing)。目标类型化的精确细节 并不重要。你可以简单地从编写一个不带参数类型的函数文本开始。并且,如果编译器 不能识别,再加上类型。几次之后你就对什么情况编译器能或不能解开谜题有感觉了。

第二种去除无用字符的方式是省略类型是被推断的参数之外的括号。前面例子里,x 两边的括号不是必须的:

  scala> someNumbers.filter(x => x > 0)
  res8: List[Int] = List(5, 10)

高阶函数(Higher-order Function)

高阶函数是指一个函数可以作为参数传递给另外一个函数。

例如,以下函数safeStringOp在进行字符串操作前增加了判断字符串是否为空的检查:

scala> def safeStringOp(s: String, f: String => String) = {
     |   if (s != null) f(s) else s
     | }
safeStringOp: (s: String, f: String => String)String

scala> def reverser(s: String) = s.reverse
reverser: (s: String)String

scala> safeStringOp(null, reverser)
res0: String = null

scala> safeStringOp("12345", reverser)
res1: String = 54321

注意这里的传入的reverser方法可以省略,直接用函数字面量来定义:

scala> def safeStringOp(s: String, f: String => String) = {
     |   if (s != null) f(s) else s
     | }
safeStringOp: (s: String, f: String => String)String

scala> safeStringOp(null, (s: String) => s.reverse)
res0: String = null

scala> safeStringOp("12345", (s: String) => s.reverse)
res1: String = 54321

还可以进一步简写为:

注意这里的传入的reverser方法可以省略,直接用函数字面量来定义:

scala> def safeStringOp(s: String, f: String => String) = {
     |   if (s != null) f(s) else s
     | }
safeStringOp: (s: String, f: String => String)String

scala> safeStringOp("12345", _.reverse)
res5: String = 54321

scala> safeStringOp(null, _.reverse)
res6: String = null

除了函数字面量,还可以用代码块调用高阶函数:

scala> safeStringOp("12345", { 
     |     s =>
     |     val tmp = s.reverse
     |     tmp + s
     | })
res5: String = 5432112345

如上例所示,可以在一个代码块里写多行代码的函数。

另一个例子,记录方法执行时间的包装:

scala> def timer[A](f: => A):A = {
     |   def now = System.currentTimeMillis
     |   val start = now
     |   val a = f;
     |   val end = now
     |   println(s"Executed in ${end - start} ms")
     |   a
     | }
timer: [A](f: => A)A

scala> timer {
     |   util.Random.setSeed(System.currentTimeMillis)
     |   for (i <- 1 to 100000) util.Random.nextDouble
     |   util.Random.nextDouble
     | }
Executed in 11 ms
res15: Double = 0.6996654430439804

timer的参数类型为A,并且参数如果是函数的话,以正则序按名称调用。

占位符语法(Placeholder syntax)

如果想让函数文本更简洁,可以把下划线当做一个或更多参数的占位符,只要每个参数在 函数文本内仅出现一次,也就是说,在这种情况下每个下划线都代表一个不同的参数。

比如,_ > 0对于检查值是否大于零的函数来说就是非常短的标注:

  scala> someNumbers.filter(_ > 0)
  res9: List[Int] = List(5, 10)

可以把下划线看作表达式里需要被「填入」的「空白」。相当于:

  scala> someNumbers.filter(x => x > 0)
  res10: List[Int] = List(5, 10)

有时编译器有可能没有足够的信息推断缺失的参数类型。如只是写:

  scala> val f = _ + _
  <console>:4: error: missing parameter type for expanded 
  function ((x$1, x$2) => x$1.$plus(x$2))
         val f = _ + _
                 ^

这种情况下使用冒号指定类型:

  scala> val f = (_: Int) + (_: Int)
  f: (Int, Int) => Int = <function>

  scala> f(5, 10)
  res11: Int = 15

注意定义函数与定义常量的区别,常量没有括号:

scala> def logStart() = "=" * 50 + "\nStarting NOW\n" + "=" * 50
logStart: ()String

scala> def logStart = "=" * 50 + "\nStarting NOW\n" + "=" * 50
logStart: String

偏应用函数(partially applied function)

下划线占位符不仅能代替一个参数,还可以代替整个参数列表:

  someNumbers.foreach(println(_))

或简化为:

  someNumbers.foreach(println _)

注意:在函数名和下划线之间留一个空格,因为不这样做编译器会认为在调用名为 println_的方法。

另一个用占位符代表整个参数的例子:

scala> def max(a:Int, b:Int):Int = if(a > b)  a else b
max: (a: Int, b: Int)Int

scala> var res = (Integer.MIN_VALUE /: list) { max _ }
res: Int = 99

Scala把短格式直接看作是你输入了下列代码:

  someNumbers.foreach(x => println(x))

以这种方式使用下划线时,你就正在写一个偏应用函数(partially applied function)。 Scala里,当你调用函数,传入任何需要的参数,你就是在把函数应用到参数上。如:

  scala> def sum(a: Int, b: Int, c: Int) = a + b + c
  sum: (Int,Int,Int)Int

你就可以把函数sum应用到参数1,2和3上,如下:

  scala> sum(1, 2, 3)
  res12: Int = 6

偏应用函数是一种表达式,你不需要提供函数需要的所有参数。代之以仅提供部分, 或不提供所需参数。比如,要创建不提供任何三个所需参数的调用sum的偏应用表达式, 只要在sum之后放一个下划线即可。然后可以把得到的函数存入变量。举例如下:

  scala> val a = sum _
  a: (Int, Int, Int) => Int = <function>

有了这个代码,Scala编译器以偏应用函数表达式,sum _,实例化一个带三个缺失 整数参数的函数值,并把这个新的函数值的索引赋给变量a。当你把这个新函数值应用于 三个参数之上时,它就转回头调用sum,并传入这三个参数:

  scala> a(1, 2, 3)
  res13: Int = 6

简单地来说,如果参数没有给齐,Scala会建立一个新类,它有一个特殊的apply方法。对 偏应用函数的调用其实是对这个apply方法的调用。更加细节的描述是这样的:

名为a的变量指向一个函数值对象。这个函数值是由Scala编译器依照偏应用函数表达式 sum _,自动产生的类的一个实例。

编译器产生的类有一个apply方法带三个参数(产生的类扩展了特质Function3,定义了 三个参数的apply方法)。之所以带三个参数是因为sum _表达式缺少的参数数量为3。 Scala编译器把表达式a(1,2,3)翻译成对函数值的apply方法的调用,传入三个参数 1,2,3。因此a(1,2,3)是下列代码的短格式:

  scala> a.apply(1, 2, 3)
  res14: Int = 6

Scala编译器根据表达式sum _自动产生的类里的apply方法,简单地把这三个缺失的参数 传到sum,并返回结果。本例中apply调用了sum(1,2,3),并返回sum返回的6。

这种一个下划线代表全部参数列表的表达式的另一种用途,就是把它当作转换def为 函数值的方式。例如,如果你有一个本地函数,如sum(a: Int, b: Int, c: Int): Int, 你可以把它「包装」在apply方法具有同样的参数列表和结果类型的函数值中。当你把这个 函数值应用到某些参数上时,它依次把sum应用到同样的参数,并返回结果。

尽管不能把方法或嵌套函数赋值给变量,或当作参数传递给其它方法,但是如果你把方法 或嵌套函数通过在名称后面加一个下划线的方式包装在函数值中,就可以做到了。

现在,尽管sum _确实是一个偏应用函数,或许对你来说为什么这么称呼并不是很明显。 这个名字源自于函数未被应用于它所有的参数。在sum _的例子里,它没有应用于任何 参数。不过还可以通过提供某些但不是全部需要的参数表达一个偏应用函数。举例如下:

  scala> val b = sum(1, _: Int, 3)
  b: (Int) => Int = <function>

这个例子里,你提供了第一个和最后一个参数给sum,但中间参数缺失。因为仅有一个参数 缺失,Scala编译器会产生一个新的函数类,其apply方法带一个参数。在使用一个参数调用 的时候,这个产生的函数的apply方法调用sum,传入1,传递给函数的参数,还有3。如下:

  scala> b(2)       // b.apply调用了sum(1,2,3)
  res15: Int = 6

  scala> b(5)       // b.apply调用了sum(1,5,3)
  res16: Int = 9

如果你正在写一个省略所有参数的偏应用程序表达式,如println _sum _,而且在 代码的那个地方正需要一个函数,你可以去掉下划线从而表达得更简明。例如,代之以 打印输出someNumbers里的每一个数字:

  val someNumbers = List(-11, -10, -5, 0, 5, 10) 
  someNumbers.foreach(println _)

你可以只是写成:

  someNumbers.foreach(println) 

最后一种格式仅在需要写函数的地方,如例子中的foreach调用,才能使用。编译器知道 这种情况需要一个函数,因为foreach需要一个函数作为参数传入。在不需要函数的情况下 ,尝试使用这种格式将引发一个编译错误。举例如下:

  scala> val c = sum
  <console>:5: error: missing arguments for method sum...
  follow this method with `_' if you want to treat it as
     a partially applied function
         val c = sum
                 ^
  scala> val d = sum _
  d: (Int, Int, Int) => Int = <function>

  scala> d(10, 20, 30)
  res17: Int = 60 

为什么要使用尾下划线? Scala的偏应用函数语法凸显了Scala与经典函数式语言如Haskell 或ML之间设计折中的差异。在经典函数式语言中,偏应用函数被当作普通的例子。更进一步 ,这些语言拥有非常严格的静态类型系统能够暴露出你在偏应用中可能犯的所有错误。

Scala与指令式语言如Java关系近得多,在这些语言中没有应用所有参数的方法会被认为是 错误的。进一步说,子类型推断的面向对象的传统和全局的根类型接受一些被经典函数式 语言认为是错误的程序。

举例来说,如果你误以为List的drop(n: Int)方法如tail(),那么你会忘记你需要 传递给drop一个数字。你或许会写,println(drop)。如果Scala采用偏应用函数在 任何地方都OK的经典函数式传统,这个代码就将通过类型检查。然而,你会惊奇地发现这个 println语句打印的输出将总是<function>!可能发生的事情是表达式drop将被看作 是函数对象。因为println可以带任何类型对象,这个代码可以编译通过,但产生 出乎意料的结果。

为了避免这样的情况,Scala需要你指定显示省略的函数参数,尽管标志简单到仅用一个_ 。Scala允许你仅在需要函数类型的地方才能省略这个仅用的_。

把偏应用函数赋值给变量

对于一个函数:

scala> def double(x: Int): Int = x * 2
double: (x: Int)Int

如果要赋值给变量,需要声明变量的类型为函数:

scala> val d1: (Int) => Int = double
d1: Int => Int = <function1>

scala> d1(2)
res0: Int = 4

也可以不声明变量的类型,但用通配符(_)表示参数列表。 这样就可以自动推导出变量类型为函数:

scala> val d2 = double _
d2: Int => Int = <function1>

scala> d2(4)
res1: Int = 8

对于没有参数的函数,用()表示变量类型中的参数列表 (()正好也是Unit类型的字面量):

scala> def logStart() = "=" * 50 + "\nStarting NOW\n" + "=" * 50
logStart: ()String

scala> val start: () => String = logStart
start: () => String = <function0>

scala> start()
res2: String =
==================================================
Starting NOW
==================================================

结合柯里化特性,可以把多个参数列表中的一个参数列表用通配符表示:

scala> val c = fun01(1, _: Int)
c: Int => Int = <function1>

scala> def fun01(a: Int)(b: Int) = a + b
fun01: (a: Int)(b: Int)Int

scala> val c = fun01(1) _
c: Int => Int = <function1>

scala> c(2)
res4: Int = 3

偏函数

偏函数(Paritial Function)并不是对所有输入的参数都有定义。 偏函数是类型PartialFunction[A, B]的一个实例,其中A是参数类型, B是返回类型。

  • 偏函数中只能使用case语句,而且整个函数必须用花括号包围, 不能像普通的函数字面量一样使用小括号。
  • 如果输入的参数不能匹配函数内的逻辑,系统会抛出MatchError

偏函数定义域

如果要检查一个偏函数是否有定义,一定要告诉编译器正在使用的函数是偏函数。类型 Lint[Int] => Int包含了不管是否是偏函数的,从整数列表到整数的所有函数。仅包含 整数列表到的偏函数的,应该写成Partialfunction[List[Int],Int]

下面是偏函数的定义例子:

val second: PartialFunction[List[Int],Int] = {
  case x :: y :: _ => y
}

偏函数有一个isDefineAt方法来测试函数对某个值是否有定义。以这个例子来说,对于 至少两个元素的列表是有定义的:

scala> second.isDefinedAt(List(5,6,7))
res27: Boolean = true

scala> second.isDefinedAt(List())
res28: Boolean = false

Scala在编译器在把这样的表达式转为偏函数时会对模式进行两次翻译:一次是真实函数的 实现;另一次是测试函数是否对参数有定义的实现。例如上面的函数

{case x :: y :: _ => y}
// trans to

会被翻译成:

new PartialFunction[List[Int], Int] {
  def apply(xs: List[Int]) = xs match {
    case x :: y :: _ => y
  }
  def isDefinedAt(xs: List[Int]) = xs match {
    case x :: y :: _ => true
    case _ => false
  }
}

这只有在声明类型为PartialFunction时才会发生。如果只是Function1或没有声明, 函数字面量会编译为完整的函数。

偏函数可能会引起运行时的异常,所以在调用前用isDefineAt检查一下。

串连偏函数

通过orElse函数可以把多个偏函数「链式」串连成一个新的函数,处理多种输入: val pf = pf1 orElse pf2 orElse pf3 ...

例:串起办能处理字符串和双精度数的两个函数,能同时处理字符串与双精度数:

scala> val pf1: PartialFunction[Any, String] = { case s: String => "YES" }
pf1: PartialFunction[Any,String] = <function1>

scala> val pf2: PartialFunction[Any, String] = { case d: Double => "YES" }
pf2: PartialFunction[Any,String] = <function1>

scala> val pf = pf1 orElse pf2
pf: PartialFunction[Any,String] = <function1>

输出结果,偏函数有定义返回YES没有定义时返回ERROR

scala> def tryPF(x: Any, f: PartialFunction[Any, String]): String = try {
     |   f(x).toString } catch { case _: MatchError => "ERROR!" }
tryPF: (x: Any, f: PartialFunction[Any,String])String

检查一个偏函数是不是有定义:

scala> def d(x: Any, f: PartialFunction[Any, String]) = 
     |   f.isDefinedAt(x).toString
d: (x: Any, f: PartialFunction[Any,String])String

显示函数在不同输入时的结果:

scala> println("      |    pf1 - String   | pdf2 - Double  |   pf - All  ")
scala> println("x     | def?    | pf1(x)  | def? | pf2(x)  | def? | pf(x)")

scala> List("str", 3.14, 10) foreach { x =>
     |   printf("%-5s | %-5s | %-6s | %-5s | %-6s | %-5s | %-6s\n",
     |     x.toString, d(x, pf1), tryPF(x, pf1), d(x, pf2), tryPF(x, pf2),
     |     d(x, pf), tryPF(x, pf))
     | }
str   | true  | YES    | false | ERROR! | true  | YES   
3.14  | false | ERROR! | true  | YES    | true  | YES   
10    | false | ERROR! | false | ERROR! | false | ERROR!

偏函数与偏应用函数的区别

  • 偏函数(Partial Fuction)是指函数并不一定能处理所有可能的参数。
  • 偏应用函数(Partial Appled Function)是指调用函数时只用了部分参数, 在接下来的过程中可能会传入其余的参数。

闭包(Closures)

函数不仅可以用到参数:

  (x: Int) => x + more  // how much more? 

more是个自由变量(free variable),因为函数文本自身没有给出其含义。相对的, x变量是一个绑定变量(bound variable),因为它在函数的上下文中有明确意义:

被定义为函数的唯一参数,一个Int。如果你尝试独立使用这个函数文本,范围内 没有任何more的定义,编译器会报错说:

  scala> (x: Int) => x + more
  <console>:5: error: not found: value more
         (x: Int) => x + more
                         ^ 

另一方面,只要有一个叫做more的什么东西同样的函数文本将工作正常:

  scala> var more = 1
  more: Int = 1

  scala> val addMore = (x: Int) => x + more
  addMore: (Int) => Int = <function>

  scala> addMore(10)
  res19: Int = 11 

依照这个函数文本在运行时创建的函数值(对象)被称为闭包(closure)。名称源自于 通过「捕获」自由变量的绑定对函数文本执行的「关闭」行动。

不带自由变量的函数文本,如(x: Int) => x + 1,被称为封闭区间(closed term), 这里术语:term指的是一小部分源代码。因此依照这个函数文本在运行时创建的函数值 严格意义上来讲就不是闭包,因为(x: Int) => x + 1在编写的时候就已经封闭了。

任何带有自由变量的函数文本,如(x: Int) => x + more,都是开放区间(open term) 。因此,任何依照(x: Int) => x + more在运行期创建的函数值将必须捕获它的自由变量 more的绑定。由于函数值是关闭这个开放术语(x: Int) => x + more的行动的 最终产物,得到的函数值将包含一个指向捕获的more变量的参考,因此被称为闭包。

如果more在闭包创建之后被改变了闭包会反映这个变化。如下:

  scala> more = 9999
  more: Int = 9999

  scala> addMore(10)
  res21: Int = 10009 

直觉上,Scala的闭包捕获了变量本身,而不是变量指向的值。相对的,Java的内部类根本 不允许你访问外围范围内可以改变的变量,因此到底是捕获了变量还是捕获了它当前具有 的值就没有差别了。

反过来也同样。闭包对捕获变量作出的改变在闭包之外也可见:

  scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
  someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

  scala> var sum = 0
  sum: Int = 0

  scala> someNumbers.foreach(sum +=  _)

  scala> sum
  res23: Int = -11 

上面的代码中变量sum不在函数字面量sun += _(完整形式为:(x) => sum += x) 里,所以这也是一个闭包的应用。

对于会有不同实例的场景,如:本地变量,闭包会对应到创建时关联的那个变量。

例如,以下是创建和返回「递增」闭包的函数:

  def makeIncreaser(more: Int) = (x: Int) => x + more 

每次函数被调用时都会创建一个新闭包。每个闭包都会访问闭包创建时外部的more变量。

  scala> val inc1 = makeIncreaser(1)
  inc1: (Int) => Int = <function>

  scala> val inc9999 = makeIncreaser(9999)
  inc9999: (Int) => Int = <function> 

结果依赖于闭包被创建时more是如何定义的:

  scala> inc1(10)
  res24: Int = 11

  scala> inc9999(10)
  res25: Int = 10009 

尽管本例中more是一个已经返回的方法调用的参数也没有区别。Scala编译器在这种 情况下重新安排了它以使得捕获的参数继续存在于堆中,而不是堆栈中,因此可以保留在 创建它的方法调用之外。这种重新安排的工作都是自动处理的,程序员可以随意捕获想要的 变量:val,var,或参数。

尾递归(tail recursive)

当函数递归时调用自己这个动作正好是整个函数的最后一个动作时,Scala编译器可以对其 进行优化。这被称为尾递归。

Scala编译器检测到尾递归就用新值更新函数参数,然后把它替换成一个回到函数开头的 跳转。这样减小了递归调用的开销。简单地说,就是每次调用不会用新的栈,而是在原来 的框架内。

递归经常是比基于循环的更优美和简明的方案。如果方案是尾递归,就无须付出任何运行期 开销。

声明尾递归优化

在函数定义前加上@annotation.tailrec注解表示希望编译器进行尾递归优化。 如果该函数无法被优化则无法通过编译:

scala> @annotation.tailrec
     | def power(x: Int, n: Int): Long = {
     |   if (n < 1) 1
     |   else x * power(x, n-1)
     | }
<console>:10: error: could not optimize @tailrec annotated method power: it contains a recursive call not in tail position
         else x * power(x, n-1)
                ^

调整的方案就是把最后一步乘法操作提前,并把要保存的结果作为参数传给下一次调用:

scala> @annotation.tailrec
     | def power(x: Int, n: Int, t: Int = 1): Long = {
     |   if (n < 1) t
     |   else power(x, n-1, x * t)
     | }
power: (x: Int, n: Int, t: Int)Long

scala> power(2, 8)
res1: Long = 256

跟踪尾递归函数

尾递归函数将不会为每个调用制造新的堆栈框架;所有的调用将在一个框架内执行。所以在 调试的时候会比较怪。

例如,这个函数调用自身若干次之后抛出一个异常:

  def boom(x: Int): Int = 
    if (x == 0) throw new Exception("boom!")
    else boom(x - 1) + 1 

这个递增操作函数不是尾递归,所以它显示的堆栈信息看起来很正常:

  scala>  boom(3)
  java.lang.Exception: boom!
  	at .boom(<console>:5)
  	at .boom(<console>:6)
  	at .boom(<console>:6)
  	at .boom(<console>:6)
  	at .<init>(<console>:6)
  ...

如果你现在修改了boom从而让它变成尾递归:

 def bang(x: Int): Int = 
   if (x == 0) throw new Exception("bang!")
   else bang(x - 1) 

堆栈信息看起来是这样的:

  scala> bang(5)
  java.lang.Exception: bang!
  	at .bang(<console>:5)
  	at .<init>(<console>:6)
  ... 

这回,你仅看到了bang的一个堆栈框架。或许你会认为bang在调用自己之前就崩溃了,但 这不是事实。

可以给scala-shell或者scalac编译器加上参数-g:notailcalls 关掉尾递归。这样就能 得到一个长长的堆栈跟踪了:

  scala> bang(5)
  java.lang.Exception: bang!
  	at .bang(<console>:5)
  	at .bang(<console>:5)
  	at .bang(<console>:5)
  	at .bang(<console>:5)
  	at .bang(<console>:5)
  	at .bang(<console>:5)
  	at .<init>(<console>:6)
  ... 

尾递归的局限

Scala里尾递归的使用局限很大,因为JVM指令集使实现更加先进的尾递归形式变得很困难。 Scala仅优化了直接递归调用使其返回同一个函数。如果递归是间接的,就像在下面的例子 里两个互相递归的函数,就没有优化的可能性了:

  def isEven(x: Int): Boolean =
    if (x == 0) true else isOdd(x - 1)
  def isOdd(x: Int): Boolean =
    if (x == 0) false else isEven(x - 1) 

同样如果最后一个调用是一个函数值你也不能获得尾调用优化。如下列代码:

  val funValue = nestedFun _
  def nestedFun(x: Int) { 
    if (x != 0) { println(x); funValue(x - 1) }
  } 

funValue变量指向一个实质是包装了nestedFun的调用的函数值。当你把这个函数值 应用到参数上,它会转向把nestedFun应用到同一个参数,并返回结果。

因此你或许希望Scala编译器能执行尾调用优化,但在这个例子里做不到。因此,尾调用 优化受限于方法或嵌套函数在最后一个操作调用本身,而没有转到某个函数值或什么其它的 中间函数的情况。

蹦床

对于消除递归,有一个比尾递归更加通用的机制叫作「蹦床」。它通过一个循环不断地调用 函数,每个函数都返回一下要被调用的函数。尾递归其实是蹦床的一个特例:每个函数都 返回它自己。

Scala的TailCalls工具对象帮助实现蹦床。相互递归的函数返回类型为TailRec[A]。 其值为done(result)tailcall(fun)二者之一。其中fun代表下一个要被调用的函数 ,这个函数不能有额外的参数而且返回类型为TailRec[A]的函数。

例子:

scala> import scala.util.control.TailCalls._
import scala.util.control.TailCalls._

scala> object TailCallDemo {
     |   def evenLength(xs: Seq[Int]): TailRec[Boolean] =
     |     if (xs.isEmpty) done(true) else tailcall(oddLength(xs.tail))
     |  
     |   def oddLength(xs: Seq[Int]): TailRec[Boolean] =
     |     if (xs.isEmpty) done(false) else tailcall(evenLength(xs.tail))
     |  
     | }
defined object TailCallDemo

调用时,用result方法取得结果:

scala> TailCallDemo.evenLength(1 to 1000000000).result
res5: Boolean = true