Jade Dungeon

定界延续

定界延续

捕获并执行延续

延续可以让程序回到之间的一个位置上。比如IO操作异常时发现文件不存在:

contents = scala.io.Souce.fromFile(filename, "UTF-8").mkString

可以通过延续回到失败点然后重试。

要实现这一效果,首先要通过shift结构捕获一个延续,然后指定对这个捕获到的延续 进行什么操作。比如说,把这个延续保存下来:

var cont: (Unit => Unit) = null
...
shift { k: (Unit => Unit) => // 延续被传递给了shift
	cont = k // 保存下来备用
}

这里用到的延续是一个不带参数也不带返回值的函数(严格地说,参数和返回值都是Unit 类型),以后会看到有带参数和返回值的延续。

以后要回到shift这个位置,需要执行这个延续:做法就是简单地调用cont

定界

在Scala中,延续是定界的:它只能延展到指定的边界。这个边界由reset {...}标出:

reset {
	...
	shift { k: (Unit => Unit) =>
		count = k
	} // 对count的调用将从这里开始
	...
}   // 到这里结束

当调用count时,执行将从shift处开始,蒋一直延展到reset块的边界。

例子,读一个文件并捕获延续:

import scala.util.continuations._

object Main extends App {
  var cont : (Unit => Unit) = null  
  var filename = "myfile.txt"
  var contents = ""

  reset {
    while (contents == "") {
      try {
        contents = scala.io.Source.fromFile(filename, "UTF-8").mkString
      } catch { case _ => }
      shift { k : (Unit => Unit) => 
        cont = k 
      }
    }
  }
  
	// 如果要重试的话,只要执行延续即可:
  if (contents == "") {
    println("Try another filename: ");
    filename = readLine
    cont()
  }
  println(contents)
}

在Scala 2.9中,要启动延续插件才可以编译使用了延续的程序:

scalac -P:continuations:enable Continuations.scala

延续捕获的原理

可以把shift块想象成一个位于reset块中的空位。当执行延续时可以把一个值传到这个 空位里,运算继续,就好像shift本身就是哪个值一样。

例如:

  var cont : (Int => Double) = null  
  reset {
    0.5 * shift { k: (Int => Double) => { cont = k } } + 1
  }

把整个shift替换成一个空位:

    0.5 * /* hole */ + 1

当调用cont(3)时,这个位置被填上值3

也就是说cont可以被看作这样一个函数:

x: Int => 0.5 * x + 1

延续的类型为Int => Double,因为填入的类型为Int并计算出Double

reset和shift的控制流程

import scala.util.continuations._

object Main extends App {
  var cont : (Unit => Unit) = null  
  reset { 
    println("Before shift")
    shift { 
        k : (Unit => Unit) => { 
          cont = k 
          println("Inside shift")
        } 
    }
    println("After shift")
  }
  println("After reset")
  cont()
}

执行的顺序:

Before shift  // reset执行时
Inside shift
After reset   // 退出reset块
After shift   // 调用cont时,执行跳回reset

shift之前的代码不是延续的一部分。延续将从包含shift的表达式(该表达式会变成那个 空位)开始,一直延展到reset的末尾。以来例来说就是:

/* hole */ : Unit => /* hole */ ;
print("After shift")

cont()方法参数为Unit,这里的空位只是简单地被替换成了()

从这里可以看出,reset中的shift会立即跳出reset。当执行一个跳入reset的延续时,如果 再次遇到shift(在循环中的话会遇到同一个shift),它同样会立即跳出reset。函数调用 也会立即退出,返回shift的值。

reset表达式的值

如果是因为执行了shift而退出了reset,那么得到的值就是shift的值:

val result1 = reset { shift { k: (String => String) => "Exit" }; "End" }
println(result1) // result is "Exit" 

如果没有执行到shift而是reset执行到结尾,值就是reset块的值(即块中的最后一个 表达式):

val result2 = reset { 
	if (false)
		shift { k: (String => String) => "Exit" };
	else
		"End" 
}
println(result2) // result is "End"

reset和shift表达式的类型

类型分别是reset[B, C]shift[A, B, C]。可以这样看:

reset {

	// shift之前
	
	shift { k: (A => B) => // 由这里推断A与B

		// shift中 类型C

	} // shift代表的空位,类型为A

	// shift之后的部分,必须产出类型B的值

}

如果reset块可能返回一个类型为B或C的值(由于分支或循环的原因),那么B必须是C的 子类型。

这里如果编译器无法正确判断类型,会引发错误。

正确的代码:

val result = reset { 
  if (scala.util.Random.nextBoolean()) {
    shift { 
      k: (String => String) => { // A与B都是String
        "Exit"                   // C是String
      }
    }                            // 空位的类型同是String
  } 
  else "End"                     // 和B一样是String
}

错误的代码:

val result = reset { 
  if (scala.util.Random.nextBoolean()) {
    shift { 
      k: (Unit => Unit) => {     // A与B都是Unit
        "Exit"                   // C是String
      }
    }                            // 空位的类型同A一样是Unit
  } 
  else "End"                     // 错:应该和B一样是Unit,但这里是String
}

提示:如果推断出的类型不符合期望,可以给reset与shift添加类型参数。如把延续类型 改为Unit => Any,并用reset[Any, Any]来让上面的代码通过编译。

当类型推断错误时报错信息可能会非常难以理解,很容易进入瞎改类型赶到能正确编译的 状态中。尽量不要这样做,仔细考虑延续发生时,希望的行为是什么。虽然说起来很抽象, 但是在解决实际问题时程序员应该知道当调用延续时想传入的类型与结果的类型。这样可 确定A与B的类型。再适当组织代码让C与B相等。这样无论reset是如何退出的都会产出B类型 。

CPS注解

某些虚拟机中,延续实现方式是抓取运行时栈的快照,在调用延续时恢复。

JVM不允许这样的操作,所以Scala编译器对reset块中的代码进行了「延续传递风格」(CPS) 的变换。

经过CPS变换的方法与常规的Scala方法不一样,不能混用。所以如果一个普通的方法包含 了shift,就要加上注释。

更明确地说,reset和shift之间不能隔着方法,不然这个方法就要加上注释。如: reset块里有一个或多个方法,方法里用shift块,那中间的方法就都要加上注释。

这样看起来很麻烦,但这些注解本来不是给开发应用的程序员用的,而是给设计类库的开发 人员设计特殊控制流程结构的。不应该让外部的人看到。

至于注解如何使用,联系之前说过shift方法的类型为shift[A, B, C],则:

  • 使用注解时,要声明@cpsParam(B, C)
  • 如果BC类型相同时可以用@cps[B]
  • @cps[Unit]可以写为@suspendable。但还不如@cps[Unit]简洁明了,所以很少用。

例,在一个循环读文件的方法中使用shift块必须要加CPS注解声明:

def tryRead(): Unit @cps[Unit] = {
  while (contents == "") {
    try {
      contents = scala.io.Source.fromFile(filename, "UTF-8").mkString
    } catch { case _ => }
    shift { k : (Unit => Unit) => 
      cont = k 
    }
  }
}

注意:给方法添加注解时,必须指定返回类型,并且必须要加上=(哪怕是返回Unit), 这是注解语法的限制,与延续无关。

把递归访问转为迭代

例,递归遍历树形结构,如目录下所有文件:

def processDirectory(dir: File) {
	val files = dir.listFiles
	for (f <- files) {
		if (f.isDirectory)
			processDirectory(f)
		else
			println(f)
	}
}

递归时不能控制在读到100个文件时跳出。但用延续就简单了:每读到一个文件就跳出递归 如果不满100个就再跳回去。实现方案是在需要中断的点放一个shift:

if (f.isDirectory)
	processDirectory(f)
else {
	shift {
		k: (Unit => Unit) => { cont = k }
	}
	println(f)
}

这里的shit有两个作用:

  1. 每当执行时跳到reset的末尾,还要捕获延续,这样以后才能跳回来。
  2. 把整个过程的启动点用reset包起来,然后就可以用需要的调用的次数来调用捕获到的 延续了
reset {
	processDirectory(new File(rootDirName))
}
for (i <- 1 to 100) cont()

还要加上CPS注解:

def processDirectory(dir : File) : Unit @cps[Unit]

还有一个问题:for循环会被翻译成一个foreach()方法调用,这是一个没有被注解为 CPS变换的方法。只能用while循环来代替foreach()方法调:

var i = 0
while (i < files.length) {
	val f = file(i)
	i += 1
	...
}

完整的程序:

// Compile as scalac -P:continuations:enable PrintFiles.scala
import scala.util.continuations._
import java.io._

object PrintFiles extends App {

  var cont : (Unit => Unit) = null

  def processDirectory(dir : File) : Unit @cps[Unit] = {
    val files = dir.listFiles
    var i = 0
    while (i < files.length) {
      val f = files(i)
      i += 1
      if (f.isDirectory)
        processDirectory(f)
      else {
        shift {
          k: (Unit => Unit) => {
            cont = k                // ➋
          }
        }                           // ➎
        println(f)
      }
    }
  }

  reset {
    processDirectory(new File("/")) // ➊
  }                                 // ➌

  for (i <- 1 to 100) cont()        // ➍
}
  1. 进入reset块时,processDirectory()被调用➊。
  2. 一旦该方法找到第一个不是目录的文件,就进入shift块➋。
  3. 延续函数被保存到cont,程序跳到reset块的末尾➌。
  4. cont()被调用➍。程序重新跳加递归➎。
  5. 递归继续,赶到下一个文件被找到,再次进入shift。

控制反转

场景:两个页面上有两个表单,分别让用户填姓和名,这种情况下如果可以直接这样写会 很方便:

val firstname = getResponse(page1)
val lastname = getResponse(page2)

但http Web应用下的请求响应是无状态的,而且流程是不受控制的。但基于延续的Web框架 可以解决这样的问题:

在第一个页面等待用户响应时保留下延续,等响应到来以后再调用延续。而这一切对于程序 开发人员是透明的。

为简单起见先用一个GUI程序来说明,在第一个界面让用户输入first name,然后用户点 下一步按钮,调用到getResponse()方法。

def run() {
  reset {
    val response1 = getResponse("What is your first name?") // ➊
    val response2 = getResponse("What is your last name?")
    process(response1, response2)                           // ➎
  }
}                                                           // ➌

def process(s1: String, s2: String) {
  label.setText("Hello, " + s1 + " " + s2)
}

注意process()方法并没有包含shift,所以不用加上CPS注解。CPS注解要加在捕获延续的 getResponse()方法上:

def getResponse(prompt: String): String @cps[Unit] = {
  label.setText(prompt)
  setListener(button) { cont() }
  shift {
    k: (Unit => Unit) => {
      cont = k                     // ➋
    }                              // ➍
  }
  setListener(button) { }
  textField.getText
}
  • run()方法中的业务逻辑被包在了一个reset块中:当第一次调用getResponse()时➊ ,它将进入shift块➋,捕获到延续,然后返回到reset块的末尾➌,退出run()方法。
  • 当用户输入first name并点下一步时,按钮的事件处理器会执行延续,程序从➍开始 继续,用户输入被返回给run()方法。run()方法第二次调用getResponse(), 又会在延续被捕获时退出。
  • 用户输入last name并点一下步,结果被送到run()方法并传递给process()

有趣的是整个活动都在事件分发线程中完成,而且没有阻塞。为了展示这个效果,可以直接 在按钮监听器中启动run()方法。

完整的代码:

// Compile as scalac -P:continuations:enable InvControl.scala

import java.awt._
import java.awt.event._
import javax.swing._
import scala.util.continuations._

object Main extends App {
  val frame = new JFrame
  val button = new JButton("Next")
  
  setListener(button) { run() }

  val textField = new JTextArea(10, 40)
  textField.setEnabled(false)
  val label = new JLabel("Welcome to the demo app")
  frame.add(label, BorderLayout.NORTH)
  frame.add(textField)
  
  val panel = new JPanel
  panel.add(button)
  frame.add(panel, BorderLayout.SOUTH)
  frame.pack()
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
  frame.setVisible(true)

  def run() {
    reset {
      val response1 = getResponse("What is your first name?") // ➊
      val response2 = getResponse("What is your last name?")
      process(response1, response2)                           // ➎
    }
  }                                                           // ➌

  def process(s1: String, s2: String) {
    label.setText("Hello, " + s1 + " " + s2)
  }

  var cont: Unit => Unit = null

  def getResponse(prompt: String): String @cps[Unit] = {
    label.setText(prompt)
    setListener(button) { cont() }
    shift {
      k: (Unit => Unit) => {
        cont = k                                               // ➋
      }                                                        // ➍
    }
    setListener(button) { }
    textField.getText
  }

  def setListener(button: JButton)(action: => Unit) {
    for (l <- button.getActionListeners) button.removeActionListener(l)
    button.addActionListener(new ActionListener {
      override def actionPerformed(event: ActionEvent) { action }
    })
  }
}

CPS变换

前面说过JVM不支持延续,所以Scala通过CPS来实现延续。CPS变换会产出一些对象,这些 对象会指定如何处理「剩下的运算」的函数。如下面的shift方法:

shift { 函数 }

shift的代码体是一个(A => B) => C的函数,它的参数类型是A => B的延续作为函数, 返回值为C,这个值被传递到包含延续的reset块之外。所以shift会返回一个对象:

ControlContext[A, B, C](函数)

这里的控制上下文(ControlContext)已经简化处理过了,真实的版本还包含了异常处理与 常量优化等内容。

这个上下文描述了如何处理延续函数。它可能会把函数推到一边;也有可能会计算出结果。 上下文并不知道如何处理延续函数,它只是预期接收延续,计算要依赖其他人。

延续是必须被计算,要计算的内容可以分为我们知道的内容(用f表示)和不知道的内容 (用k1表示)。为了简化问题,直接用上f,这样只需要假定其他人会算完k1再给 我们。这样就可以先应用f再应用k1来完成整个运算。

把上面的想法对应到上下文中:shift被翻译成一个知道如何处理shift之后所有事情的 上下文。现在f到了shift以后。可以再做出一个新的控制上下文来处理f之后余下的 运算。像是这样:

new ControlContext(k1 => fun(a => k1(f(a))))

a => k1(f(a))先运行f()然后再完成k1指定的运算。而fun则按通常的方法处理 去处的结果。

以上是控制上下文的基本操作,被称为map。以下是map()方法的定义:

class ControlContext[+A, -B, +C] (val fun: (A => B) => C) {

	def map[A1] (f: A => A1) = new ControlContext[A1, B, C] (
			(k1: (A1 => B)) => fun(x: A => k1(f(x)))
		)

}

这里的map()方法和后面会涉及的flatMap()方法与映射并没有关系。叫map这个名字 是因为与映射方法一样遵从单子法则(monad laws)。这个法则就不解释了,扯远了。

map方法看起来很复杂,但用起来很直观:cc.map(f)接收一个上下文,产生一个处理 过f之后的新的上下文。这样一级一级传下去,直到所有的f都被处理过了,没有什么 要处理的了就是最终状态。这就是抵达reset边界的时候,只要把一个什么都不做的方法 传给fun()。就得到最终的shift结果。

reset正是这样定义的:

def reset[B, C] (cc: ControlContext[B, B, C]) = cc.fun(x => x)

看一个简单的例子:

 var cont : (Int => Double) = null  
 
 reset {
   0.5 * shift { 
       k: (Int => Double) => { 
         cont = k 
       } 
   } + 1
 }

这时只要一步就可以计算出整个延续:

 => 0.5 * /* hole */ + 1

所以,我们得到了:

reset {
	new ControlContext[Int, Double, Unit] (k => cont = k).map(
			/* hole */ => 0.5 * /* hole */ + 1
	)
}

即:

reset {
	new ControlContext[Double, Double, Unit] (
			k1 => cont = k1(x: Int => 0.5 * x + 1)
	)
}

这样reset就可以求值了,k1是一个恒等函数,结果为:

cont = x: Int => 0.5 * x + 1

可以通过-Xprint:selectivecps编译参数看到CPS变换生成的代码:

// Compile as scalac -P:continuations:enable -Xprint:selectivecps Continuations.scala

import scala.util.continuations._

object Main extends App {
  
  var cont : (Int => Double) = null  
  reset {
    0.5 * shift { 
        k: (Int => Double) => { 
          cont = k 
        } 
    } + 1
  }
  println(cont(10))
  println(cont(20))
}


/*
[[syntax trees at end of selectivecps]]// Scala source: cont3.scala
package <empty> {
  final object Main extends java.lang.Object with App with ScalaObject {
    def this(): object Main = {
      Main.super.this();
      ()
    };
    private[this] var cont: Int => Double = null;
    <accessor> def cont: Int => Double = Main.this.cont;
    <accessor> def cont_=(x$1: Int => Double): Unit = Main.this.cont = x$1;
    scala.util.continuations.`package`.reset[Double, Unit]({
      package.this.shiftR[Int, Double, Unit](((k: Int => Double) => Main.this.cont_=(k))).map[Double](((tmp1: Int) => 0.5.*(tmp1).+(1)))
    });
    scala.this.Predef.println(Main.this.cont.apply(10));
    scala.this.Predef.println(Main.this.cont.apply(20))
  }
}
*/

转换嵌套的控制上下文

如果有多层CPS嵌套的话就复杂了,比如把递归转为迭代的场景。

递归访问树太复杂了,这里以访问一个链表为例:

var cont: Unit => String = null

def visit(a: List[String]): String @cps[String] = {
  if (a.isEmpty) "" else {
    shift {
      k: (Unit => String) => { 
        cont = k
        a.head
      }
    }
    visit(a.tail)
  }
}

shift生成的上下文和前一个为:

new ControlContext[Unit, String, String] (k => { cont = k; a.head }

但除了shift以外,还有一个visit()调用,它也要生成一个上下文。虽然visit()方法 的返回类型是String,但@cps注解让编译器经过CPS变换以后实际返回的类型成了 ControlContext

更加确切地说,shift被替换成了(),因为延续函数的参数类型为Unit。这样一下, 剩下的运算就是:

() => visit(a.tail)

前一节中我们会把这个函数作为参数调用map()方法,但由于它的返回类型是一个控制 上下文,所以我们要调用flatMap()

if 
	(a.isEmpty) new ControlContext(k => k(""))
else 
	new Controlcontext(k => { cont = k; a.head }).flatMap(() => visit(a.tail))

flatMap()的定义为:

class ControlContext[+A, -B, +C] (val fun: (A => B) => C) {

	def flatMap[A1, B1, C1 <: B] (f: A => Shift[A1, B1, C1]) = 
		new ControlContext[A1, B1, C] (
			(k1: (A1 => B1)) => fun(x: A => f(x).fun(k1))
		)
}

以上代码的大概意思是:如果剩下的运算是由另一个想要处理剩下运算的上下文开始的话, 就让它开始做。这将定义出一个延续,由我们来处理。

注意类型界定C1 <: B。这是因为f(x).fun(k1)的类型为C1,但是fun()的参数类型 是A => B的函数。

现在来模拟一次调用:

val lst = List("Fred")
reset { viset(lst) }

由于lst不为空,所以得到:

reset {
	new Controlcontext(k => { cont = k; a.head }).flatMap(() => visit(a.tail))
}

根据flatMap()的定义,可以得到:

reset {
	new Controlcontext(
		k => {
			cont = () => visit(a.tail).fun(k1);
			lst.head 
		}
	)
}

然后reset把k1设为恒等函数,我们得到:

cont = () => visit(lst.tail).fun(x => x)
lst.head

如果是更长的列表的话会得到同样的结果,不同的是visit(lst.tail.tail)。本例的列表 因为只有一个元素已经调用完了,visit(lst.tail)将返回:

new ControlContext(k => k(""))

应用恒等函数,得到结果""。这里用空字符串作为返回结果显得比较造作,但是因为 cont预期返回一个类型为String的值,所以不能用Unit

全部代码如下:

// Compile as scalac -P:continuations:enable -Xprint:selectivecps Visig.scala

import scala util.continuations._

object Main extends App {
  var cont: Unit => String = null

  def visit(a: List[String]): String @cps[String] = {
    if (a.isEmpty) "" else {
      shift {
        k: (Unit => String) => { 
          cont = k
          a.head
        }
      }
      visit(a.tail)
      println("After visit")
      ""
    }
  }

  reset {
    visit(List("Mary", "had", "a", "little", "lamb"))
  }
  println(cont())
  println(cont())
}

/*

[[syntax trees at end of selectivecps]]// Scala source: cont11.scala
package <empty> {
  final object Main extends java.lang.Object with App with ScalaObject {
    def this(): object Main = {
      Main.super.this();
      ()
    };
    private[this] var cont: Unit => String = null;
    <accessor> def cont: Unit => String = Main.this.cont;
    <accessor> def cont_=(x$1: Unit => String): Unit = Main.this.cont = x$1;
    def visit(a: List[String]): scala.util.continuations.ControlContext[String,String,String] = if (a.isEmpty)
      package.this.shiftUnitR[java.lang.String(""), String]("")
    else
      {
        val tmp1$shift: scala.util.continuations.ControlContext[Unit,String,String] = package.this.shiftR[Unit, String, String](((k: Unit => String) => {
          Main.this.cont_=(k);
          a.head
        }));
        if (tmp1$shift.isTrivial)
          {
            val tmp1: Unit = tmp1$shift.getTrivialValue;
            tmp1;
            Main.this.visit(a.tail)
          }
        else
          tmp1$shift.flatMap[String, String, String](((tmp1: Unit) => {
            tmp1;
            Main.this.visit(a.tail)
          }))
      };
    scala.util.continuations.`package`.reset[String, String](Main.this.visit(immutable.this.List.apply[java.lang.String]("Mary", "had", "a", "little", "lamb")));
    scala.this.Predef.println(Main.this.cont.apply(()));
    scala.this.Predef.println(Main.this.cont.apply(()))
  }
}

*/

抛出的异常

try-catch

Scala不检查抛出的异常是否被捕获。也就是说Scala没有throws声明。所以所有的Scala 都被翻译成不抛出任何异常的Java方法。这样做的原因是throws声明就是为了强制开发 人员一定要处理异常。但是的很多开发人员写Java在捕获了以后也不处理,这样语法上虽然 过了但是等于没有抛出声明。比如下面这样的catch块里一句语句也没有:

try {
	...
} catch (IOException e) {
	// do nothing
}

这样异常没有处理,反而还给代码包了一层try-catch。Scala为了代码干净就直接不声明 抛出异常了。

但是为了和Java程序对接,声明一下会抛出哪些异常还是有必要的。不然Java代码就不能 捕获可能抛出的异常。所以通过注解标签@throws来说明:

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

从Java看来是这个样子的:

  $ javap Reader
  Compiled from "Reader.scala"
  public class Reader extends java.lang.Object implements
  scala.ScalaObject{
      public Reader(java.lang.String);
      public int read()       throws java.io.IOException;
      public int $tag();
  }
  $

这样Java中调用Scala代码时才能捕获异常:

try {
	reader.read();
} catch (IOException e) {
	// ...
}

Scala里对异常的捕获:

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  
}  

finally子句:

import java.io.FileReader  
val file = openFile()  
try {  
	// 使用文件  
} finally {  
	file.close() // 确保关闭文件  
}  

有返回值:

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

  • 如果没有异常抛出,则对应于try子句;
  • 如果抛出异常并被捕获,则对应于相应的catch子句;
  • 如果异常被抛出但没被捕获,表达式就没有返回值。
  • 如果finally子句计算得到的值被抛弃。
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"
							)  
}

上面代码展示了如何尝试拆分URL,但如果URL格式错误就使用缺省值。

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

如:

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

调用f()产生结果值2,因为finally虽然不返回值,但是和Java一样它的内容return 2 一定会在最后被执行。

相反:

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

调用g()产生1,这是因为finally的值是被抛弃的,所以表达式的结果是1。

这两个例子展示了有可能另大多数程序员感到惊奇的行为,因此通常最好还是避免从 finally子句中返回值。最好是把finally子句当作确保某些副作用,如关闭打开的文件, 发生的途径。

util.Try

util.Try提供了比try-catch更安全,而且纯一元(monadic)的表述方式。 它有两个子类SuccessFailure

设计一个可能抛出异常的方法loopAndFail

scala> def loopAndFail(end: Int, failAt: Int): Int = {
     |   for (i <- 1 to end) {
     |     println(s"$i")
     |     if (i == failAt) throw new Exception("Too Many Interations")
     |   }
     |   end
     | }
loopAndFail: (end: Int, failAt: Int)Int

scala> loopAndFail(2, 3)
1
2
res7: Int = 2

scala> loopAndFail(10, 3)
1
2
3
java.lang.Exception: Too Many Interations
  at $anonfun$loopAndFail$1.apply$mcVI$sp(<console>:10)
  at scala.collection.immutable.Range.foreach$mVc$sp(Range.scala:166)
  at .loopAndFail(<console>:8)
  ... 33 elided

没有异常返回子类型Success

scala> util.Try(loopAndFail(2, 3))
1
2
res8: scala.util.Try[Int] = Success(2)

有异常返回子类Failure

scala> util.Try(loopAndFail(5, 3))
1
2
3
res9: scala.util.Try[Int] = Failure(java.lang.Exception: Too Many Interations)

常用方法介绍

对于一个可能会抛异常的函数nextError

scala> def nextError = util.Try{ 1 / util.Random.nextInt(2) }
nextError: scala.util.Try[Int]

使用归约函数flatMap可以达到只有在成功的时候才会调用一个也是用util.Try 的函数:

scala> nextError flatMap { _ => nextError }
res6: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)

scala> nextError flatMap { _ => nextError }
res7: scala.util.Try[Int] = Success(1)

注意这里的nextError是没有参数的,所以这里用了_占用参数表的位置。

使用遍历方法foreach可以达到只有在成功的时候才执行指定的操作:

scala> nextError foreach(x => println("success! " + x))

scala> nextError foreach(x => println("success! " + x))
success! 1

使用getOrElse方法可以在成功时调用结果,失败时返回指定的默认值:

scala> nextError getOrElse 0
res15: Int = 1

scala> nextError getOrElse 0
res16: Int = 0

使用orElse函数,可以在失败时返回util.Try的函数(正好和flatmap相反):

scala> nextError orElse nextError
res19: scala.util.Try[Int] = Success(1)

scala> nextError orElse nextError
res20: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)

使用toOption方法把util.Try转为Option类型,Success类型变为SomeFailure类型变为None,但缺点是会丢弃Exception信息:

scala> nextError.toOption
res22: Option[Int] = Some(1)

scala> nextError.toOption
res23: Option[Int] = None

使用map函数可以在成功时调用一个函数,把成功的结果映射为一个新的值:

scala> nextError map (_ * 2)
res32: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)

scala> nextError map (_ * 2)
res33: scala.util.Try[Int] = Success(2)

用模式匹配也可以实现对结果的处理:

scala> nextError match {
     |   case util.Success(x) => x;
     |   case util.Failure(error) => -1
     | }
res34: Int = 1

scala> nextError match {
     |   case util.Success(x) => x;
     |   case util.Failure(error) => -1
     | }
res36: Int = -1

最后,什么也不做也是一种处理办法,如果发生错误会按调用栈向上传播, 赶到最后有被捕获或是应用退出:

scala> nextError
res37: scala.util.Try[Int] = Success(1)

scala> nextError
res38: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)

例子,解析字符串中的数字,orElse尝试从字符串中解析一个数字, 如果成功再用foreach输出操作结果:

scala> val input = " 123"
input: String = " 123"

scala> val result = util.Try(input.toInt) orElse util.Try(input.trim.toInt)
result: scala.util.Try[Int] = Success(123)

scala> result foreach { r => println(s"Parse '$input' to $r!") }
Parse ' 123' to 123!

scala> val x = result match {
     |   case util.Success(x) => Some(x)
     |   case util.Failure(ex) => {
     |     println(s"Couldn't parse input '$input'")
     |     None
     |   }
     | }
x: Option[Int] = Some(123)

结合Scala与Java

在Java中使用Scala

要保证classpath里有scala-librasy.jar。虽然Scala代码被编译成了Java字节码,但 还是要知道编译成了字节码以后长什么样子。

一般性原则

Scala尽可能把Scala特性编译成对等的Java特性,如:类、方法、字符串、异常等。

虽然在运行时进行确定重载方法是一个很好的方案,但是为了和Java的重载一致Scala还是 和Java保持一致使用编译时解析重载。这样Scala的方法与调用方式可以和Java的一致。

但是像特质这样在Java里没有对应的特性就比较麻烦,还有Java和Scala的泛型要细节上是 有冲突的,只能用别的方式解决。注意在不同版本中这样的解决方案会不断的优化改变。 所以可靠的方式还是用javap工具检查.class文件。

值类型

Int这样的值类型会尽量用Java的Int表示。但有些情况下如List[Any]时不能确定用 的是哪一种类型,所以会用Interger这样的包装器类。

高阶函数

Java不支持高阶函数与闭包等特性,所以用不了。如果预计以后要给Java用的话,就再定义 一个不用高阶函数实现的函数给Java代码调用……

单例对象与伴生对象

由于在Java里没有对应的特性,所以采用静态和实例方法结合的方式。每个Scala的单例 对象编译器都会创建一个名称后加美元符号的Java类。对于名为App的单例对象编译器 产出一个名为App$的Java类。这个类拥有Scala单例对象的所有字段和方法,这个Java类 同时还有一个名为MODULE$的静态字段,保存该类在运行期创建的一个实例。

完整的例子:

  object App {
    def main(args: Array[String]) {
      println("Hello, world!")
    }
  }

会生成一个Java类App$

  $ javap App$
  public final class App$ extends java.lang.Object
  implements scala.ScalaObject{
      public static final App$ MODULE$;
      public static {};
      public App$();
      public void main(java.lang.String[]);
      public int $tag();
  }
单例对象

编译器还要为单例对象App自动创建一个叫App的Java类。这个类对于每个Scala单例 对象的方法都有一个静态转发方法与之对应:

  $ javap App
  Compiled from "App.scala"
  public final class App extends java.lang.Object{
      public static final int $tag();
      public static final void main(java.lang.String[]);
  }

在Java中调用单例对象的例子:

object Single {
	def greet() { println("hello") }
}

在Java里调用时就像用静态方法一样:

public class SingleUser {
	public static void main(String [] args) {
		Single.greet()
	}
}
伴生对象

反之如果已经有一个名为App的类了,Scala会创建一个相对应的Java类App来保存定义 App类的成员。在这种情况下就不包含任何转发到同名单例对象的方法,Java代码必须 通过MODULE$字段来访问这个单例。

class Buddy {
	def greet() { println("this is Buddy class") }
}

object Buddy {
	def greet() { println("this is Buddy object") }
}

在Java里调用伴生对象要通过MODULE$

public class BUddyUser {
	public static void main(String [] args) {
		new Buddy().greet();
		Buddy$.MODULE$.greet();
	}
}

作为接口的特质

每个特质都会创建一个同名的Java接口。这个接口可以作为Java类型使用,可以通过这个 接口类型的变量来调用Scala的对象方法。

反过来如果要在Java中建立一个Scala特质的情况非常罕见,但也有特殊情况下需要这样做 。如果Scala特质只有抽象方法的话就直接翻译成Java接口。所以本质上说能Scala语法来 编写Java接口。

举例来说,如果一个特质没有实现方法,那个Java代码里可以把它作为接口来用:

trait Writable {
	def wirte(msg: String) : Unit
}

Java代码里可以实现它:

public class AWritableJavaClass implements Writable {
	public void write(String msg) {}
}

如果特质里有实现:

trait Printable {
	def print() {}
}

那在Java里就不能实现它了,但可以反它作为一个类型,持有它的一个引用。

Scala中调用Java

带泛型的静态方法

Java中定义:

public class RegistryBuilder<I> {
	public static <I> RegistryBuilder<I> create() {
		return new RegistryBuilder<I>();
	}

	RegistryBuilder() {
	}
}

Java中调用:

RegistryBuilder.<ConnectionSocketFactory> create()

Scala中调用:

RegistryBuilder.create[ConnectionSocketFactory]

注解

标准注解的额外效果

有一些注解编译器在针对Java平台编译时会产额外的信息。编译器会首先按Scala原则去 处理,然后针对Java做一些额外的工作。

过期

@deprecated标记的方法或类,编译器会为产的代码添加Java自己的过期注解。所以Java 也会警告过期。

volatile字段

对应到Java里的volatile修饰符。所以这两套机制一样,对volatile字段的访问也完全 根据Java内存模型所规定的volatile字段处理原则来进行排列。

序列化

@serializable被加上Java的Serializable接口。@SerialVersionUID被转成Java的 版本字段:

@SerialVersionUID(42L) class Person extends Serializable

对应的Java:

public class Person implements java.io.Serializable {
  private final static long SerialVersionUID = 1234L
  
	// ...
}

@transient变量会被加上Java的transient修饰符:

Java注解

Java注解可以直接在Scala代码中用,任何Java框架都会看到这些注解。如Junit的注解:

  import org.junit.Test
  import org.junit.Assert.assertEquals

  class SetTest {

    @Test
    def testMultiAdd {
      val set = Set() + 1 + 2 + 3 + 1 + 2 + 3
      assertEquals(3, set.size)
    }
  }

Scala可以直接用:

  $ scala -cp junit-4.3.1.jar:. org.junit.runner.JUnitCore SetTest
  JUnit version 4.3.1
  .
  Time: 0.023
 
  OK (1 test)

编写自己的注解

为了让注解对Java反射可见,必须用Java语法编写并用javac编译。将来Scala可能会有 自己的反射,但现在Scala还没有办法来实现Java注解的全部功能。

但是有可以要使用Scala反射来访问Scala的注解。所以要先用Java来写:

  import java.lang.annotation.*;
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.METHOD)
  public @interface Ignore { }

使用javac编译过以后,Scala里使用的方式:

  object Tests {
    @Ignore
    def testData = List(0, 1, -1, 5, -5)

    def test1 {
      assert(testData == (testData.head :: testData.tail))
    }

    def test2 {
      assert(testData.contains(testData.head))
    }
  }

这里的test1test2应该是测试方法,尽管testDatatest开头,但实际上应该 被忽略。

通过Java的反身API来观察这些注解是否被用到:

  for {
    method <- Tests.getClass.getMethods
    if method.getName.startsWith("test")
    if method.getAnnotation(classOf[Ignore]) == null
  } {
    println("found a test method: " + method)
  }

在这里用反射方法getClassgetMethods来检查输入对象类的所有字段。与注解相关的 部分是getAnnotation方法,它用来查找特定类型的注解,这里用来查找我们定义的 Ignore类型的注解。运行起来是这样的:

  $ javac Ignore.java
  $ scalac Tests.scala
  $ scalac FindTests.scala
  $ scala FindTests
  found a test method: public void Tests$.test2()
  found a test method: public void Tests$.test1()

注意这些方法在Java反射看来是位于Test$类而不是Test类中,因为这是单例对象。

还要注意Java注解的限制,比如,注解的参数只能用常量不能用表达式。(可以用 @serial(1234)而不能用@serial(x*2))。