Scala 教程

Scala 是一门多范式的编程语言,一种类似 Java 的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。

我们认为最有意义的学习方式是,不要把 Scala 看做是改进的 Java,而是把它作为一门新的语言。所以这里不会介绍 Java 的使用经验,而将集中讲解解释器和“对象-函数式”的风格,以及 Scala 编程风格。特别强调了可维护性,并发,常用工具和利用类型系统的优势。

适用人群

本教程是为有经验的工程师准备,所以建议读者学习前,了解下 Java 语言的基础或 Scala 入门教程。


学习前提

Scala 是一种相对较新的语言,但借鉴了许多熟悉的概念,所以在学习前,我们假设您已经对面向对象编程、并发、可维护等基本编程思想有了一定的了解。

鸣谢:http://twitter.github.io/scala_school/zh_cn/index.html


5

版本信息

书中演示代码基于以下版本:

语言版本信息
Scala2.8.0

基础

关于这节课

最初的几个星期将涵盖基本语法和概念,然后我们将通过更多的练习展开这些内容。

有一些例子是以解释器交互的形式给出的,另一些则是以源文件的形式给出的。

安装一个解释器,可以使探索问题空间变得更容易。

为什么选择 Scala?

  • 表达能力
    • 函数是一等公民
    • 闭包
  • 简洁
    • 类型推断
    • 函数创建的文法支持
  • Java互操作性
    • 可重用 Java 库
    • 可重用 Java 工具
    • 没有性能惩罚

Scala 如何工作?

  • 编译成 Java 字节码
  • 可在任何标准 JVM 上运行
    • 甚至是一些不规范的JVM上,如 Dalvik
    • Scala 编译器是 Java 编译器的作者写的

用 Scala 思考

Scala 不仅仅是更好的 Java。你应该用全新的头脑来学习它,你会从这些课程中认识到这一点的。

安装 Scala 请看:Scala 安装及环境配置

启动解释器

使用自带的 sbt console 启动。

$ sbt console[...]Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_20).Type in expressions to have them evaluated.Type :help for more information.scala>

表达式

scala> 1 + 1res0: Int = 2

res0 是解释器自动创建的变量名称,用来指代表达式的计算结果。它是 Int 类型,值为 2。

Scala 中(几乎)一切都是表达式。

你可以给一个表达式的结果起个名字赋成一个不变量(val)。

scala> val two = 1 + 1two: Int = 2

你不能改变这个不变量的值。

变量

如果你需要修改这个名称和结果的绑定,可以选择使用 var

scala> var name = "steve"name: java.lang.String = stevescala> name = "marius"name: java.lang.String = marius

函数

你可以使用 def 创建函数.

scala> def addOne(m: Int): Int = m + 1addOne: (m: Int)Int

在 Scala 中,你需要为函数参数指定类型签名。

scala> val three = addOne(2)three: Int = 3

如果函数不带参数,你可以不写括号。

scala> def three() = 1 + 2three: ()Intscala> three()res2: Int = 3scala> threeres3: Int = 3

匿名函数

你可以创建匿名函数。

scala> (x: Int) => x + 1res2: (Int) => Int = <function1>

这个函数为名为 x 的 Int 变量加 1。

scala> res2(1)res3: Int = 2

你可以传递匿名函数,或将其保存成不变量。

scala> val addOne = (x: Int) => x + 1addOne: (Int) => Int = <function1>scala> addOne(1)res4: Int = 2

如果你的函数有很多表达式,可以使用 {} 来格式化代码,使之易读。

def timesTwo(i: Int): Int = {     println("hello world")  i * 2}

对匿名函数也是这样的。

scala> { i: Int =>      println("hello world")      i * 2}res0: (Int) => Int = <function1>

在将一个匿名函数作为参数进行传递时,这个语法会经常被用到。

部分应用(Partial application)

你可以使用下划线“_”部分应用一个函数,结果将得到另一个函数。Scala 使用下划线表示不同上下文中的不同事物,你通常可以把它看作是一个没有命名的神奇通配符。在`{ + 2 }`的上下文中,它代表一个匿名参数。你可以这样使用它:

scala> def adder(m: Int, n: Int) = m + nadder: (m: Int,n: Int)Intscala> val add2 = adder(2, _:Int)add2: (Int) => Int = <function1>scala> add2(3)res50: Int = 5

你可以部分应用参数列表中的任意参数,而不仅仅是最后一个。

柯里化函数

有时会有这样的需求:允许别人一会在你的函数上应用一些参数,然后又应用另外的一些参数。

例如一个乘法函数,在一个场景需要选择乘数,而另一个场景需要选择被乘数。

scala> def multiply(m: Int)(n: Int): Int = m * nmultiply: (m: Int)(n: Int)Int

你可以直接传入两个参数。

scala> multiply(2)(3)res0: Int = 6

你可以填上第一个参数并且部分应用第二个参数。

scala> val timesTwo = multiply(2) _timesTwo: (Int) => Int = <function1>scala> timesTwo(3)res1: Int = 6

你可以对任何多参数函数执行柯里化。例如之前的 adder 函数

第一次传参是一个加数,返回一个函数,调用第二个函数传参,另一个数,得出结果是和

scala> (adder _).curriedres1: (Int) => (Int) => Int = <function1>

scala> res1(2)

res2: (Int) => Int = <function1>

scala> res2(3)

res3: Int = 5

可变长度参数

这是一个特殊的语法,可以向方法传入任意多个同类型的参数。例如要在多个字符串上执行 String 的 capitalize 函数,可以这样写:

def capitalizeAll(args: String*) = {    args.map { arg =>    arg.capitalize  }}scala> capitalizeAll("rarity", "applejack")res2: Seq[String] = ArrayBuffer(Rarity, Applejack)

scala> class Calculator {     |   val brand: String = "HP"         |   def add(m: Int, n: Int): Int = m + n     | }defined class Calculatorscala> val calc = new Calculatorcalc: Calculator = Calculator@e75a11scala> calc.add(1, 2)res1: Int = 3scala> calc.brandres2: String = "HP"

上面的例子展示了如何在类中用 def 定义方法和用 val 定义字段值。方法就是可以访问类的状态的函数。

构造函数

构造函数不是特殊的方法,他们是除了类的方法定义之外的代码。让我们扩展计算器的例子,增加一个构造函数参数,并用它来初始化内部状态。

class Calculator(brand: String) {  /**   * A constructor.  */  val color: String = if (brand == "TI") {    "blue"  } else if (brand == "HP") {    "black"  } else {    "white"  }  // An instance method.  def add(m: Int, n: Int): Int = m + n}

注意两种不同风格的评论。

你可以使用构造函数来构造一个实例:

scala> val calc = new Calculator("HP")calc: Calculator = Calculator@1e64cc4dscala> calc.colorres0: String = black

表达式

上文的 Calculator 例子说明了 Scala 是如何面向表达式的。颜色的值就是绑定在一个if/else表达式上的。Scala 是高度面向表达式的:大多数东西都是表达式而非指令。

旁白: 函数 vs 方法

函数和方法在很大程度上是可以互换的。由于函数和方法是如此的相似,你可能都不知道你调用的东西是一个函数还是一个方法。而当真正碰到的方法和函数之间的差异的时候,你可能会感到困惑。

scala> class C {     |   var acc = 0     |   def minc = { acc += 1 }     |   val finc = { () => acc += 1 }     | }defined class Cscala> val c = new Cc: C = C@1af1bd6scala> c.minc // calls c.minc()scala> c.finc // returns the function as a value:res2: () => Unit = <function0>

当你可以调用一个不带括号的“函数”,但是对另一个却必须加上括号的时候,你可能会想哎呀,我还以为自己知道 Scala 是怎么工作的呢。也许他们有时需要括号?你可能以为自己用的是函数,但实际使用的是方法。

在实践中,即使不理解方法和函数上的区别,你也可以用 Scala 做伟大的事情。如果你是 Scala 新手,而且在读两者的差异解释,你可能会跟不上。不过这并不意味着你在使用 Scala 上有麻烦。它只是意味着函数和方法之间的差异是很微妙的,只有深入语言内部才能清楚理解它。

继承

class ScientificCalculator(brand: String) extends Calculator(brand) {  def log(m: Double, base: Double) = math.log(m) / math.log(base)}

参考 Effective Scala 指出如果子类与父类实际上没有区别,类型别名是优于继承的。A Tour of Scala 详细介绍了子类化

重载方法

class EvenMoreScientificCalculator(brand: String) extends ScientificCalculator(brand) {  def log(m: Int): Double = log(m, math.exp(1))}

抽象类

你可以定义一个抽象类,它定义了一些方法但没有实现它们。取而代之是由扩展抽象类的子类定义这些方法。你不能创建抽象类的实例。

scala> abstract class Shape {     |   def getArea():Int    //子类应定义为这个     | }defined class Shapescala> class Circle(r: Int) extends Shape {     |   def getArea():Int = { r * r * 3 }     | }defined class Circlescala> val s = new Shape<console>:8: error: class Shape is abstract; cannot be instantiated       val s = new Shape               ^scala> val c = new Circle(2)c: Circle = Circle@65c0035b

特质(Traits)

特质是一些字段和行为的集合,可以扩展或混入(mixin)你的类中。

trait Car {  val brand: String}trait Shiny {  val shineRefraction: Int}class BMW extends Car {  val brand = "BMW"}

通过 with 关键字,一个类可以扩展多个特质:

class BMW extends Car with Shiny {  val brand = "BMW"  val shineRefraction = 12}

参考 Effective Scala 对特质的观点

什么时候应该使用特质而不是抽象类? 如果你想定义一个类似接口的类型,你可能会在特质和抽象类之间难以取舍。这两种形式都可以让你定义一个类型的一些行为,并要求继承者定义一些其他行为。一些经验法则:

  • 优先使用特质。一个类扩展多个特质是很方便的,但却只能扩展一个抽象类。
  • 如果你需要构造函数参数,使用抽象类。因为抽象类可以定义带参数的构造函数,而特质不行。例如,你不能说trait t(i: Int) {},参数i是非法的。

类型

此前,我们定义了一个函数的参数为 Int,表示输入是一个数字类型。其实函数也可以是泛型的,来适用于所有类型。当这种情况发生时,你会看到用方括号语法引入的类型参数。下面的例子展示了一个使用泛型键和值的缓存。

trait Cache[K, V] {  def get(key: K): V  def put(key: K, value: V)  def delete(key: K)}

方法也可以引入类型参数。

def remove[K](key: K)


基础知识(续)

apply 方法

当类或对象有一个主要用途的时候,apply 方法为你提供了一个很好的语法糖。

scala> class Foo {}defined class Fooscala> object FooMaker {     |   def apply() = new Foo     | }defined module FooMakerscala> val newFoo = FooMaker()newFoo: Foo = Foo@5b83f762

scala> class Bar {     |   def apply() = 0     | }defined class Barscala> val bar = new Barbar: Bar = Bar@47711479scala> bar()res8: Int = 0

在这里,我们实例化对象看起来像是在调用一个方法。以后会有更多介绍!

单例对象

单例对象用于持有一个类的唯一实例。通常用于工厂模式。

object Timer {  var count = 0  def currentCount(): Long = {    count += 1    count  }}

可以这样使用:

scala> Timer.currentCount()res0: Long = 1

单例对象可以和类具有相同的名称,此时该对象也被称为“伴生对象”。我们通常将伴生对象作为工厂使用。

下面是一个简单的例子,可以不需要使用new来创建一个实例了。

class Bar(foo: String)object Bar {  def apply(foo: String) = new Bar(foo)}

函数即对象

在 Scala 中,我们经常谈论对象的函数式编程。这是什么意思?到底什么是函数呢?

函数是一些特质的集合。具体来说,具有一个参数的函数是 Function1 特质的一个实例。这个特征定义了 apply()语法糖,让你调用一个对象时就像你在调用一个函数。

scala> object addOne extends Function1[Int, Int] {     |   def apply(m: Int): Int = m + 1     | }defined module addOnescala> addOne(1)res2: Int = 2

这个 Function 特质集合下标从 0 开始一直到 22。为什么是 22?这是一个主观的魔幻数字(magic number)。我从来没有使用过多于 22 个参数的函数,所以这个数字似乎是合理的。

apply 语法糖有助于统一对象和函数式编程的二重性。你可以传递类,并把它们当做函数使用,而函数本质上是类的实例。

这是否意味着,当你在类中定义一个方法时,得到的实际上是一个 Function*的实例?不是的,在类中定义的方法是方法而不是函数。在 repl 中独立定义的方法是 Function* 的实例。

类也可以扩展 Function,这些类的实例可以使用()调用。

scala> class AddOne extends Function1[Int, Int] {     |   def apply(m: Int): Int = m + 1     | }defined class AddOnescala> val plusOne = new AddOne()plusOne: AddOne = <function1>scala> plusOne(1)res0: Int = 2

可以使用更直观快捷的 extends (Int => Int) 代替 extends Function1[Int, Int]

class AddOne extends (Int => Int) {  def apply(m: Int): Int = m + 1}

你可以将代码组织在包里。

package com.twitter.example

在文件头部定义包,会将文件中所有的代码声明在那个包中。

值和函数不能在类或单例对象之外定义。单例对象是组织静态函数(static function)的有效工具。

package com.twitter.exampleobject colorHolder {  val BLUE = "Blue"  val RED = "Red"}

现在你可以直接访问这些成员

println("the color is: " + com.twitter.example.colorHolder.BLUE)

注意在你定义这个对象时 Scala 解释器的返回:

scala> object colorHolder {     |   val Blue = "Blue"     |   val Red = "Red"     | }defined module colorHolder

这暗示了 Scala 的设计者是把对象作为 Scala 的模块系统的一部分进行设计的。

模式匹配

这是 Scala 中最有用的部分之一。

匹配值

val times = 1times match {  case 1 => "one"  case 2 => "two"  case _ => "some other number"}

使用守卫进行匹配

times match {  case i if i == 1 => "one"  case i if i == 2 => "two"  case _ => "some other number"}

注意我们是怎样将值赋给变量i的。

在最后一行指令中的_是一个通配符;它保证了我们可以处理所有的情况。

否则当传进一个不能被匹配的数字的时候,你将获得一个运行时错误。我们以后会继续讨论这个话题的。

参考 Effective Scala 对[什么时候使用模式匹配](http://twitter.github.com/effectivescala/#Functional programming-Pattern matching)和[模式匹配格式化的建议](http://twitter.github.com/effectivescala/#Formatting-Pattern matching)。 A Tour of Scala 也描述了模式匹配

匹配类型

你可以使用 match 来分别处理不同类型的值。

def bigger(o: Any): Any = {  o match {    case i: Int if i < 0 => i - 1    case i: Int => i + 1    case d: Double if d < 0.0 => d - 0.1    case d: Double => d + 0.1    case text: String => text + "s"  }}

匹配类成员

还记得我们之前的计算器吗。

让我们通过类型对它们进行分类。

def calcType(calc: Calculator) = calc match {  case _ if calc.brand == "hp" && calc.model == "20B" => "financial"  case _ if calc.brand == "hp" && calc.model == "48G" => "scientific"  case _ if calc.brand == "hp" && calc.model == "30B" => "business"  case _ => "unknown"}

样本类 Case Classes

使用样本类可以方便得存储和匹配类的内容。你不用 new 关键字就可以创建它们。

scala> case class Calculator(brand: String, model: String)defined class Calculatorscala> val hp20b = Calculator("hp", "20b")hp20b: Calculator = Calculator(hp,20b)

样本类基于构造函数的参数,自动地实现了相等性和易读的 toString 方法。

scala> val hp20b = Calculator("hp", "20b")hp20b: Calculator = Calculator(hp,20b)scala> val hp20B = Calculator("hp", "20b")hp20B: Calculator = Calculator(hp,20b)scala> hp20b == hp20Bres6: Boolean = true

样本类也可以像普通类那样拥有方法。

使用样本类进行模式匹配

样本类就是被设计用在模式匹配中的。让我们简化之前的计算器分类器的例子。

val hp20b = Calculator("hp", "20B")val hp30b = Calculator("hp", "30B")def calcType(calc: Calculator) = calc match {  case Calculator("hp", "20B") => "financial"  case Calculator("hp", "48G") => "scientific"  case Calculator("hp", "30B") => "business"  case Calculator(ourBrand, ourModel) => "Calculator: %s %s is of unknown type".format(ourBrand, ourModel)}

最后一句也可以这样写

  case Calculator(_, _) => "Calculator of unknown type"

或者我们完全可以不将匹配对象指定为 Calculator 类型

  case _ => "Calculator of unknown type"

或者我们也可以将匹配的值重新命名。

  case c@Calculator(_, _) => "Calculator: %s of unknown type".format(c)

异常

Scala 中的异常可以在 try-catch-finally 语法中通过模式匹配使用。

try {  remoteCalculatorService.add(1, 2)} catch {  case e: ServerIsDownException => log.error(e, "the remote calculator service is unavailable. should have kept your trusty HP.")} finally {  remoteCalculatorService.close()}

try 也是面向表达式的

val result: Int = try {  remoteCalculatorService.add(1, 2)} catch {  case e: ServerIsDownException => {    log.error(e, "the remote calculator service is unavailable. should have kept your trusty HP.")    0  }} finally {  remoteCalculatorService.close()}

这并不是一个完美编程风格的展示,而只是一个例子,用来说明 try-catch-finally 和 Scala 中其他大部分事物一样是表达式。

当一个异常被捕获处理了,finally 块将被调用;它不是表达式的一部分。

Finagle 介绍

Finagle-Friendly REPL

我们将要讨论的不是标准 Scala 的代码。如果你喜欢使用 REPL 学习,你可能想知道如何获得一个加入 Finagle 及其依赖的 Scala REPL。

你可以在这里获取 Finagle 源代码。

如果你在 finagle 目录下有 Finagle 的源代码,你可以通过下面的命令得到一个控制台

$ cd finagle$ ./sbt "project finagle-http" console ...build output...scala>

Futures

Finagle 使用 com.twitter.util.Future [1]编码延迟操作。Future 是尚未生成的值的一个句柄。Finagle 使用 Future 作为其异步 API 的返回值。同步 API 会在返回前等待结果;但是异步 API 则不会等待。例如,个对互联网上一些服务的HTTP请求可能半秒都不会返回。你不希望你的程序阻塞等待半秒。“慢”的 API 可以立即返回一个 Future,然后在需要解析其值时“填充”。

val myFuture = MySlowService(request) // returns right away   ...do other things...val serviceResult = myFuture.get() // blocks until service "fills in" myFuture

在实践中,你不会发送一个请求,然后在几行代码后调用 myFuture.get。Future 提供了注册回调的方法,在值变得可用时会调用注册的回调函数。

如果你用过其他异步 API,当看到“回调”你也许会畏缩。你可能会联想到他们难以辨认的代码流,被调用的函数藏在离调用处远远的地方。但是,Future 可以利用 Scala 中“函数是一等公民”的特性编写出更可读的代码流。你可以在调用它的地方简单地定义一个处理函数。

例如,写代码调度请求,然后“处理”回应,你可以保持代码在一起:

val future = dispatch(req) // returns immediately, but future is "empty"future onSuccess { reply => // when the future gets "filled", use its value  println(reply)}

你可以在 REPL 中用体验一下 Future。虽然不是学习如何在实际代码中使用他们的好方法,但可以帮助理解 API。当你使用 REPL,Promise 是一个方便的类。它是 Future 抽象类的一个具体子类。你可以用它来创建一个还没有值的 Future。

scala> import com.twitter.util.{Future,Promise}import com.twitter.util.{Future, Promise}scala> val f6 = Future.value(6) // create already-resolved futuref6: com.twitter.util.Future[Int] = com.twitter.util.ConstFuture@c63a8afscala> f6.get()res0: Int = 6scala> val fex = Future.exception(new Exception) // create resolved sad futurefex: com.twitter.util.Future[Nothing] = com.twitter.util.ConstFuture@38ddab20scala> fex.get()java.lang.Exception  ... stack trace ...scala> val pr7 = new Promise[Int] // create unresolved futurepr7: com.twitter.util.Promise[Int] = Promise@1994943491(...)scala> pr7.get()  ...console hangs, waiting for future to resolve...Ctrl-CExecution interrupted by signal.scala> pr7.setValue(7)scala> pr7.get()res1: Int = 7scala>

在实际代码中使用 Future 时,你通常不会调用 get,而是使用回调函数。get 仅仅是方便在 REPL 修修补补。

顺序组合

Future 有类似集合 API 中的组合子(如 map, flatMap) 。回顾一下集合组合子,它让你可以表达如 “我有一个整数 List 和一个 square 函数:map 那个列表获得整数平方的列表”这样的操作。这种表达方式很灵巧;你可以把组合子函数和另一个函数放在一起有效地组成一个新函数。面向 Future 的组合子可以让你这样表达:“我有一个期望整数的 Future 和一个 square 函数:map 那个 Future 获得一个期望整数平方的 Future”。

如果你在定义一个异步 API,传入一个请求值,你的 API 应该返回一个包装在 Future 中的响应。因此,这些把输入和函数加入 Future 的组合子是相当有用的:它们帮助你根据其它异步 API 定义你自己的异步 API。

最重要的 Future 的组合子是 flatMap[2]

def Future[A].flatMap[B](f: A => Future[B]): Future[B]

flatMap 序列化两个 Future。即,它接受一个Future和一个异步函数,并返回另一个 Future。方法签名中是这样写的:给定一个 Future 成功的值,函数f提供下一个 Future。如果/当输入的 Future 成功完成,flatMap 自动调用f。只有当这两个 Future 都已完成,此操作所代表的 Future才算完成。如果任何一个 Future 失败,则操作确定的 Future 也将失败。这种隐交织的错误让我们只需要在必要时来处理错误,所以语法意义很大。flatMap 是这些语义组合子的标准名称。

如果你有一个 Future 并且想在异步 API 使用其值,使用 flatMap。例如,假设你有一个 Future[User],需要一个 Future[Boolean]表示用户是否已被禁止。有一个 isBanned 的异步 API 来判断一个用户是否已被禁止。此时可以使用 flatMap :

scala> import com.twitter.util.{Future,Promise}import com.twitter.util.{Future, Promise}scala> class User(n: String) { val name = n }defined class Userscala> def isBanned(u: User) = { Future.value(false) }isBanned: (u: User)com.twitter.util.Future[Boolean]scala> val pru = new Promise[User]pru: com.twitter.util.Promise[User] = Promise@897588993(...)scala> val futBan = pru flatMap isBanned // apply isBanned to futurefutBan: com.twitter.util.Future[Boolean] = Promise@1733189548(...)scala> futBan.get()  ...REPL hangs, futBan not resolved yet...Ctrl-CExecution interrupted by signal.scala> pru.setValue(new User("prudence"))scala> futBan.get()res45: Boolean = falsescala>

同样,如果要在 Future 中应用一个同步函数,可以使用 map。例如,假设你有一个 Future[RawCredentials] 需要一个 Future[Credentials]。你有一个的同步的 normalize 函数将 RawCredentials 转换成 Credentials。可以使用 map:

scala> class RawCredentials(u: String, pw: String) {     |   val username = u     |   val password = pw     | }defined class RawCredentialsscala> class Credentials(u: String, pw: String) {     |   val username = u     |   val password = pw     | }defined class Credentialsscala> def normalize(raw: RawCredentials) = {     |   new Credentials(raw.username.toLowerCase(), raw.password)     | }normalize: (raw: RawCredentials)Credentialsscala> val praw = new Promise[RawCredentials]praw: com.twitter.util.Promise[RawCredentials] = Promise@1341283926(...)scala> val fcred = praw map normalize // apply normalize to futurefcred: com.twitter.util.Future[Credentials] = Promise@1309582018(...)scala> fcred.get()   ...REPL hangs, fcred doesn't have a value yet...Ctrl-CExecution interrupted by signal.scala> praw.setValue(new RawCredentials("Florence", "nightingale"))scala> fcred.get().usernameres48: String = florencescala>

Scala 有快捷语法来调用 flatMap:for 表达式。假设你想通过异步 API 验证登录请求,然后通过另一个异步 API 检查用户是否被禁止。在 for 表达式的帮助下,我们可以这样写:

scala> def authenticate(req: LoginRequest) = {     |   // TODO: we should check the password     |   Future.value(new User(req.username))     | }authenticate: (req: LoginRequest)com.twitter.util.Future[User]scala> val f = for {     |  u <- authenticate(request)     |  b <- isBanned(u)     | } yield (u, b)f: com.twitter.util.Future[(User, Boolean)] = Promise@35785606(...)scala>

它产生一个 f: Future[(User, Boolean)],包含用户对象和一个表示该用户是否已被禁止的布尔值。注意这里是怎样实现顺序组合的:isBanned 使用了 authenticate 的输出作为其输入。

并发组合

你可能想一次获取来自多个服务的数据。例如,如果你正在编写一个 Web 服务来显示内容和广告,它可能会从两个服务中分别获取内容和广告。但是,你怎么告诉代码来等待两份答复呢?如果必须自己实现可能会非常棘手,幸运的是你可以使用并发组合子。

Future 提供了一些并发组合子。一般来说,他们都是将 Future 的一个序列转换成包含一个序列的 Future,只是方式略微不同。这是很好的,因为它(本质上)可以让你把几个 Future 封装成一个单一的 Future。

object Future {  …  def collect[A](fs: Seq[Future[A]]): Future[Seq[A]]  def join(fs: Seq[Future[_]]): Future[Unit]  def select(fs: Seq[Future[A]]) : Future[(Try[A], Seq[Future[A]])]}

collect 参数是具有相同类型 Future 的一个集合,返回一个 Future,其类型是包含那个类型值的一个序列。当所有的 Future 都成功完成或者当中任何一个失败,都会使这个 Future 完成。返回序列的顺序和传入序列的顺序相对应。

scala> val f2 = Future.value(2)f2: com.twitter.util.Future[Int] = com.twitter.util.ConstFuture@13ecdec0scala> val f3 = Future.value(3)f3: com.twitter.util.Future[Int] = com.twitter.util.ConstFuture@263bb672scala> val f23 = Future.collect(Seq(f2, f3))f23: com.twitter.util.Future[Seq[Int]] = Promise@635209178(...)scala> val f5 = f23 map (_.sum)f5: com.twitter.util.Future[Int] = Promise@1954478838(...)scala> f5.get()res9: Int = 5

join 参数是混合类型的 Future 序列,返回一个 Future[Unit],当所有的相关 Future 完成时(无论他们是否失败)该 Future 完成。其作用是标识一组异构操作完成。对那个内容和广告的例子来说,这可能是一个很好的解决方案。

scala> val ready = Future.join(Seq(f2, f3))ready: com.twitter.util.Future[Unit] = Promise@699347471(...)scala> ready.get() // doesn't ret value, but I know my futures are donescala>

当传入的 Future 序列的第一个 Future 完成的时候,select 会返回一个 Future。它会将那个完成的 Future 和其它未完成的 Future 一起放在 Seq 中返回。 (它不会做任何事情来取消剩余的 Future。你可以等待更多的回应,或者忽略他们)

scala> val pr7 = new Promise[Int] // unresolved futurepr7: com.twitter.util.Promise[Int] = Promise@1608532943(...)scala> val sel = Future.select(Seq(f2, pr7)) // select from 2 futs, one resolvedsel: com.twitter.util.Future[...] = Promise@1003382737(...)scala> val(complete, stragglers) = sel.get()complete: com.twitter.util.Try[Int] = Return(2)stragglers: Seq[...] = List(...)scala> complete.get()res110: Int = 2scala> stragglers(0).get() // our list of not-yet-finished futures has one item  ...get() hangs the REPL because this straggling future is not finished...Ctrl-CExecution interrupted by signal.scala> pr7.setValue(7)scala> stragglers(0).get()res113: Int = 7scala>

组合例子:缓存速率限制

这些组合子表达了典型的网络服务操作。这段假设的代码在对速率进行限制(为了保持本地速率限制缓存)的同时,将用户的请求调度到后台服务:

// Find out if user is rate-limited. This can be slow; we have to ask// the remote server that keeps track of who is rate-limited.def isRateLimited(u: User): Future[Boolean] = {  ...}// Notice how you can swap this implementation out now with something that might// implement a different, more restrictive policy.// Check the cache to find out if user is rate-limited. This cache// implementation is just a Map, and can return a value right way. But we// return a Future anyhow in case we need to use a slower implementation later.def isLimitedByCache(u: User): Future[Boolean] =  Future.value(limitCache(u))// Update the cachedef setIsLimitedInCache(user: User, v: Boolean) { limitCache(user) = v }// Get a timeline of tweets... unless the user is rate-limited (then throw// an exception instead)def getTimeline(cred: Credentials): Future[Timeline] =  isLimitedByCache(cred.user) flatMap {    case true => Future.exception(new Exception("rate limited"))    case false =>      // First we get auth'd user then we get timeline.      // Sequential composition of asynchronous APIs: use flatMap      val timeline = auth(cred) flatMap(getTimeline)      val limited = isRateLimited(cred.user) onSuccess(                                       setIsLimitedInCache(cred.user, _))      // 'join' concurrently combines differently-typed futures      // 'flatMap' sequentially combines, specifies what to do next      timeline join limited flatMap {        case (_, true) => Future.exception(new Exception("rate limited"))        case (timeline, _) => Future.value(timeline)      }  }}

这个例子结合了顺序和并发组合。请注意,除了给转化速率限制回应一个异常以外,没有明确的错误处理。如果任何 Future 在这里失败,它会自动传播到返回的 Future 中。

组合例子:网络爬虫

你已经看到了怎样使用 Future 组合子的例子,不过也许意犹未尽。假设你有一个简单的互联网模型。该互联网中只有 HTML 网页和图片,其中页面可以链接到图像和其他网页。你可以获取一个页面或图像,但 API 是异步的。这个假设的 API 成这些“可获取”的数据为资源:

import com.twitter.util.{Try,Future,Promise}// a fetchable thingtrait Resource {  def imageLinks(): Seq[String]  def links(): Seq[String]}// HTML pages can link to Imgs and to other HTML pages.class HTMLPage(val i: Seq[String], val l: Seq[String]) extends Resource {  def imageLinks() = i  def links = l}// IMGs don't actually link to anything elseclass Img() extends Resource {  def imageLinks() = Seq()  def links() = Seq()}// profile.html links to gallery.html and has an image link to portrait.jpgval profile = new HTMLPage(Seq("portrait.jpg"), Seq("gallery.html"))val portrait = new Img// gallery.html links to profile.html and two imagesval gallery = new HTMLPage(Seq("kitten.jpg", "puppy.jpg"), Seq("profile.html"))val kitten = new Imgval puppy = new Imgval internet = Map(  "profile.html" -> profile,  "gallery.html" -> gallery,  "portrait.jpg" -> portrait,  "kitten.jpg" -> kitten,  "puppy.jpg" -> puppy)// fetch(url) attempts to fetch a resource from our fake internet.// Its returned Future might contain a Resource or an exceptiondef fetch(url: String) = { new Promise(Try(internet(url))) }

顺序组合

假设给定一个页面 URL,而你希望获取该页面的第一个图。也许你正在做一个网站,在上面用户可以发布有趣的网页链接。为了帮助其他用户决定某个链接是否值得追踪,你打算显示那个链接中第一张图像的缩略图。

即使你不知道组合子,你仍然可以写一个缩略图获取函数:

def getThumbnail(url: String): Future[Resource]={  val returnVal = new Promise[Resource]  fetch(url) onSuccess { page => // callback for successful page fetch    fetch(page.imageLinks()(0)) onSuccess { p => // callback for successful img fetch      returnVal.setValue(p)    } onFailure { exc => // callback for failed img fetch      returnVal.setException(exc)    }  } onFailure { exc => // callback for failed page fetch    returnVal.setException(exc)  }  returnVal}

这个版本的函数能工作。它的大部分内容用来解析 Future,然后把他们的内容传给另一个 Future。

我们希望得到一个页面,然后从该页面获得一个图像。如果你想获得 A,然后再获得 B 的,这通常意味着顺序组合。由于 B 是异步的,所以需要使用 flatMap:

def getThumbnail(url: String): Future[Resource] =  fetch(url) flatMap { page => fetch(page.imageLinks()(0)) }

通过并发组合

抓取页面的第一个图片是好的,但也许我们应该获取所有图片,并让用户自己进行选择。我们可以使用for循环一个个地抓取,但这需要很长时间;所以我们想并行获取它们。如果你想的事情“并行”发生,这通常意味着并发组合。所以我们使用 Future.collect 的提取所有的图像:

def getThumbnails(url:String): Future[Seq[Resource]] =  fetch(url) flatMap { page =>    Future.collect(      page.imageLinks map { u => fetch(u) }    )  }

如果这对你有意义,那太好了。你可能会看不懂这行代码 page.imageLinks map { u => fetch(u) }:它使用 map 和 map 后的函数返回一个 Future。当接下来的事情是返回一个Future时,我们不是应该使用flatMap吗?但是请注意,在 map 前的不是一个 Future;它是一个集合。collection map function 返回一个集合;我们使用 Future.collect 收集 Future 的集合到一个 Future 中。

并发 + 递归

除了页面中的图片以外,我们可能会想获取它链接的其他页面。通过递归我们可以构建一个简单的网络爬虫。

// Returndef crawl(url: String): Future[Seq[Resource]] =  fetch(url) flatMap { page =>    Future.collect(      page.links map { u => crawl(u) }    ) map { pps => pps.flatten }}crawl("profile.html")   ...hangs REPL, infinite loop...Ctrl-CExecution interrupted by signal.scala>// She's gone rogue, captain! Have to take her out!// Calling Thread.stop on runaway Thread[Thread-93,5,main] with offending code:// scala> crawl("profile.html")

在实践中,这个网络爬虫不是很有用:首先我们没有告诉它何时停止爬行;其次即使资源刚刚被获取过,它仍然会不厌其烦地重新获取。

服务

一个 Finagle 服务用来处理 RPC,读取请求并给予回复的。服务是针对请求和回应的一个函数Req => Future[Rep]

abstract class Service[-Req, +Rep] extends (Req => Future[Rep])

在服务中,我们要同时定义客户端和服务器。

一个 Finagle 客户端“引入”一个网络服务。从概念上讲,Finagle 客户端由两部分组成

  • 一个使用服务的函数:分发一个 Req 并处理 Future[Rep]
  • 配置怎样分发这些请求;例如,作为 HTTP 请求发送到 api.twitter.com 的 80 端口

同样,Finagle 服务端“输出”网络服务。一个服务端由两个部分组成:

  • 一个实现服务的函数:传入一个 Req 并返回一个 Future[Rep]
  • 配置如何“监听”输入的 Reqs;例如,在 80 端口的 HTTP 请求。

这种设计分离了服务的“业务逻辑”和数据如何在网络中流动的配置。

我们也谈论 Finagle “过滤器”。过滤器在服务之间,修改流经它的数据。过滤器可以很好地和服务组合在一起。例如,如果你有一个速率限制过滤器和一个 tweet 服务,你可以把它们组合在一起形成有速率限制的 tweet 服务。

客户端

一个 Finagle 客户端“引入”一个网络服务。它有一些配置来设定如何在网络上发送数据。一个简单的 HTTP 客户端可能看起来像这样:

import org.jboss.netty.handler.codec.http.{DefaultHttpRequest, HttpRequest, HttpResponse, HttpVersion, HttpMethod}import com.twitter.finagle.Serviceimport com.twitter.finagle.builder.ClientBuilderimport com.twitter.finagle.http.Http// Don't worry, we discuss this magic "ClientBuilder" laterval client: Service[HttpRequest, HttpResponse] = ClientBuilder()  .codec(Http())  .hosts("twitter.com:80") // If >1 host, client does simple load-balancing  .hostConnectionLimit(1)  .build()val req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")val f = client(req) // Client, send the request// Handle the response:f onSuccess { res =>  println("got response", res)} onFailure { exc =>  println("failed :-(", exc)}

服务端

一个服务端按服务进行定义,并配置如何“监听”网络上的请求。一个简单的 HTTP 服务端可能看起来像这样:

import com.twitter.finagle.Serviceimport com.twitter.finagle.http.Httpimport com.twitter.util.Futureimport org.jboss.netty.handler.codec.http.{DefaultHttpResponse, HttpVersion, HttpResponseStatus, HttpRequest, HttpResponse}import java.net.{SocketAddress, InetSocketAddress}import com.twitter.finagle.builder.{Server, ServerBuilder}import com.twitter.finagle.builder.ServerBuilder// Define our service: OK response for root, 404 for other pathsval rootService = new Service[HttpRequest, HttpResponse] {  def apply(request: HttpRequest) = {    val r = request.getUri match {      case "/" => new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK)      case _ => new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND)    }    Future.value(r)  }}// Serve our service on a portval address: SocketAddress = new InetSocketAddress(10000)val server: Server = ServerBuilder()  .codec(Http())  .bindTo(address)  .name("HttpServer")  .build(rootService)

这个name是我们强加的,虽然没有在例子中使用它,但这个字段对分析和调试是很有用的。

过滤器

过滤器改造服务,它们可以提供通用的服务功能。例如你有几个服务需要支持速率限制,这时可以写一个限速过滤器并将其应用于所有的服务就解决问题了。过滤器也可以将服务分解成不同的阶段。

一个简单的代理可能看起来像这样:

class MyService(client: Service[..]) extends Service[HttpRequest, HttpResponse]{  def apply(request: HttpRequest) = {    client(rewriteReq(request)) map { res =>      rewriteRes(res)    }  }}

其中 rewriteReq 和 rewriteRes 可以提供协议翻译,例如。

abstract class Filter[-ReqIn, +RepOut, +ReqOut, -RepIn]  extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut])

通过图示可以更清晰地看出其类型:

    ((ReqIn, Service[ReqOut, RepIn])         => Future[RepOut])          (*   Service   *)[ReqIn -> (ReqOut -> RepIn) -> RepOut]

下面的例子展示了怎样通过过滤器来提供服务超时机制。

class TimeoutFilter[Req, Rep](  timeout: Duration,  exception: RequestTimeoutException,  timer: Timer)  extends Filter[Req, Rep, Req, Rep]{  def this(timeout: Duration, timer: Timer) =    this(timeout, new IndividualRequestTimeoutException(timeout), timer)  def apply(request: Req, service: Service[Req, Rep]): Future[Rep] = {    val res = service(request)    res.within(timer, timeout) rescue {      case _: java.util.concurrent.TimeoutException =>        res.cancel()        Trace.record(TimeoutFilter.TimeoutAnnotation)        Future.exception(exception)    }  }}

这个例子展示了怎样(通过认证服务)提供身份验证来将 Service[AuthHttpReq, HttpRep] 转换为 Service[HttpReq, HttpRep]

class RequireAuthentication(authService: AuthService)  extends Filter[HttpReq, HttpRep, AuthHttpReq, HttpRep] {  def apply(    req: HttpReq,    service: Service[AuthHttpReq, HttpRep]  ) = {    authService.auth(req) flatMap {      case AuthResult(AuthResultCode.OK, Some(passport), _) =>        service(AuthHttpReq(req, passport))      case ar: AuthResult =>        Future.exception(          new RequestUnauthenticated(ar.resultCode))    }  }}

这样使用过滤器是有好处的。它可以帮助你将“身份验证逻辑”固定在一个地方。拥有一个独立的类型执行请求授权,会使追查程序安全问题变得更容易。

过滤器可以使用 andThen 组合在一起。传入一个 Service 参数给 andThen 将创建一个(添加了过滤功能)的Service(类型用来做说明)。

val authFilter: Filter[HttpReq, HttpRep, AuthHttpReq, HttpRep]val timeoutfilter[Req, Rep]: Filter[Req, Rep, Req, Rep]val serviceRequiringAuth: Service[AuthHttpReq, HttpRep]val authenticateAndTimedOut: Filter[HttpReq, HttpRep, AuthHttpReq, HttpRep] =  authFilter andThen timeoutFilterval authenticatedTimedOutService: Service[HttpReq, HttpRep] =  authenticateAndTimedOut andThen serviceRequiringAuth

生成器(Builder)

生成器把所有组件组合在一起。一个 ClientBuilder 对给定的一组参数生成一个 Service,而一个 ServerBuilder 获取一个 Service 的实例,并调度传入请求给它。为了确定 Service 的类型,我们必须提供一个编解码器(Codec)。编解码器提供底层协议的实现(如 HTTP,thrift,memcached)。这两个 Builder 都有很多参数,其中一些是必填的。

下面是一个调用 ClientBuilder 的例子(类型用来做说明)

val client: Service[HttpRequest, HttpResponse] = ClientBuilder()  .codec(Http)  .hosts("host1.twitter.com:10000,host2.twitter.com:10001,host3.twitter.com:10003")  .hostConnectionLimit(1)  .tcpConnectTimeout(1.second)  .retries(2)  .reportTo(new OstrichStatsReceiver)  .build()

这将构建一个客户端在三个主机上进行负载平衡,最多在每台主机建立一个连接,并在两次失败尝试后放弃。统计数据会报给 ostrich 。以下生成器选项是必须的(而且它们也被静态强制填写了):hosts 或 cluster, codec 和 hostConnectionLimit。

同样的,你也可以使用一个 ServerBuilder 来创建“监听”传入请求的服务:

val service = new MyService(...) // construct instance of your Finagle servicevar filter = new MyFilter(...) // and maybe some filtersvar filteredServce = filter andThen serviceval  server = ServerBuilder()  .bindTo(new InetSocketAddress(port))  .codec(ThriftServerFramedCodec())  .name("my filtered service")//  .hostConnectionMaxLifeTime(5.minutes)//  .readTimeout(2.minutes)  .build(filteredService)

通过这些参数会生成一个Thrift服务器监听端口 port,并将请求分发给 service。如果我们去掉 hostConnectionMaxLifeTime 的注释,每个连接将被允许留存长达 5 分钟。如果我们去掉 readTimeout 的注释,那么我们就需要在 2 分钟之内发送请求。ServerBuilder 必选项有:name, bindTo 和 codec。

不要阻塞(除非你用正确的方式)

Finagle 自动操纵线程来保证服务顺利运行。但是,如果你的服务阻塞了,它会阻塞所有 Finagle 线程。

如果你的代码调用了一个阻塞操作(apply 或 get),使用 Future 池来包装阻塞代码。阻塞操作将运行在自己的线程池中,返回一个 Future 来完成(或失败)这个操作,并可以和其它 Future 组合。如果你的代码中使用 Future 的顺序组合,不用担心它会“阻塞”组合中的 Future。

[1]小心,还有其它“Future”类。不要将 com.twitter.util.Future 和scala.actor.Future 或 java.util.concurrent.Future 混淆起来!

[2] 如果你学习类型系统和/或分类理论,你会高兴地发现 flatMap 相当于一元绑定。

集合

基本数据结构

Scala 提供了一些不错的集合。

参考 Effective Scala 对怎样使用集合的观点

列表 List

scala> val numbers = List(1, 2, 3, 4)numbers: List[Int] = List(1, 2, 3, 4)

集 Set

集没有重复

scala> Set(1, 1, 2)res0: scala.collection.immutable.Set[Int] = Set(1, 2)

元组 Tuple

元组是在不使用类的前提下,将元素组合起来形成简单的逻辑集合。

scala> val hostPort = ("localhost", 80)hostPort: (String, Int) = (localhost, 80)

与样本类不同,元组不能通过名称获取字段,而是使用位置下标来读取对象;而且这个下标基于 1,而不是基于 0。

scala> hostPort._1res0: String = localhostscala> hostPort._2res1: Int = 80

元组可以很好得与模式匹配相结合。

hostPort match {  case ("localhost", port) => ...  case (host, port) => ...}

在创建两个元素的元组时,可以使用特殊语法:->

scala> 1 -> 2res0: (Int, Int) = (1,2)

参考 Effective Scala 对 [解构绑定](http://twitter.github.com/effectivescala/#Functional programming-Destructuring bindings) (“拆解”一个元组)的观点。

映射 Map

它可以持有基本数据类型。

Map(1 -> 2)Map("foo" -> "bar")

这看起来像是特殊的语法,不过不要忘了上文讨论的->可以用来创建二元组。

Map()方法也使用了从第一节课学到的变参列表:Map(1 -> "one", 2 -> "two")将变为 Map((1, "one"), (2, "two")),其中第一个参数是映射的键,第二个参数是映射的值。

映射的值可以是映射甚或是函数。

Map(1 -> Map("foo" -> "bar"))Map("timesTwo" -> { timesTwo(_) })

选项 Option

Option 是一个表示有可能包含值的容器。

Option基本的接口是这样的:

trait Option[T] {  def isDefined: Boolean  def get: T  def getOrElse(t: T): T}

Option 本身是泛型的,并且有两个子类: Some[T] 或 None

我们看一个使用 Option 的例子:

Map.get 使用 Option 作为其返回值,表示这个方法也许不会返回你请求的值。

scala> val numbers = Map("one" -> 1, "two" -> 2)numbers: scala.collection.immutable.Map[java.lang.String,Int] = Map(one -> 1, two -> 2)scala> numbers.get("two")res0: Option[Int] = Some(2)scala> numbers.get("three")res1: Option[Int] = None

现在我们的数据似乎陷在 Option 中了,我们怎样获取这个数据呢?

直觉上想到的可能是在 isDefined 方法上使用条件判断来处理。

// We want to multiply the number by two, otherwise return 0.val result = if (res1.isDefined) {  res1.get * 2} else {  0}

我们建议使用 getOrElse 或模式匹配处理这个结果。

getOrElse 让你轻松地定义一个默认值。

val result = res1.getOrElse(0) * 2

模式匹配能自然地配合 Option 使用。

val result = res1 match {  case Some(n) => n * 2  case None => 0}

参考 Effective Scala 对使用 [Options](http://twitter.github.com/effectivescala/#Functional programming-Options) 的意见。

函数组合子

List(1, 2, 3) map squared 对列表中的每一个元素都应用了squared 平方函数,并返回一个新的列表 List(1, 4, 9)。我们称这个操作 map 组合子。 (如果想要更好的定义,你可能会喜欢 Stackoverflow 上对组合子的说明。)他们常被用在标准的数据结构上。

map

map 对列表中的每个元素应用一个函数,返回应用后的元素所组成的列表。

scala> numbers.map((i: Int) => i * 2)res0: List[Int] = List(2, 4, 6, 8)

或传入一个部分应用函数

scala> def timesTwo(i: Int): Int = i * 2timesTwo: (i: Int)Intscala> numbers.map(timesTwo _)res0: List[Int] = List(2, 4, 6, 8)

foreach

foreach 很像 map,但没有返回值。foreach 仅用于有副作用[side-effects]的函数。

scala> numbers.foreach((i: Int) => i * 2)

什么也没有返回。

你可以尝试存储返回值,但它会是 Unit 类型(即void)

scala> val doubled = numbers.foreach((i: Int) => i * 2)doubled: Unit = ()

filter

filter 移除任何对传入函数计算结果为 false 的元素。返回一个布尔值的函数通常被称为谓词函数[或判定函数]

scala> numbers.filter((i: Int) => i % 2 == 0)res0: List[Int] = List(2, 4)scala> def isEven(i: Int): Boolean = i % 2 == 0isEven: (i: Int)Booleanscala> numbers.filter(isEven _)res2: List[Int] = List(2, 4)

zip

zip 将两个列表的内容聚合到一个对偶列表中。

scala> List(1, 2, 3).zip(List("a", "b", "c"))res0: List[(Int, String)] = List((1,a), (2,b), (3,c))

partition

partition 将使用给定的谓词函数分割列表。

scala> val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)scala> numbers.partition(_ % 2 == 0)res0: (List[Int], List[Int]) = (List(2, 4, 6, 8, 10),List(1, 3, 5, 7, 9))

find

find 返回集合中第一个匹配谓词函数的元素。

scala> numbers.find((i: Int) => i > 5)res0: Option[Int] = Some(6)

drop & dropWhile

drop 将删除前 i 个元素

scala> numbers.drop(5)res0: List[Int] = List(6, 7, 8, 9, 10)

dropWhile 将删除元素直到找到第一个匹配谓词函数的元素。例如,如果我们在 numbers 列表上使用 dropWhile 奇数的函数, 1 将被丢弃(但 3 不会被丢弃,因为他被 2 “保护”了)。

scala> numbers.dropWhile(_ % 2 != 0)res0: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)

foldLeft

scala> numbers.foldLeft(0)((m: Int, n: Int) => m + n)res0: Int = 55

0 为初始值(记住 numbers 是 List[Int] 类型),m 作为一个累加器。

直接观察运行过程:

scala> numbers.foldLeft(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }m: 0 n: 1m: 1 n: 2m: 3 n: 3m: 6 n: 4m: 10 n: 5m: 15 n: 6m: 21 n: 7m: 28 n: 8m: 36 n: 9m: 45 n: 10res0: Int = 55

foldRight

和 foldLeft 一样,只是运行过程相反。

scala> numbers.foldRight(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }m: 10 n: 0m: 9 n: 10m: 8 n: 19m: 7 n: 27m: 6 n: 34m: 5 n: 40m: 4 n: 45m: 3 n: 49m: 2 n: 52m: 1 n: 54res0: Int = 55

flatten

flatten 将嵌套结构扁平化为一个层次的集合。

scala> List(List(1, 2), List(3, 4)).flattenres0: List[Int] = List(1, 2, 3, 4)

flatMap

flatMap 是一种常用的组合子,结合映射 [mapping] 和扁平化 [flattening]。flatMap 需要一个处理嵌套列表的函数,然后将结果串连起来。

scala> val nestedNumbers = List(List(1, 2), List(3, 4))nestedNumbers: List[List[Int]] = List(List(1, 2), List(3, 4))scala> nestedNumbers.flatMap(x => x.map(_ * 2))res0: List[Int] = List(2, 4, 6, 8)

可以把它看做是“先映射后扁平化”的快捷操作:

scala> nestedNumbers.map((x: List[Int]) => x.map(_ * 2)).flattenres1: List[Int] = List(2, 4, 6, 8)

这个例子先调用 map,然后可以马上调用 flatten,这就是“组合子”的特征,也是这些函数的本质。

参考 Effective Scala 对 [flatMap](http://twitter.github.com/effectivescala/#Functional programming-flatMap) 的意见。

扩展函数组合子

现在我们已经学过集合上的一些函数。

我们将尝试写自己的函数组合子。

有趣的是,上面所展示的每一个函数组合子都可以用 fold 方法实现。让我们看一些例子。

def ourMap(numbers: List[Int], fn: Int => Int): List[Int] = {  numbers.foldRight(List[Int]()) { (x: Int, xs: List[Int]) =>    fn(x) :: xs  }}scala> ourMap(numbers, timesTwo(_))res0: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

为什么是List[Int]()?Scala没有聪明到理解你的目的是将结果积聚在一个空的 Int 类型的列表中。

Map?

所有展示的函数组合子都可以在 Map 上使用。Map 可以被看作是一个二元组的列表,所以你写的函数要处理一个键和值的二元组。

scala> val extensions = Map("steve" -> 100, "bob" -> 101, "joe" -> 201)extensions: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101), (joe,201))

现在筛选出电话分机号码低于 200 的条目。

scala> extensions.filter((namePhone: (String, Int)) => namePhone._2 < 200)res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101))

因为参数是元组,所以你必须使用位置获取器来读取它们的键和值。

幸运的是,我们其实可以使用模式匹配更优雅地提取键和值。

scala> extensions.filter({case (name, extension) => extension < 200})res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101))

Searchbird

我们要使用 Scala 和先前介绍的 Finagle 框架构建一个简单的分布式搜索引擎。

设计目标:大图景

从广义上讲,我们的设计目标包括 抽象 (abstraction:在不知道其内部的所有细节的前提下,利用该系统功能的能力)、 模块化 (modularity:把系统分解为小而简单的片段,从而更容易被理解和/或被更换的能力)和 扩展性 (scalability:用简单直接的方法给系统扩容的能力)。

我们要描述的系统有三个部分: (1) 客户端 发出请求,(2) 服务端 接收请求并应答,和(3) 传送 机制来这些通信包装起来。通常情况下,客户端和服务器位于不同的机器上,通过网络上的一个特定的端口进行通信,但在这个例子中,它们将运行在同一台机器上(而且仍然使用端口进行通信) 。在我们的例子中,客户端和服务器将用 Scala 编写,传送协议将使用 Thrift 处理。本教程的主要目的是展示一个简单的具有良好可扩展性的服务器和客户端。

探索默认的引导程序项目

首先,使用 scala-bootstrapper 创建一个骨架项目( “ Searchbird ” )。这将创建一个简单的基于 Finagle 和 key-value 内存存储的 Scala 服务。我们将扩展这个工程以支持搜索值,并进而支持多进程多个内存存储的搜索。

$ mkdir searchbird ; cd searchbird$ scala-bootstrapper searchbirdwriting build.sbtwriting config/development.scalawriting config/production.scalawriting config/staging.scalawriting config/test.scalawriting consolewriting Gemfilewriting project/plugins.sbtwriting README.mdwriting sbtwriting src/main/scala/com/twitter/searchbird/SearchbirdConsoleClient.scalawriting src/main/scala/com/twitter/searchbird/SearchbirdServiceImpl.scalawriting src/main/scala/com/twitter/searchbird/config/SearchbirdServiceConfig.scalawriting src/main/scala/com/twitter/searchbird/Main.scalawriting src/main/thrift/searchbird.thriftwriting src/scripts/searchbird.shwriting src/scripts/config.shwriting src/scripts/devel.shwriting src/scripts/server.shwriting src/scripts/service.shwriting src/test/scala/com/twitter/searchbird/AbstractSpec.scalawriting src/test/scala/com/twitter/searchbird/SearchbirdServiceSpec.scalawriting TUTORIAL.md

首先,来看下 scala-bootstrapper 为我们创建的默认项目。这是一个模板。虽然最终将替换它的大部分内容,不过作为支架它还是很方便的。它定义了一个简单(但完整)的 key-value 存储,并包含了配置、thrift 接口、统计输出和日志记录。

在我们看代码之前,先运行一个客户端和服务器,看看它是如何工作的。这里是我们构建的:

这里是我们的服务输出的接口。由于 Searchbird 服务是一个 Thrift 服务(和我们大部分服务一样),因而其外部接口使用 Thrift IDL(“接口描述语言”)定义。

src/main/thrift/searchbird.thrift

service SearchbirdService {  string get(1: string key) throws(1: SearchbirdException ex)  void put(1: string key, 2: string value)}

这是非常直观的:我们的服务 SearchbirdService 输出两个 RPC 方法 get 和 put 。他们组成了一个到 key-value 存储的简单接口。

现在,让我们运行默认的服务,启动客户端连接到这个服务,并通过这个接口来探索他们。打开两个窗口,一个用于服务器,一个用于客户端。

在第一个窗口中,用交互模式启动 SBT(在命令行中运行 ./sbt [1]),然后构建和运行项目内 SBT。这会运行 Main.scala 定义的 主 进程。

$ ./sbt...> compile> run -f config/development.scala...[info] Running com.twitter.searchbird.Main -f config/development.scala

配置文件 (development.scala) 实例化一个新的服务,并监听 9999 端口。客户端可以连接到 9999 端口使用此服务。

现在,我们将使用 控制台 shell脚本初始化和运行一个客户端实例,即 SearchbirdConsoleClient 实例 (SearchbirdConsoleClient.scala) 。在另一个窗口中运行此脚本:

$ ./console 127.0.0.1 9999[info] Running com.twitter.searchbird.SearchbirdConsoleClient 127.0.0.1 9999'client' is bound to your thrift client.finagle-client> 

客户端对象 client 现在连接到本地计算机上的 9999 端口,并可以跟服务交互了。接下来我们发送一些请求:

scala> client.put("marius", "Marius Eriksen")res0: ...scala> client.put("stevej", "Steve Jenson")res1: ...scala> client.get("marius")res2: com.twitter.util.Future[String] = ...scala> client.get("marius").get()res3: String = Marius Eriksen

(第二个 get() 调用解析 client.get() 返回的 Future 类型值,阻塞直到该值准备好。)

该服务器还输出运行统计(配置文件中指定这些信息在 9900 端口)。这不仅方便对各个服务器进行检查,也利于聚集全局的服务统计(以机器可读的 JSON 接口)。打开第三个窗口来查看这些统计:

$ curl localhost:9900/stats.txtcounters:  Searchbird/connects: 1  Searchbird/received_bytes: 264  Searchbird/requests: 3  Searchbird/sent_bytes: 128  Searchbird/success: 3  jvm_gc_ConcurrentMarkSweep_cycles: 1  jvm_gc_ConcurrentMarkSweep_msec: 15  jvm_gc_ParNew_cycles: 24  jvm_gc_ParNew_msec: 191  jvm_gc_cycles: 25  jvm_gc_msec: 206gauges:  Searchbird/connections: 1  Searchbird/pending: 0  jvm_fd_count: 135  jvm_fd_limit: 10240  jvm_heap_committed: 85000192  jvm_heap_max: 530186240  jvm_heap_used: 54778640  jvm_nonheap_committed: 89657344  jvm_nonheap_max: 136314880  jvm_nonheap_used: 66238144  jvm_num_cpus: 4  jvm_post_gc_CMS_Old_Gen_used: 36490088  jvm_post_gc_CMS_Perm_Gen_used: 54718880  jvm_post_gc_Par_Eden_Space_used: 0  jvm_post_gc_Par_Survivor_Space_used: 1315280  jvm_post_gc_used: 92524248  jvm_start_time: 1345072684280  jvm_thread_count: 16  jvm_thread_daemon_count: 7  jvm_thread_peak_count: 16  jvm_uptime: 1671792labels:metrics:  Searchbird/handletime_us: (average=9598, count=4, maximum=19138, minimum=637, p25=637, p50=4265, p75=14175, p90=19138, p95=19138, p99=19138, p999=19138, p9999=19138, sum=38393)  Searchbird/request_latency_ms: (average=4, count=3, maximum=9, minimum=0, p25=0, p50=5, p75=9, p90=9, p95=9, p99=9, p999=9, p9999=9, sum=14)

除了我们自己的服务统计信息以外,还有一些通用的 JVM 统计。

现在,让我们来看看配置、服务器和客户端的实现代码。

…/config/SearchbirdServiceConfig.scala

配置是一个 Scala 的特质,有一个方法 apply: RuntimeEnvironment => T 来创建一些 T 。在这个意义上,配置是“工厂” 。在运行时,配置文件(通过使用Scala编译器库)被取值为一个脚本,并产生一个配置对象。 RuntimeEnvironment 是一个提供各种运行参数(命令行标志, JVM 版本,编译时间戳等)查询的一个对象。

SearchbirdServiceConfig 类就是这样一个配置类。它使用其默认值一起指定配置参数。 (Finagle 支持一个通用的跟踪系统,我们在本教程将不会介绍: Zipkin 一个集合/聚合轨迹的 分布式跟踪系统。)

class SearchbirdServiceConfig extends ServerConfig[SearchbirdService.ThriftServer] {  var thriftPort: Int = 9999  var tracerFactory: Tracer.Factory = NullTracer.factory  def apply(runtime: RuntimeEnvironment) = new SearchbirdServiceImpl(this)}

在我们的例子中,我们要创建一个 SearchbirdService.ThriftServer。这是由 thrift 代码生成器生成的服务器类型[2]

…/Main.scala

在 SBT 控制台中键入“run”调用 main ,这将配置和初始化服务器。它读取配置(在 development.scala 中指定,并会作为参数传给“run”),创建 SearchbirdService.ThriftServer ,并启动它。 RuntimeEnvironment.loadRuntimeConfig 执行配置赋值,并把自身作为一个参数来调用 apply [3]

object Main {  private val log = Logger.get(getClass)  def main(args: Array[String]) {    val runtime = RuntimeEnvironment(this, args)    val server = runtime.loadRuntimeConfig[SearchbirdService.ThriftServer]    try {      log.info("Starting SearchbirdService")      server.start()    } catch {      case e: Exception =>        log.error(e, "Failed starting SearchbirdService, exiting")        ServiceTracker.shutdown()        System.exit(1)    }  }}

…/SearchbirdServiceImpl.scala

这是实质的服务:我们用自己的实现扩展 SearchbirdService.ThriftServer 。回忆一下 thrift 为我们生成的 SearchbirdService.ThriftServer 。它为每一个 thrift 方法生成一个 Scala 方法。到目前为止,在我们的例子中生成的接口是:

trait SearchbirdService {  def put(key: String, value: String): Future[Void]  def get(key: String): Future[String]}

返回值是 Future[Value] 而不是直接返回值,可以推迟它们的计算(finagle 的文档有 Future 更多的细节)。对本教程的目的来说,你唯一需要知道的有关 Future 的知识点是,可以通过 get() 获取其值。

scala-bootstrapper 默认实现的 key-value 存储很简单:它提供了一个通过 get 和 put 访问的 数据库 数据结构。

class SearchbirdServiceImpl(config: SearchbirdServiceConfig) extends SearchbirdService.ThriftServer {  val serverName = "Searchbird"  val thriftPort = config.thriftPort  override val tracerFactory = config.tracerFactory  val database = new mutable.HashMap[String, String]()  def get(key: String) = {    database.get(key) match {      case None =>        log.debug("get %s: miss", key)        Future.exception(SearchbirdException("No such key"))      case Some(value) =>        log.debug("get %s: hit", key)        Future(value)    }  }  def put(key: String, value: String) = {    log.debug("put %s", key)    database(key) = value    Future.Unit  }  def shutdown() = {    super.shutdown(0.seconds)  }}

其结果是构建在 Scala HashMap 上的一个简单 thrift 接口。

一个简单的搜索引擎

现在,我们将扩展现有的例子,来创建一个简单的搜索引擎。然后,我们将进一步扩展它成为由多个分片组成的 分布式 搜索引擎,使我们能够适应比单台机器内存更大的语料库。

为了简单起见,我们将最小化扩展目前的 thrift 服务,以支持搜索操作。使用模型是用 put 把文件加入搜索引擎,其中每个文件包含了一系列的记号(词),那么我们就可以输入一串记号,然后搜索会返回包含这个串中所有记号的所有文件。该体系结构是与前面的例子相同,但增加了一个新的 @search@ 调用。

要实现这样一个搜索引擎需要修改以下两个文件:

src/main/thrift/searchbird.thrift

service SearchbirdService {  string get(1: string key) throws(1: SearchbirdException ex)  void put(1: string key, 2: string value)  list<string> search(1: string query)}

我们增加了一个 search 方法来搜索当前哈希表,返回其值与查询匹配的键列表。实现也很简单直观:

…/SearchbirdServiceImpl.scala

大部分修改都在这个文件中。

现在的 数据库 HashMap 保存一个正向索引来持有到文档的键映射。我们重命名它为 forward 并增加一个 倒排(reverse) 索引(映射记号到所有包含该记号的文件)。所以在 SearchbirdServiceImpl.scala 中,更换 database 定义:

val forward = new mutable.HashMap[String, String]  with mutable.SynchronizedMap[String, String]val reverse = new mutable.HashMap[String, Set[String]]  with mutable.SynchronizedMap[String, Set[String]]

在 get 调用中,使用 forward 替换 数据库 即可,在其他方面 get 保持不变(仅执行正向查找)。不过 put 还需要改变:我们还需要为文件中的每个令牌填充反向索引,把文件的键附加到令牌关联的列表中。用下面的代码替换 put 调用。给定一个特定的搜索令牌,我们现在可以使用反向映射来查找文件。

def put(key: String, value: String) = {  log.debug("put %s", key)  forward(key) = value  // serialize updaters  synchronized {    value.split(" ").toSet foreach { token =>      val current = reverse.getOrElse(token, Set())      reverse(token) = current + key    }  }  Future.Unit}

需要注意的是(即使 HashMap 是线程安全的)同时只能有一个线程可以更新倒排索引,以确保对映射条目的 读-修改-写 是一个原子操作。 (这段代码过于保守;在进行 检索-修改-写 操作时,它锁定了整个映射,而不是锁定单个条目。)。另外还要注意使用 Set 作为数据结构;这可以确保即使一个文件中两次出现同样的符号,它也只会被 foreach 循环处理一次。

这个实现仍然有一个问题,作为留给读者的一个练习:当我们用一个新文档覆盖的一个键的时候,我们诶有删除任何倒排索引中引用的旧文件。

现在进入搜索引擎的核心:新的 search 方法。他应该解析查询,寻找匹配的文档,然后对这些列表做相交操作。这将产生包含所有查询中的标记的文件列表。在 Scala 中可以很直接地表达;添加这段代码到SearchbirdServiceImpl 类中:

def search(query: String) = Future.value {  val tokens = query.split(" ")  val hits = tokens map { token => reverse.getOrElse(token, Set()) }  val intersected = hits reduceLeftOption { _ & _ } getOrElse Set()  intersected.toList}

在这段短短的代码中有几件事情是值得关注的。在构建命中列表时,如果键( token )没有被发现, getOrElse 会返回其第二个参数(在这种情况下,一个空 Set )。我们使用 left-reduce 执行实际的相交操作。特别是当 reduceLeftOption 发现 hits 为空时将不会继续尝试执行 reduce 操作。这使我们能够提供一个默认值,而不是抛出一个异常。其实这相当于:

def search(query: String) = Future.value {  val tokens = query.split(" ")  val hits = tokens map { token => reverse.getOrElse(token, Set()) }  if (hits.isEmpty)    Nil  else    hits reduceLeft { _ & _ } toList}

使用哪种方式大多是个人喜好的问题,虽然函数式风格往往会避开带有合理默认值的条件语句。

现在,我们可以尝试在控制台中实验我们新的实现。重启服务器:

$ ./sbt...> compile> run -f config/development.scala...[info] Running com.twitter.searchbird.Main -f config/development.scala

然后再从 searchbird 目录,启动客户端:

$ ./console 127.0.0.1 9999...[info] Running com.twitter.searchbird.SearchbirdConsoleClient 127.0.0.1 9999'client' is bound to your thrift client.finagle-client> 

粘贴以下说明到控制台:

client.put("basics", " values functions classes methods inheritance try catch finally expression oriented")client.put("basics", " case classes objects packages apply update functions are objects (uniform access principle) pattern")client.put("collections", " lists maps functional combinators (map foreach filter zip")client.put("pattern", " more functions! partialfunctions more pattern")client.put("type", " basic types and type polymorphism type inference variance bounds")client.put("advanced", " advanced types view bounds higher kinded types recursive types structural")client.put("simple", " all about sbt the standard scala build")client.put("more", " tour of the scala collections")client.put("testing", " write tests with specs a bdd testing framework for")client.put("concurrency", " runnable callable threads futures twitter")client.put("java", " java interop using scala from")client.put("searchbird", " building a distributed search engine using")

现在,我们可以执行一些搜索,返回包含搜索词的文件的键。

> client.search("functions").get()res12: Seq[String] = ArrayBuffer(basics)> client.search("java").get()res13: Seq[String] = ArrayBuffer(java)> client.search("java scala").get()res14: Seq[String] = ArrayBuffer(java)> client.search("functional").get()res15: Seq[String] = ArrayBuffer(collections)> client.search("sbt").get()res16: Seq[String] = ArrayBuffer(simple)> client.search("types").get()res17: Seq[String] = ArrayBuffer(type, advanced)

回想一下,如果调用返回一个 Future ,我们必须使用一个阻塞的 get() 来获取其中包含的值。我们可以使用 Future.collect 命令来创建多个并发请求,并等待所有请求成功返回:

> import com.twitter.util.Future...> Future.collect(Seq(    client.search("types"),    client.search("sbt"),    client.search("functional")  )).get()res18: Seq[Seq[String]] = ArrayBuffer(ArrayBuffer(type, advanced), ArrayBuffer(simple), ArrayBuffer(collections))

分发我们的服务

单台机器上一个简单的内存搜索引擎将无法搜索超过内存大小的语料库。现在,我们要大胆改进,用一个简单的分片计划来构建分布式节点。下面是框图:

抽象

为了帮助我们的工作,我们会先介绍另一个抽象索引来解耦 SearchbirdService 对索引实现的依赖。这是一个直观的重构。我们首先添加一个索引文件到构建 (创建文件 searchbird/src/main/scala/com/twitter/searchbird/Index.scala ):

…/Index.scala

package com.twitter.searchbirdimport scala.collection.mutableimport com.twitter.util._import com.twitter.conversions.time._import com.twitter.logging.Loggerimport com.twitter.finagle.builder.ClientBuilderimport com.twitter.finagle.thrift.ThriftClientFramedCodectrait Index {  def get(key: String): Future[String]  def put(key: String, value: String): Future[Unit]  def search(key: String): Future[List[String]]}class ResidentIndex extends Index {  val log = Logger.get(getClass)  val forward = new mutable.HashMap[String, String]    with mutable.SynchronizedMap[String, String]  val reverse = new mutable.HashMap[String, Set[String]]    with mutable.SynchronizedMap[String, Set[String]]  def get(key: String) = {    forward.get(key) match {      case None =>        log.debug("get %s: miss", key)        Future.exception(SearchbirdException("No such key"))      case Some(value) =>        log.debug("get %s: hit", key)        Future(value)    }  }  def put(key: String, value: String) = {    log.debug("put %s", key)    forward(key) = value    // admit only one updater.    synchronized {      (Set() ++ value.split(" ")) foreach { token =>        val current = reverse.get(token) getOrElse Set()        reverse(token) = current + key      }    }    Future.Unit  }  def search(query: String) = Future.value {    val tokens = query.split(" ")    val hits = tokens map { token => reverse.getOrElse(token, Set()) }    val intersected = hits reduceLeftOption { _ & _ } getOrElse Set()    intersected.toList  }}

现在,我们把 thrift 服务转换成一个简单的调度机制:为每一个索引实例提供一个 thrift 接口。这是一个强大的抽象,因为它分离了索引实现和服务实现。服务不再知道索引的任何细节;索引可以是本地的或远程的,甚至可能是许多索引的组合,但服务并不关心,索引实现可能会更改但是不用修改服务。

将 SearchbirdServiceImpl 类定义更换为以下(简单得多)的代码(其中不再包含索引实现细节)。注意初始化服务器现在需要第二个参数 Index 。

…/SearchbirdServiceImpl.scala

class SearchbirdServiceImpl(config: SearchbirdServiceConfig, index: Index) extends SearchbirdService.ThriftServer {  val serverName = "Searchbird"  val thriftPort = config.thriftPort  def get(key: String) = index.get(key)  def put(key: String, value: String) =    index.put(key, value) flatMap { _ => Future.Unit }  def search(query: String) = index.search(query)  def shutdown() = {    super.shutdown(0.seconds)  }}

…/config/SearchbirdServiceConfig.scala

相应地更新 SearchbirdServiceConfig 的 apply 调用:

class SearchbirdServiceConfig extends ServerConfig[SearchbirdService.ThriftServer] {  var thriftPort: Int = 9999  var tracerFactory: Tracer.Factory = NullTracer.factory  def apply(runtime: RuntimeEnvironment) = new SearchbirdServiceImpl(this, new ResidentIndex)}

我们将建立一个简单的分布式系统,一个主节点组织查询其子节点。为了实现这一目标,我们将需要两个新的 Index 类型。一个代表远程索引,另一种是其他多个 Index 实例的组合索引。这样我们的服务就可以实例化多个远程索引的复合索引来构建分布式索引。请注意这两个 Index 类型具有相同的接口,所以服务器不需要知道它们所连接的索引是远程的还是复合的。

…/Index.scala

在 Index.scala 中定义了 CompositeIndex :

class CompositeIndex(indices: Seq[Index]) extends Index {  require(!indices.isEmpty)  def get(key: String) = {    val queries = indices.map { idx =>      idx.get(key) map { r => Some(r) } handle { case e => None }    }    Future.collect(queries) flatMap { results =>      results.find { _.isDefined } map { _.get } match {        case Some(v) => Future.value(v)        case None => Future.exception(SearchbirdException("No such key"))      }    }  }  def put(key: String, value: String) =    Future.exception(SearchbirdException("put() not supported by CompositeIndex"))  def search(query: String) = {    val queries = indices.map { _.search(query) rescue { case _=> Future.value(Nil) } }    Future.collect(queries) map { results => (Set() ++ results.flatten) toList }  }}

组合索引构建在一组相关 Index 实例的基础上。注意它并不关心这些实例实际上是如何实现的。这种组合类型在构建不同查询机制的时候具有极大的灵活性。我们没有定义拆分机制,所以复合索引不支持 put 操作。这些请求被直接交由子节点处理。 get 的实现是查询所有子节点,并提取第一个成功的结果。如果没有成功结果的话,则抛出一个异常。注意因为没有结果是通过抛出一个异常表示的,所以我们 处理Future ,是将任何异常转换成 None 。在实际系统中,我们很可能会为遗漏值填入适当的错误码,而不是使用异常。异常在构建原型时是方便和适宜的,但不能很好地组合。为了把真正的例外和遗漏值区分开,必须要检查异常本身。相反,把这种区别直接嵌入在返回值的类型中是更好的风格。

search 像以前一样工作。和提取第一个结果不同,我们把它们组合起来,通过使用 Set 确保其唯一性。

RemoteIndex 提供了到远程服务器的一个 Index 接口。

class RemoteIndex(hosts: String) extends Index {  val transport = ClientBuilder()    .name("remoteIndex")    .hosts(hosts)    .codec(ThriftClientFramedCodec())    .hostConnectionLimit(1)    .timeout(500.milliseconds)    .build()  val client = new SearchbirdService.FinagledClient(transport)  def get(key: String) = client.get(key)  def put(key: String, value: String) = client.put(key, value) map { _ => () }  def search(query: String) = client.search(query) map { _.toList }}

这样就使用一些合理的默认值,调用代理,稍微调整类型,就构造出一个 finagle thrift 客户端。

全部放在一起

现在我们拥有了需要的所有功能。我们需要调整配置,以便能够调用一个给定的节点,不管是主节点亦或是数据分片节点。为了做到这一点,我们将通过创建一个新的配置项来在系统中枚举分片。我们还需要添加 Index 参数到我们的 SearchbirdServiceImpl 实例。然后,我们将使用命令行参数(还记得 Config 是如何做到的吗)在这两种模式中启动服务器。

…/config/SearchbirdServiceConfig.scala

class SearchbirdServiceConfig extends ServerConfig[SearchbirdService.ThriftServer] {  var thriftPort: Int = 9999  var shards: Seq[String] = Seq()  def apply(runtime: RuntimeEnvironment) = {    val index = runtime.arguments.get("shard") match {      case Some(arg) =>        val which = arg.toInt        if (which >= shards.size || which < 0)          throw new Exception("invalid shard number %d".format(which))        // override with the shard port        val Array(_, port) = shards(which).split(":")        thriftPort = port.toInt        new ResidentIndex      case None =>        require(!shards.isEmpty)        val remotes = shards map { new RemoteIndex(_) }        new CompositeIndex(remotes)    }    new SearchbirdServiceImpl(this, index)  }}

现在,我们将调整配置:添加“分片”初始化到 SearchbirdServiceConfig 的初始化中(我们可以通过端口 9000 访问分片 0,9001 访问分片 1,依次类推)。

config/development.scala

new SearchbirdServiceConfig {  // Add your own config here  shards = Seq(    "localhost:9000",    "localhost:9001",    "localhost:9002"  )  ...

注释掉 admin.httpPort 的设置(我们不希望在同一台机器上运行多个服务,而不注释的话这些服务都会试图打开相同的端口):

  // admin.httpPort = 9900

现在,如果我们不带任何参数调用我们的服务器程序,它会启动一个主节点来和所有分片通信。如果我们指定一个分片参数,它会在指定端口启动一个分片服务器。

让我们试试吧!我们将启动 3 个服务:2 个分片和 1 个主节点。首先编译改动:

$ ./sbt> compile...> exit

然后启动三个服务:

$ ./sbt 'run -f config/development.scala -D shard=0'$ ./sbt 'run -f config/development.scala -D shard=1'$ ./sbt 'run -f config/development.scala'

您可以在 3 个不同的窗口中分别运行,或在同一窗口开始依次逐个运行,等待其启动后,只用 ctrl-z 悬挂这个命令,并使用 bg 将它放在后台执行。

然后,我们将通过控制台与它们进行互动。首先,让我们填充一些数据在两个分片节点。从 searchbird 目录运行:

$ ./console localhost 9000...> client.put("fromShardA", "a value from SHARD_A")> client.put("hello", "world")
$ ./console localhost 9001...> client.put("fromShardB", "a value from SHARD_B")> client.put("hello", "world again")

一旦完成就可以退出这些控制台会话。现在通过主节点查询我们的数据库(9999 端口):

$ ./console localhost 9999[info] Running com.twitter.searchbird.SearchbirdConsoleClient localhost 9999'client' is bound to your thrift client.finagle-client> client.get("hello").get()res0: String = worldfinagle-client> client.get("fromShardC").get()SearchbirdException(No such key)...finagle-client> client.get("fromShardA").get()res2: String = a value from SHARD_Afinagle-client> client.search("hello").get()res3: Seq[String] = ArrayBuffer()finagle-client> client.search("world").get()res4: Seq[String] = ArrayBuffer(hello)finagle-client> client.search("value").get()res5: Seq[String] = ArrayBuffer(fromShardA, fromShardB)

这个设计有多个数据抽象,允许更加模块化和可扩展的实现:

  • ResidentIndex 数据结构对网络、服务器或客户端一无所知。
  • CompositeIndex 对其索引构成的底层数据结构和组合方式一无所知;它只是简单地把请求分配给他们。
  • 服务器相同的 search 接口(特质)允许服务器查询其本地数据结构(ResidentIndex) ,或分发到其他服务器(CompositeIndex) 查询,而不需要知道这个区别,这是从调用隐藏的。
  • SearchbirdServiceImpl 和 Index 现在是相互独立的模块,这使服务实现变得简单,同时实现了服务和其数据结构之间的分离。
  • 这个设计灵活到允许一个或多个远程索引运行在本地机器或远程机器上。

这个实现的可能改进将包括:

  • 当前的实现将 put() 调用发送到所有节点。取而代之,我们可以使用一个哈希表,将 put()调用只发送到一个节点,而在所有节点之间分配存储。
    • 但是值得注意的是,在这个策略下我们失去了冗余。我们怎样在不需要完全复制的前提下保持一定的冗余度呢?
  • 当系统出错时我们没有做任何有趣的处理(例如我们没有处理任何异常)。

[1]本地 ./sbt 脚本只是保证该 SBT 版本和我们知道的所有库是一致的。

[2]target/gen-scala/com/twitter/searchbird/SearchbirdService.scala

[3] 更多信息见 Ostrich’s README

模式匹配与函数组合

函数组合

让我们创建两个函数:

scala> def f(s: String) = "f(" + s + ")"f: (String)java.lang.Stringscala> def g(s: String) = "g(" + s + ")"g: (String)java.lang.String

compose

compose 组合其他函数形成一个新的函数 f(g(x))

scala> val fComposeG = f _ compose g _fComposeG: (String) => java.lang.String = <function>scala> fComposeG("yay")res0: java.lang.String = f(g(yay))

andThen

andThen 和 compose很像,但是调用顺序是先调用第一个函数,然后调用第二个,即g(f(x))

scala> val fAndThenG = f _ andThen g _fAndThenG: (String) => java.lang.String = <function>scala> fAndThenG("yay")res1: java.lang.String = g(f(yay))

柯里化 vs 偏应用

case 语句

那么究竟什么是 case 语句?

这是一个名为 PartialFunction 的函数的子类。

多个 case 语句的集合是什么?

他们是共同组合在一起的多个 PartialFunction。

理解 PartialFunction(偏函数)

对给定的输入参数类型,函数可接受该类型的任何值。换句话说,一个(Int) => String的函数可以接收任意 Int 值,并返回一个字符串。

对给定的输入参数类型,偏函数只能接受该类型的某些特定的值。一个定义为(Int) => String 的偏函数可能不能接受所有 Int 值为输入。

isDefinedAt 是 PartialFunction 的一个方法,用来确定 PartialFunction 是否能接受一个给定的参数。

注意:偏函数 PartialFunction 和我们前面提到的部分应用函数是无关的。

参考 Effective Scala 对 [PartialFunction](http://twitter.github.com/effectivescala/#Functional programming-Partial functions) 的意见。

scala> val one: PartialFunction[Int, String] = { case 1 => "one" }one: PartialFunction[Int,String] = <function1>scala> one.isDefinedAt(1)res0: Boolean = truescala> one.isDefinedAt(2)res1: Boolean = false

您可以调用一个偏函数。

scala> one(1)res2: String = one

PartialFunctions 可以使用 orElse 组成新的函数,得到的 PartialFunction 反映了是否对给定参数进行了定义。

scala> val two: PartialFunction[Int, String] = { case 2 => "two" }two: PartialFunction[Int,String] = <function1>scala> val three: PartialFunction[Int, String] = { case 3 => "three" }three: PartialFunction[Int,String] = <function1>scala> val wildcard: PartialFunction[Int, String] = { case _ => "something else" }wildcard: PartialFunction[Int,String] = <function1>scala> val partial = one orElse two orElse three orElse wildcardpartial: PartialFunction[Int,String] = <function1>scala> partial(5)res24: String = something elsescala> partial(3)res25: String = threescala> partial(2)res26: String = twoscala> partial(1)res27: String = onescala> partial(0)res28: String = something else

case 之谜

上周我们看到一些新奇的东西。我们在通常应该使用函数的地方看到了一个 case 语句。

scala> case class PhoneExt(name: String, ext: Int)defined class PhoneExtscala> val extensions = List(PhoneExt("steve", 100), PhoneExt("robey", 200))extensions: List[PhoneExt] = List(PhoneExt(steve,100), PhoneExt(robey,200))scala> extensions.filter { case PhoneExt(name, extension) => extension < 200 }res0: List[PhoneExt] = List(PhoneExt(steve,100))

为什么这段代码可以工作?

filter 使用一个函数。在这个例子中是一个谓词函数(PhoneExt) => Boolean

PartialFunction 是 Function 的子类型,所以 filter 也可以使用 PartialFunction!

类型和多态基础

什么是静态类型?

按 Pierce 的话讲:“类型系统是一个语法方法,它们根据程序计算的值的种类对程序短语进行分类,通过分类结果错误行为进行自动检查。”

类型允许你表示函数的定义域和值域。例如,从数学角度看这个定义:

f: R -> N

它告诉我们函数“f”是从实数集到自然数集的映射。

抽象地说,这就是具体类型的准确定义。类型系统给我们提供了一些更强大的方式来表达这些集合。

鉴于这些注释,编译器可以静态地 (在编译时)验证程序是合理的。也就是说,如果值(在运行时)不符合程序规定的约束,编译将失败。

一般说来,类型检查只能保证不合理的程序不能编译通过。它不能保证每一个合理的程序都可以编译通过。

随着类型系统表达能力的提高,我们可以生产更可靠的代码,因为它能够在我们运行程序之前验证程序的不变性(当然是发现类型本身的模型 bug!)。学术界一直很努力地提高类型系统的表现力,包括值依赖(value-dependent)类型!

需要注意的是,所有的类型信息会在编译时被删去,因为它已不再需要。这就是所谓的擦除。

Scala 中的类型

Scala 强大的类型系统拥有非常丰富的表现力。其主要特性有:

  • 参数化多态性 粗略地说,就是泛型编程
  • (局部)类型推断 粗略地说,就是为什么你不需要这样写代码 val i: Int = 12: Int
  • 存在量化 粗略地说,为一些没有名称的类型进行定义
  • 视窗 我们将下周学习这些;粗略地说,就是将一种类型的值“强制转换”为另一种类型

参数化多态性

多态性是在不影响静态类型丰富性的前提下,用来(给不同类型的值)编写通用代码的。

例如,如果没有参数化多态性,一个通用的列表数据结构总是看起来像这样(事实上,它看起来很像使用泛型前的Java):

scala> 2 :: 1 :: "bar" :: "foo" :: Nilres5: List[Any] = List(2, 1, bar, foo)

现在我们无法恢复其中成员的任何类型信息。

scala> res5.headres6: Any = 2

所以我们的应用程序将会退化为一系列类型转换(“asInstanceOf[]”),并且会缺乏类型安全的保障(因为这些都是动态的)。

多态性是通过指定 类型变量 实现的。

scala> def drop1[A](l: List[A]) = l.taildrop1: [A](l: List[A])List[A]scala> drop1(List(1,2,3))res1: List[Int] = List(2, 3)

Scala 有秩 1 多态性

粗略地说,这意味着在 Scala 中,有一些你想表达的类型概念“过于泛化”以至于编译器无法理解。假设你有一个函数

def toList[A](a: A) = List(a)

你希望继续泛型地使用它:

def foo[A, B](f: A => List[A], b: B) = f(b)

这段代码不能编译,因为所有的类型变量只有在调用上下文中才被固定。即使你“钉住”了类型 B:

def foo[A](f: A => List[A], i: Int) = f(i)

…你也会得到一个类型不匹配的错误。

类型推断

静态类型的一个传统反对意见是,它有大量的语法开销。Scala 通过 类型推断 来缓解这个问题。

在函数式编程语言中,类型推断的经典方法是 Hindley Milner 算法,它最早是实现在 ML 中的。

Scala 类型推断系统的实现稍有不同,但本质类似:推断约束,并试图统一类型。

例如,在 Scala 中你无法这样做:

scala> { x => x }<console>:7: error: missing parameter type       { x => x }

而在 OCaml 中你可以:

# fun x -> x;;- : 'a -> 'a = <fun>

在 Scala 中所有类型推断是 局部的 。Scala 一次分析一个表达式。例如:

scala> def id[T](x: T) = xid: [T](x: T)Tscala> val x = id(322)x: Int = 322scala> val x = id("hey")x: java.lang.String = heyscala> val x = id(Array(1,2,3,4))x: Array[Int] = Array(1, 2, 3, 4)

类型信息都保存完好,Scala 编译器为我们进行了类型推断。请注意我们并不需要明确指定返回类型。

变性 Variance

Scala 的类型系统必须同时解释类层次和多态性。类层次结构可以表达子类关系。在混合 OO 和多态性时,一个核心问题是:如果 T’T 一个子类,Container[T’]应该被看做是 Container[T] 的子类吗?变性(Variance)注解允许你表达类层次结构和多态类型之间的关系:

名称含义Scala 标记
协变covariantC[T’]是 C[T] 的子类[+T]
逆变contravariantC[T] 是 C[T’]的子类[-T]
不变invariantC[T] 和 C[T’]无关[T]

子类型关系的真正含义:对一个给定的类型T,如果T’是其子类型,你能替换它吗?

scala> class Covariant[+A]defined class Covariantscala> val cv: Covariant[AnyRef] = new Covariant[String]cv: Covariant[AnyRef] = Covariant@4035acf6scala> val cv: Covariant[String] = new Covariant[AnyRef]<console>:6: error: type mismatch; found   : Covariant[AnyRef] required: Covariant[String]       val cv: Covariant[String] = new Covariant[AnyRef]                                   ^
scala> class Contravariant[-A]defined class Contravariantscala> val cv: Contravariant[String] = new Contravariant[AnyRef]cv: Contravariant[AnyRef] = Contravariant@49fa7bascala> val fail: Contravariant[AnyRef] = new Contravariant[String]<console>:6: error: type mismatch; found   : Contravariant[String] required: Contravariant[AnyRef]       val fail: Contravariant[AnyRef] = new Contravariant[String]                                     ^

逆变似乎很奇怪。什么时候才会用到它呢?令人惊讶的是,函数特质的定义就使用了它!

trait Function1 [-T1, +R] extends AnyRef

如果你仔细从替换的角度思考一下,会发现它是非常合理的。让我们先定义一个简单的类层次结构:

scala> class Animal { val sound = "rustle" }defined class Animalscala> class Bird extends Animal { override val sound = "call" }defined class Birdscala> class Chicken extends Bird { override val sound = "cluck" }defined class Chicken

假设你需要一个以 Bird 为参数的函数:

scala> val getTweet: (Bird => String) = // TODO

标准动物库有一个函数满足了你的需求,但它的参数是 Animal。在大多数情况下,如果你说“我需要一个___,我有一个___的子类”是可以的。但是,在函数参数这里是逆变的。如果你需要一个接受参数类型 Bird 的函数变量,但却将这个变量指向了接受参数类型为 Chicken 的函数,那么给它传入一个 Duck 时就会出错。然而,如果将该变量指向一个接受参数类型为 Animal 的函数就不会有这种问题:

scala> val getTweet: (Bird => String) = ((a: Animal) => a.sound )getTweet: Bird => String = <function1>

函数的返回值类型是协变的。如果你需要一个返回 Bird 的函数,但指向的函数返回类型是 Chicken,这当然是可以的。

scala> val hatch: (() => Bird) = (() => new Chicken )hatch: () => Bird = <function0>

边界

Scala 允许你通过边界来限制多态变量。这些边界表达了子类型关系。

scala> def cacophony[T](things: Seq[T]) = things map (_.sound)<console>:7: error: value sound is not a member of type parameter T       def cacophony[T](things: Seq[T]) = things map (_.sound)                                                        ^scala> def biophony[T <: Animal](things: Seq[T]) = things map (_.sound)biophony: [T <: Animal](things: Seq[T])Seq[java.lang.String]scala> biophony(Seq(new Chicken, new Bird))res5: Seq[java.lang.String] = List(cluck, call)

类型下界也是支持的,这让逆变和巧妙协变的引入得心应手。List[+T] 是协变的;一个 Bird 的列表也是 Animal 的列表。List 定义一个操作::(elem T)返回一个加入了 elem 的新的 List。新的 List 和原来的列表具有相同的类型:

scala> val flock = List(new Bird, new Bird)flock: List[Bird] = List(Bird@7e1ec70e, Bird@169ea8d2)scala> new Chicken :: flockres53: List[Bird] = List(Chicken@56fbda05, Bird@7e1ec70e, Bird@169ea8d2)

List 同样定义了::[B >: T](x: B) 来返回一个List[B]。请注意B >: T,这指明了类型B为类型T的超类。这个方法让我们能够做正确地处理在一个List[Bird]前面加一个 Animal 的操作:

scala> new Animal :: flockres59: List[Animal] = List(Animal@11f8d3a8, Bird@7e1ec70e, Bird@169ea8d2)

注意返回类型是 Animal。

量化

有时候,你并不关心是否能够命名一个类型变量,例如:

scala> def count[A](l: List[A]) = l.sizecount: [A](List[A])Int

这时你可以使用“通配符”取而代之:

scala> def count(l: List[_]) = l.sizecount: (List[_])Int

这相当于是下面代码的简写:

scala> def count(l: List[T forSome { type T }]) = l.sizecount: (List[T forSome { type T }])Int

注意量化会的结果会变得非常难以理解:

scala> def drop1(l: List[_]) = l.taildrop1: (List[_])List[Any]

突然,我们失去了类型信息!让我们细化代码看看发生了什么:

scala> def drop1(l: List[T forSome { type T }]) = l.taildrop1: (List[T forSome { type T }])List[T forSome { type T }]

我们不能使用 T 因为类型不允许这样做。

你也可以为通配符类型变量应用边界:

scala> def hashcodes(l: Seq[_ <: AnyRef]) = l map (_.hashCode)hashcodes: (Seq[_ <: AnyRef])Seq[Int]scala> hashcodes(Seq(1,2,3))<console>:7: error: type mismatch; found   : Int(1) required: AnyRefNote: primitive types are not implicitly converted to AnyRef.You can safely force boxing by casting x.asInstanceOf[AnyRef].       hashcodes(Seq(1,2,3))                     ^scala> hashcodes(Seq("one", "two", "three"))res1: Seq[Int] = List(110182, 115276, 110339486)

参考 D. R. MacIver 写的 Scala 中的存在类型

高级类型

视界(“类型类”)

有时候,你并不需要指定一个类型是等/子/超于另一个类,你可以通过转换这个类来伪装这种关联关系。一个视界指定一个类型可以被“看作是”另一个类型。这对对象的只读操作是很有用的。

隐函数允许类型自动转换。更确切地说,在隐式函数可以帮助满足类型推断时,它们允许按需的函数应用。例如:

scala> implicit def strToInt(x: String) = x.toIntstrToInt: (x: String)Intscala> "123"res0: java.lang.String = 123scala> val y: Int = "123"y: Int = 123scala> math.max("123", 111)res1: Int = 123

视界,就像类型边界,要求对给定的类型存在这样一个函数。您可以使用<%指定类型限制,例如:

scala> class Container[A <% Int] { def addIt(x: A) = 123 + x }defined class Container

这是说 A 必须“可被视”为 Int 。让我们试试。

scala> (new Container[String]).addIt("123")res11: Int = 246scala> (new Container[Int]).addIt(123) res12: Int = 246scala> (new Container[Float]).addIt(123.2F)<console>:8: error: could not find implicit value for evidence parameter of type (Float) => Int       (new Container[Float]).addIt(123.2)        ^

其他类型限制

方法可以通过隐含参数执行更复杂的类型限制。例如,List 支持对数字内容执行 sum,但对其他内容却不行。可是 Scala 的数字类型并不都共享一个超类,所以我们不能使用T <: Number。相反,要使之能工作,Scala 的 math 库对适当的类型 T 定义了一个隐含的 Numeric[T]。 然后在 List 定义中使用它:

sum[B >: A](implicit num: Numeric[B]): B

如果你调用List(1,2).sum(),你并不需要传入一个 num 参数;它是隐式设置的。但如果你调用 List("whoop").sum(),它会抱怨无法设置 num。

在没有设定陌生的对象为 Numeric 的时候,方法可能会要求某种特定类型的“证据”。这时可以使用以下类型-关系运算符:

  • A =:= B A 必须和 B 相等
  • A <:< B A 必须是 B 的子类
  • A <%< B A 必须可以被看做是 B
scala> class Container[A](value: A) { def addIt(implicit evidence: A =:= Int) = 123 + value }defined class Containerscala> (new Container(123)).addItres11: Int = 246scala> (new Container("123")).addIt<console>:10: error: could not find implicit value for parameter evidence: =:=[java.lang.String,Int]

类似地,根据之前的隐式转换,我们可以放松约束为可视性:

scala> class Container[A](value: A) { def addIt(implicit evidence: A <%< Int) = 123 + value }defined class Containerscala> (new Container("123")).addItres15: Int = 246

使用视图进行泛型编程

在 Scala 标准库中,视图主要用于实现集合的通用函数。例如“min”函数(在 Seq[] 上)就使用了这种技术:

def min[B >: A](implicit cmp: Ordering[B]): A = {  if (isEmpty)    throw new UnsupportedOperationException("empty.min")  reduceLeft((x, y) => if (cmp.lteq(x, y)) x else y)}

其主要优点是:

  • 集合中的元素并不是必须实现 Ordered 特质,但 Ordered 的使用仍然可以执行静态类型检查。
  • 无需任何额外的库支持,你也可以定义自己的排序:
scala> List(1,2,3,4).minres0: Int = 1scala> List(1,2,3,4).min(new Ordering[Int] { def compare(a: Int, b: Int) = b compare a })res3: Int = 4

作为旁注,标准库中有视图来将 Ordered 转换为 Ordering (反之亦然)。

trait LowPriorityOrderingImplicits {  implicit def ordered[A <: Ordered[A]]: Ordering[A] = new Ordering[A] {    def compare(x: A, y: A) = x.compare(y)  }}

上下文边界和implicitly[]

Scala2.8 引入了一种串联和访问隐式参数的快捷方式。

scala> def foo[A](implicit x: Ordered[A]) {}foo: [A](implicit x: Ordered[A])Unitscala> def foo[A : Ordered] {}                        foo: [A](implicit evidence$1: Ordered[A])Unit

隐式值可能会通过 implicitly 被访问

scala> implicitly[Ordering[Int]]res37: Ordering[Int] = scala.math.Ordering$Int$@3a9291cf

相结合后往往会使用更少的代码,尤其是串联视图的时候。

更高级多态性类型和特设多态性

Scala 可以对“更高阶”的类型进行抽象。例如,假设您需要用几种类型的容器处理几种类型的数据。你可能定义了一个 Container 的接口,它可以被实现为几种类型的容器:Option、List 等。你要定义可以使用这些容器里的值的接口,但不想确定值的类型。

这类似与函数柯里化。例如,尽管“一元类型”有类似List[A]的构造函数,这意味着我们必须满足一个“级别”的类型变量来产生一个具体的类型(就像一个没有柯里化的函数需要只提供一个参数列表来被调用),更高阶的类型需要更多。

scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }scala> val container = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }container: java.lang.Object with Container[List] = $anon$1@7c8e3f75scala> container.put("hey")res24: List[java.lang.String] = List(hey)scala> container.put(123)res25: List[Int] = List(123)

注意:Container是参数化类型的多态(“容器类型”)。

如果我们结合隐式转换 implicits 使用容器,我们会得到“特设的”多态性:即对容器写泛型函数的能力。

scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }scala> implicit val listContainer = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }scala> implicit val optionContainer = new Container[Some] { def put[A](x: A) = Some(x); def get[A](m: Some[A]) = m.get }scala> def tupleize[M[_]: Container, A, B](fst: M[A], snd: M[B]) = {     | val c = implicitly[Container[M]]                                  | c.put(c.get(fst), c.get(snd))     | }tupleize: [M[_],A,B](fst: M[A],snd: M[B])(implicit evidence$1: Container[M])M[(A, B)]scala> tupleize(Some(1), Some(2))res33: Some[(Int, Int)] = Some((1,2))scala> tupleize(List(1), List(2))res34: List[(Int, Int)] = List((1,2))

F-界多态性

通常有必要来访问一个(泛型)特质的具体子类。例如,想象你有一些泛型特质,但需要可以与它的某一子类进行比较。

trait Container extends Ordered[Container]

然而,现在比较方法是必须的了

def compare(that: Container): Int

因此,我们不能访问具体子类型,例如:

class MyContainer extends Container {  def compare(that: MyContainer): Int}

编译失败,因为我们对 Container 指定了 Ordered 特质,而不是对特定子类型指定的。

为了调和这一点,我们改用F-界的多态性。

trait Container[A <: Container[A]] extends Ordered[A]

奇怪的类型!但可以看到怎样对 A 实现了 Ordered 参数化,它本身就是 Container[A]

所以,现在

class MyContainer extends Container[MyContainer] {   def compare(that: MyContainer) = 0 }

他们是有序的了:

scala> List(new MyContainer, new MyContainer, new MyContainer)res3: List[MyContainer] = List(MyContainer@30f02a6d, MyContainer@67717334, MyContainer@49428ffa)scala> List(new MyContainer, new MyContainer, new MyContainer).minres4: MyContainer = MyContainer@33dfeb30

鉴于他们都是 Container[_] 的子类型,我们可以定义另一个子类并创建 Container[_] 的一个混合列表:

scala> class YourContainer extends Container[YourContainer] { def compare(that: YourContainer) = 0 }defined class YourContainerscala> List(new MyContainer, new MyContainer, new MyContainer, new YourContainer)                   res2: List[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]]   = List(MyContainer@3be5d207, MyContainer@6d3fe849, MyContainer@7eab48a7, YourContainer@1f2f0ce9)

注意结果类型是怎样成为 YourContainerMyContainer 类型确定的下界。这是类型推断的工作。有趣的是,这种类型甚至不需要是有意义的,它只是提供了一个合乎逻辑的最大下界为列表的统一类型。如果现在我们尝试使用 Ordered 会发生什么?

(new MyContainer, new MyContainer, new MyContainer, new YourContainer).min<console>:9: error: could not find implicit value for parameter cmp:  Ordering[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]]

对统一的类型 Ordered[] 不存在了。

结构类型

Scala 支持结构类型 structural types — 类型需求由接口构造表示,而不是由具体的类型表示。

scala> def foo(x: { def get: Int }) = 123 + x.getfoo: (x: AnyRef{def get: Int})Intscala> foo(new { def get = 10 })                 res0: Int = 133

这可能在很多场景都是相当不错的,但这个实现中使用了反射,所以要注意性能!

抽象类型成员

在特质中,你可以让类型成员保持抽象。

scala> trait Foo { type A; val x: A; def getX: A = x }defined trait Fooscala> (new Foo { type A = Int; val x = 123 }).getX   res3: Int = 123scala> (new Foo { type A = String; val x = "hey" }).getXres4: java.lang.String = hey

在做依赖注入等情况下,这往往是一个有用的技巧。

您可以使用 hash 操作符来引用一个抽象类型的变量:

scala> trait Foo[M[_]] { type t[A] = M[A] }defined trait Fooscala> val x: Foo[List]#t[Int] = List(1)x: List[Int] = List(1)

类型擦除和清单

正如我们所知道的,类型信息在编译的时候会因为擦除而丢失。 Scala 的清单(Manifests)功能,使我们能够选择性地恢复类型信息。清单提供了一个隐含值,根据需要由编译器生成。

scala> class MakeFoo[A](implicit manifest: Manifest[A]) { def make: A = manifest.erasure.newInstance.asInstanceOf[A] }scala> (new MakeFoo[String]).makeres10: String = ""

案例分析: Finagle

参见: https://github.com/twitter/finagle

trait Service[-Req, +Rep] extends (Req => Future[Rep])trait Filter[-ReqIn, +RepOut, +ReqOut, -RepIn]  extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut]){  def andThen[Req2, Rep2](next: Filter[ReqOut, RepIn, Req2, Rep2]) =    new Filter[ReqIn, RepOut, Req2, Rep2] {      def apply(request: ReqIn, service: Service[Req2, Rep2]) = {        Filter.this.apply(request, new Service[ReqOut, RepIn] {          def apply(request: ReqOut): Future[RepIn] = next(request, service)          override def release() = service.release()          override def isAvailable = service.isAvailable        })      }    }  def andThen(service: Service[ReqOut, RepIn]) = new Service[ReqIn, RepOut] {    private[this] val refcounted = new RefcountedService(service)    def apply(request: ReqIn) = Filter.this.apply(request, refcounted)    override def release() = refcounted.release()    override def isAvailable = refcounted.isAvailable  }    }

一个服务可以通过过滤器对请求进行身份验证。

trait RequestWithCredentials extends Request {  def credentials: Credentials}class CredentialsFilter(credentialsParser: CredentialsParser)  extends Filter[Request, Response, RequestWithCredentials, Response]{  def apply(request: Request, service: Service[RequestWithCredentials, Response]): Future[Response] = {    val requestWithCredentials = new RequestWrapper with RequestWithCredentials {      val underlying = request      val credentials = credentialsParser(request) getOrElse NullCredentials    }    service(requestWithCredentials)  }}

注意底层服务是如何需要对请求进行身份验证的,而且还是静态验证。因此,过滤器可以被看作是服务转换器。

许多过滤器可以被组合在一起:

val upFilter =  logTransaction     andThen  handleExceptions   andThen  extractCredentials andThen  homeUser           andThen  authenticate       andThen  route

享用安全的类型吧!

简单构建工具

关于 SBT

SBT 是一个现代化的构建工具。虽然它由 Scala 编写并提供了很多 Scala 便利,但它是一个通用的构建工具。

为什么选择 SBT?

  • 明智的依赖管理
  • 使用 Ivy 做依赖管理
  • “只在请求时更新”的模型
  • 对创建任务全面的 Scala 语言支持
  • 连续执行命令
  • 在项目上下文内启动解释器

入门

译注:最新的 SBT 安装方式请参考 scala-sbt 的文档

  • 下载 jar 包地址
  • 创建一个调用这个 jar 的 SBT shell 脚本,例如
java -Xmx512M -jar sbt-launch.jar "$@"
  • 确保它是可执行的,并在你的 path 下
  • 运行 sbt 来创建项目
[local ~/projects]$ sbtProject does not exist, create new project? (y/N/s) yName: sampleOrganization: com.twitterVersion [1.0]: 1.0-SNAPSHOTScala version [2.7.7]: 2.8.1sbt version [0.7.4]:      Getting Scala 2.7.7 ...:: retrieving :: org.scala-tools.sbt#boot-scala    confs: [default]    2 artifacts copied, 0 already retrieved (9911kB/221ms)Getting org.scala-tools.sbt sbt_2.7.7 0.7.4 ...:: retrieving :: org.scala-tools.sbt#boot-app    confs: [default]    15 artifacts copied, 0 already retrieved (4096kB/167ms)[success] Successfully initialized directory structure.Getting Scala 2.8.1 ...:: retrieving :: org.scala-tools.sbt#boot-scala    confs: [default]    2 artifacts copied, 0 already retrieved (15118kB/386ms)[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1[info]    using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7

可以看到它已经以较好的形式创建了项目的快照版本。

项目布局

  • 项目 – 项目定义文件
    • project/build/.scala – 主项目定义文件
    • project/build.properties – 项目、sbt 和 Scala 版本定义
  • src/main – 你的应用程序代码出现在这里,在子目录表明代码的语言(如src/main/scala, src/main/java
  • src/main/resources – 你想要添加到 jar 包中的静态文件(如日志配置)
  • src/test – 就像 src/main,不过是对测试
  • lib_managed – 你的项目依赖的 jar文件。由 sbt update 时填充
  • target – 生成物的目标路径(如自动生成的 thrift 代码,类文件,jar包)

添加一些代码

我们将为简单的 tweet 消息创建一个简单的 JSON 解析器。将以下代码加在这个文件中src/main/scala/com/twitter/sample/SimpleParser.scala

package com.twitter.samplecase class SimpleParsed(id: Long, text: String)class SimpleParser {  val tweetRegex = ""id":(.*),"text":"(.*)"".r  def parse(str: String) = {    tweetRegex.findFirstMatchIn(str) match {      case Some(m) => {        val id = str.substring(m.start(1), m.end(1)).toInt        val text = str.substring(m.start(2), m.end(2))        Some(SimpleParsed(id, text))      }      case _ => None    }  }}

这段代码丑陋并有 bug,但应该能够编译通过。

在控制台中的测试

SBT 既可以用作命令行脚本,也可以作为构建控制台。我们将主要利用它作为构建控制台,不过大多数命令可以作为参数传递给 SBT 独立运行,如

sbt test

需要注意如果一个命令需要参数,你需要使用引号包括住整个参数路径,例如

sbt 'test-only com.twitter.sample.SampleSpec'

这种方式很奇怪。

不管怎样,要开始我们的代码工作了,启动SBT吧

[local ~/projects/sbt-sample]$ sbt[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1[info]    using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7> 

SBT 允许你启动一个 Scala REPL 并加载所有项目依赖。它会在启动控制台前编译项目的源代码,从而为我们提供一个快速测试解析器的工作台。

> console[info] [info] == compile ==[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.[info] Compiling main sources...[info] Nothing to compile.[info]   Post-analysis: 3 classes.[info] == compile ==[info] [info] == copy-test-resources ==[info] == copy-test-resources ==[info] [info] == test-compile ==[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.[info] Compiling test sources...[info] Nothing to compile.[info]   Post-analysis: 0 classes.[info] == test-compile ==[info] [info] == copy-resources ==[info] == copy-resources ==[info] [info] == console ==[info] Starting scala interpreter...[info] Welcome to Scala version 2.8.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_22).Type in expressions to have them evaluated.Type :help for more information.scala> 

我们代码编译通过了,并提供了典型的 Scala 提示符。我们将创建一个新的解析器,一个 tweet 以确保其“能工作”

scala> import com.twitter.sample._            import com.twitter.sample._scala> val tweet = """{"id":1,"text":"foo"}"""tweet: java.lang.String = {"id":1,"text":"foo"}scala> val parser = new SimpleParser          parser: com.twitter.sample.SimpleParser = com.twitter.sample.SimpleParser@71060c3escala> parser.parse(tweet)                    res0: Option[com.twitter.sample.SimpleParsed] = Some(SimpleParsed(1,"foo"}))scala> 

添加依赖

我们简单的解析器对这个非常小的输入集工作正常,但我们需要添加更多的测试并让它出错。第一步是在我们的项目中添加 specs 测试库和一个真正的 JSON 解析器。要做到这一点,我们必须超越默认的 SBT 项目布局来创建一个项目。

SBT 认为 project/build 目录中的 Scala 文件是项目定义。添加以下内容到这个文件中project/build/SampleProject.scala

import sbt._class SampleProject(info: ProjectInfo) extends DefaultProject(info) {  val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1"  val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test"}

一个项目定义是一个 SBT 类。在上面例子中,我们扩展了 SBT 的 DefaultProject。

这里是通过 val 声明依赖。SBT 使用反射来扫描项目中的所有 val 依赖,并在构建时建立依赖关系树。这里使用的语法可能是新的,但本质和 Maven 依赖是相同的

<dependency>  <groupId>org.codehaus.jackson</groupId>  <artifactId>jackson-core-asl</artifactId>  <version>1.6.1</version></dependency><dependency>  <groupId>org.scala-tools.testing</groupId>  <artifactId>specs_2.8.0</artifactId>  <version>1.6.5</version>  <scope>test</scope></dependency>

现在可以下载我们的项目依赖了。在命令行中(而不是 sbt console 中)运行 sbt update

[local ~/projects/sbt-sample]$ sbt update[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1[info]    using SampleProject with sbt 0.7.4 and Scala 2.7.7[info] [info] == update ==[info] :: retrieving :: com.twitter#sample_2.8.1 [sync][info]  confs: [compile, runtime, test, provided, system, optional, sources, javadoc][info]  1 artifacts copied, 0 already retrieved (2785kB/71ms)[info] == update ==[success] Successful.[info] [info] Total time: 1 s, completed Nov 24, 2010 8:47:26 AM[info] [info] Total session time: 2 s, completed Nov 24, 2010 8:47:26 AM[success] Build completed successfully.

你会看到 sbt 检索到 specs 库。现在还增加了一个 lib_managed 目录,并且在 lib_managed/scala_2.8.1/test目录中包含 specs_2.8.0-1.6.5.jar

添加测试

现在有了测试库,可以把下面的测试代码写入src/test/scala/com/twitter/sample/SimpleParserSpec.scala文件

package com.twitter.sampleimport org.specs._object SimpleParserSpec extends Specification {  "SimpleParser" should {    val parser = new SimpleParser()    "work with basic tweet" in {      val tweet = """{"id":1,"text":"foo"}"""      parser.parse(tweet) match {        case Some(parsed) => {          parsed.text must be_==("foo")          parsed.id must be_==(1)        }        case _ => fail("didn't parse tweet")      }    }  }}

在 SBT 控制台中运行 test

> test[info] [info] == compile ==[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.[info] Compiling main sources...[info] Nothing to compile.[info]   Post-analysis: 3 classes.[info] == compile ==[info] [info] == test-compile ==[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.[info] Compiling test sources...[info] Nothing to compile.[info]   Post-analysis: 10 classes.[info] == test-compile ==[info] [info] == copy-test-resources ==[info] == copy-test-resources ==[info] [info] == copy-resources ==[info] == copy-resources ==[info] [info] == test-start ==[info] == test-start ==[info] [info] == com.twitter.sample.SimpleParserSpec ==[info] SimpleParserSpec[info] SimpleParser should[info]   + work with basic tweet[info] == com.twitter.sample.SimpleParserSpec ==[info] [info] == test-complete ==[info] == test-complete ==[info] [info] == test-finish ==[info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0[info]  [info] All tests PASSED.[info] == test-finish ==[info] [info] == test-cleanup ==[info] == test-cleanup ==[info] [info] == test ==[info] == test ==[success] Successful.[info] [info] Total time: 0 s, completed Nov 24, 2010 8:54:45 AM> 

我们的测试通过了!现在,我们可以增加更多。运行触发动作是 SBT 提供的优秀特性之一。在动作开始添加一个波浪线会启动一个循环,在源文件发生变化时重新运行动作。让我们运行 ~test 并看看会发生什么吧。

[info] == test ==[success] Successful.[info] [info] Total time: 0 s, completed Nov 24, 2010 8:55:50 AM1. Waiting for source changes... (press enter to interrupt)

现在,让我们添加下面的测试案例

 "reject a non-JSON tweet" in {      val tweet = """"id":1,"text":"foo""""      parser.parse(tweet) match {        case Some(parsed) => fail("didn't reject a non-JSON tweet")        case e => e must be_==(None)      }    }    "ignore nested content" in {      val tweet = """{"id":1,"text":"foo","nested":{"id":2}}"""      parser.parse(tweet) match {        case Some(parsed) => {          parsed.text must be_==("foo")          parsed.id must be_==(1)        }        case _ => fail("didn't parse tweet")      }    }    "fail on partial content" in {      val tweet = """{"id":1}"""      parser.parse(tweet) match {        case Some(parsed) => fail("didn't reject a partial tweet")        case e => e must be_==(None)      }    }

在我们保存文件后,SBT 会检测到变化,运行测试,并通知我们的解析器有问题

[info] == com.twitter.sample.SimpleParserSpec ==[info] SimpleParserSpec[info] SimpleParser should[info]   + work with basic tweet[info]   x reject a non-JSON tweet[info]     didn't reject a non-JSON tweet (Specification.scala:43)[info]   x ignore nested content[info]     'foo","nested":{"id' is not equal to 'foo' (SimpleParserSpec.scala:31)[info]   + fail on partial content

因此,让我们返工实现真正的 JSON 解析器

package com.twitter.sampleimport org.codehaus.jackson._import org.codehaus.jackson.JsonToken._case class SimpleParsed(id: Long, text: String)class SimpleParser {  val parserFactory = new JsonFactory()  def parse(str: String) = {    val parser = parserFactory.createJsonParser(str)    if (parser.nextToken() == START_OBJECT) {      var token = parser.nextToken()      var textOpt:Option[String] = None      var idOpt:Option[Long] = None      while(token != null) {        if (token == FIELD_NAME) {          parser.getCurrentName() match {            case "text" => {              parser.nextToken()              textOpt = Some(parser.getText())            }            case "id" => {              parser.nextToken()              idOpt = Some(parser.getLongValue())            }            case _ => // noop          }        }        token = parser.nextToken()      }      if (textOpt.isDefined && idOpt.isDefined) {        Some(SimpleParsed(idOpt.get, textOpt.get))      } else {        None      }    } else {      None    }  }}

这是一个简单的 Jackson 解析器。当我们保存,SBT 会重新编译代码和运行测试。代码变得越来越好了!

info] SimpleParser should[info]   + work with basic tweet[info]   + reject a non-JSON tweet[info]   x ignore nested content[info]     '2' is not equal to '1' (SimpleParserSpec.scala:32)[info]   + fail on partial content[info] == com.twitter.sample.SimpleParserSpec ==

哦。我们需要检查嵌套对象。让我们在 token 读取循环处添加一些丑陋的守卫。

  def parse(str: String) = {    val parser = parserFactory.createJsonParser(str)    var nested = 0    if (parser.nextToken() == START_OBJECT) {      var token = parser.nextToken()      var textOpt:Option[String] = None      var idOpt:Option[Long] = None      while(token != null) {        if (token == FIELD_NAME && nested == 0) {          parser.getCurrentName() match {            case "text" => {              parser.nextToken()              textOpt = Some(parser.getText())            }            case "id" => {              parser.nextToken()              idOpt = Some(parser.getLongValue())            }            case _ => // noop          }        } else if (token == START_OBJECT) {          nested += 1        } else if (token == END_OBJECT) {          nested -= 1        }        token = parser.nextToken()      }      if (textOpt.isDefined && idOpt.isDefined) {        Some(SimpleParsed(idOpt.get, textOpt.get))      } else {        None      }    } else {      None    }  }

…测试通过了!

打包和发布

现在我们已经可以运行 package 命令来生成一个 jar 文件。不过我们可能要与其他组分享我们的 jar 包。要做到这一点,我们将在 StandardProject 基础上构建,这给了我们一个良好的开端。

第一步是引入 StandardProject 为 SBT 插件。插件是一种为你的构建引进依赖的方式,注意不是为你的项目引入。这些依赖关系定义在 project/plugins/Plugins.scala 文件中。添加以下代码到 Plugins.scala 文件中。

import sbt._class Plugins(info: ProjectInfo) extends PluginDefinition(info) {  val twitterMaven = "twitter.com" at "http://maven.twttr.com/"  val defaultProject = "com.twitter" % "standard-project" % "0.7.14"}

注意我们指定了一个 Maven 仓库和一个依赖。这是因为这个标准项目库是由 twitter 托管的,不在 SBT 默认检查的仓库中。

我们也将更新项目定义来扩展 StandardProject,包括 SVN 发布特质,和我们希望发布的仓库定义。修改SampleProject.scala

import sbt._import com.twitter.sbt._class SampleProject(info: ProjectInfo) extends StandardProject(info) with SubversionPublisher {  val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1"  val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test"  override def subversionRepository = Some("http://svn.local.twitter.com/maven/")}

现在如果我们运行发布操作,将看到以下输出

[info] == deliver ==IvySvn Build-Version: nullIvySvn Build-DateTime: null[info] :: delivering :: com.twitter#sample;1.0-SNAPSHOT :: 1.0-SNAPSHOT :: release :: Wed Nov 24 10:26:45 PST 2010[info]  delivering ivy file to /Users/mmcbride/projects/sbt-sample/target/ivy-1.0-SNAPSHOT.xml[info] == deliver ==[info] [info] == make-pom ==[info] Wrote /Users/mmcbride/projects/sbt-sample/target/sample-1.0-SNAPSHOT.pom[info] == make-pom ==[info] [info] == publish ==[info] :: publishing :: com.twitter#sample[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar[info]  published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom[info]  published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml[info]  published ivy to com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml[info] Binary diff deleting com/twitter/sample/1.0-SNAPSHOT[info] Commit finished r977 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010[info] Copying from com/twitter/sample/.upload to com/twitter/sample/1.0-SNAPSHOT[info] Binary diff finished : r978 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010[info] == publish ==[success] Successful.[info] [info] Total time: 4 s, completed Nov 24, 2010 10:26:47 AM

这样(一段时间后),就可以在 binaries.local.twitter.com 上看到我们发布的 jar 包。

添加任务

任务就是 Scala 函数。添加一个任务最简单的方法是,在你的项目定义中引入一个 val 定义的任务方法,如

lazy val print = task {log.info("a test action"); None}

你也可以这样加上依赖和描述

lazy val print = task {log.info("a test action"); None}.dependsOn(compile) describedAs("prints a line after compile")

刷新项目,并执行 print 操作,我们将看到以下输出

> print[info] [info] == print ==[info] a test action[info] == print ==[success] Successful.[info] [info] Total time: 0 s, completed Nov 24, 2010 11:05:12 AM> 

所以它起作用了。如果你只是在一个项目定义一个任务的话,这工作得很好。然而如果你定义的是一个插件的话,它就很不灵活了。我可能要

lazy val print = printActiondef printAction = printTask.dependsOn(compile) describedAs("prints a line after compile")def printTask = task {log.info("a test action"); None}

这可以让消费者覆盖任务本身,依赖和/或任务的描述,或动作本身。大多数 SBT 内建的动作都遵循这种模式。作为一个例子,我们可以通过修改内置打包任务来打印当前时间戳

lazy val printTimestamp = task { log.info("current time is " + System.currentTimeMillis); None}override def packageAction = super.packageAction.dependsOn(printTimestamp)

有很多例子介绍了怎样调整 SBT 默认的 StandardProject,和如何添加自定义任务。

快速参考

常用命令

  • actions – 显示这个项目中可用的动作
  • update – 下载依赖
  • compile – 编译源文件
  • test – 运行测试
  • package – 创建一个可发布的 jar 文件
  • publish-local – 在本地 ivy 缓存中安装构建好的jar包
  • publish – 将你的 jar 推到一个远程库中(如果配置了的话)

更多命令

  • test-failed – 运行所有失败的规格测试
  • test-quick – 运行任何失败的和/或依赖更新的规格
  • clean-cache – 删除 SBT 缓存各种的东西。就像 sbt 的 clean 命令
  • clean-lib – 删除 lib_managed 下的一切

更多的集合

Scala 提供了一套很好的集合实现,提供了一些集合类型的抽象。这让你的代码可以与 Foo 的集合交互,而无需担心该集合是是一个 List,还是 Set,或是任何你有的类型。

这里提供了一个很好的页面来查看各种集合的默认实现,并链接到他们的 scala 在线文档。

基础知识

表 List

标准的链表。

scala> List(1, 2, 3)res0: List[Int] = List(1, 2, 3)

你可以用函数式语言的方式连接它们。

scala> 1 :: 2 :: 3 :: Nilres1: List[Int] = List(1, 2, 3)

参考 API文档

集 Set

集没有重复

scala> Set(1, 1, 2)res2: scala.collection.immutable.Set[Int] = Set(1, 2)

参考 API文档

序列 Seq

序列有一个给定的顺序。

scala> Seq(1, 1, 2)res3: Seq[Int] = List(1, 1, 2)

(请注意返回的是一个列表。因为 Seq 是一个特质;而列表是序列的很好实现。如你所见,Seq 也是一个工厂单例对象,可以用来创建列表。)

参考 API文档

映射 Map

映射是键值容器。

scala> Map('a' -> 1, 'b' -> 2)res4: scala.collection.immutable.Map[Char,Int] = Map((a,1), (b,2))

参考 API文档

层次结构

下面介绍的都是特质,它们在可变的(mutable)和不可变的(immutable)的包中都有特定实现。

Traversable

所有集合都可以被遍历。这个特质定义了标准函数组合子。 这些组合子根据 foreach 来写,所有集合必须实现。

参考 API文档

Iterable

iterator() 方法返回一个 Iterator 来迭代元素。

参考 API文档

Seq 序列

有顺序的对象序列。

参考 API文档

Set 集

没有重复的对象集合。

参考 API文档

Map

键值对。

参考 API文档

方法

Traversable

下面所有方法在子类中都是可用的。参数和返回值的类型可能会因为子类的覆盖而看起来不同。

def head : Adef tail : Traversable[A]

这里是函数组合子定义的地方。

def map [B] (f: (A) => B) : CC[B]

返回每个元素都被 f 转化的集合

def foreach[U](f: Elem => U): Unit

在集合中的每个元素上执行 f 。

def find (p: (A) => Boolean) : Option[A]

返回匹配谓词函数的第一个元素

def filter (p: (A) => Boolean) : Traversable[A]

返回所有匹配谓词函数的元素集合

划分:

def partition (p: (A) ⇒ Boolean) : (Traversable[A], Traversable[A])

按照谓词函数把一个集合分割成两部分

def groupBy [K] (f: (A) => K) : Map[K, Traversable[A]]

转换:

有趣的是,你可以转换集合类型。

def toArray : Array[A]def toArray [B >: A] (implicit arg0: ClassManifest[B]) : Array[B]def toBuffer [B >: A] : Buffer[B]def toIndexedSeq [B >: A] : IndexedSeq[B]def toIterable : Iterable[A]def toIterator : Iterator[A]def toList : List[A]def toMap [T, U] (implicit ev: <:<[A, (T, U)]) : Map[T, U]def toSeq : Seq[A]def toSet [B >: A] : Set[B]def toStream : Stream[A]def toString () : Stringdef toTraversable : Traversable[A]

把映射转换为一个数组,您会得到一个键值对的数组。

scala> Map(1 -> 2).toArrayres41: Array[(Int, Int)] = Array((1,2))

Iterable

添加一个迭代器的访问。

  def iterator: Iterator[A]

一个迭代器能给你提供什么?

def hasNext(): Booleandef next(): A

这是非常 Java 式的。你通常不会看到在 Scala 中使用迭代器,通常更容易出现的是函数组合器或 for 循环的使用。

Set

  def contains(key: A): Boolean  def +(elem: A): Set[A]  def -(elem: A): Set[A]

Map

通过键查找的键值对的序列。

可以像这样将一个键值对列表传入 apply()

scala> Map("a" -> 1, "b" -> 2)res0: scala.collection.immutable.Map[java.lang.String,Int] = Map((a,1), (b,2))

或者像这样:

scala> Map(("a", 2), ("b", 2))res0: scala.collection.immutable.Map[java.lang.String,Int] = Map((a,2), (b,2))

题外话

什么是->?这不是特殊的语法,这是一个返回元组的方法。

scala> "a" -> 2res0: (java.lang.String, Int) = (a,2)

请记住,这仅仅是下面代码的语法糖

scala> "a".->(2)res1: (java.lang.String, Int) = (a,2)

您也可以使用++操作符构建

scala> Map.empty ++ List(("a", 1), ("b", 2), ("c", 3))res0: scala.collection.immutable.Map[java.lang.String,Int] = Map((a,1), (b,2), (c,3))

常用的子类

HashSet 和 HashMap 的快速查找,这些集合的最常用的形式。 HashSet API, HashMap API

TreeMap 是 SortedMap 的一个子类,它可以让你进行有序访问。 [TreeMap API]()

Vector 快速随机选择和快速更新。 Vector API

scala> IndexedSeq(1, 2, 3)res0: IndexedSeq[Int] = Vector(1, 2, 3)

Range 等间隔的 Int 有序序列。你经常会在 for 循环看到。 Range API

scala> for (i <- 1 to 3) { println(i) }123

Ranges 支持标准的函数组合子。

scala> (1 to 3).map { i => i }res0: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3)

默认实现

使用特质的 apply 方法会给你默认实现的实例,例如,Iterable(1, 2)会返回一个列表作为其默认实现。

scala> Iterable(1, 2)res0: Iterable[Int] = List(1, 2)

序列 Seq 也是一样的,正如我们前面所看到的

scala> Seq(1, 2)res3: Seq[Int] = List(1, 2)scala> Iterable(1, 2)res1: Iterable[Int] = List(1, 2)scala> Sequence(1, 2)warning: there were deprecation warnings; re-run with -deprecation for detailsres2: Seq[Int] = List(1, 2)

Set

scala> Set(1, 2)res31: scala.collection.immutable.Set[Int] = Set(1, 2)

一些描述性的特质

IndexedSeq 快速随机访问元素和一个快速的长度操作。"API 文档":http://www.scala-lang.org/api/current/scala/collection/IndexedSeq.html

LinearSeq 通过 head 快速访问第一个元素,也有一个快速的 tail 操作。 API 文档

可变 vs 不可变

不可变

优点

  • 在多线程中不会改变

缺点

  • 一点也不能改变

Scala 允许我们是务实的,它鼓励不变性,但不惩罚我们需要的可变性。这和 var vs. val 非常相似。我们总是先从 val 开始并在必要时回退为 var。

我们赞成使用不可改变的版本的集合,但如果性能使然,也可以切换到可变的。使用不可变集合意味着你在多线程不会意外地改变事物。

可变集合

前面讨论的所有类都是不可变的。让我们来讨论常用的可变集合。

HashMap 定义了 getOrElseUpdate, += HashMap API

scala> val numbers = collection.mutable.Map(1 -> 2)numbers: scala.collection.mutable.Map[Int,Int] = Map((1,2))scala> numbers.get(1)res0: Option[Int] = Some(2)scala> numbers.getOrElseUpdate(2, 3)res54: Int = 3scala> numbersres55: scala.collection.mutable.Map[Int,Int] = Map((2,3), (1,2))scala> numbers += (4 -> 1)res56: numbers.type = Map((2,3), (4,1), (1,2))

与 Java 生活

您可以通过 JavaConverters package 轻松地在 Java 和 Scala 的集合类型之间转换。它用 asScala 装饰常用的 Java 集合以和用 asJava 方法装饰 Scala 集合。

   import scala.collection.JavaConverters._   val sl = new scala.collection.mutable.ListBuffer[Int]   val jl : java.util.List[Int] = sl.asJava   val sl2 : scala.collection.mutable.Buffer[Int] = jl.asScala   assert(sl eq sl2)

双向转换:

scala.collection.Iterable <=> java.lang.Iterablescala.collection.Iterable <=> java.util.Collectionscala.collection.Iterator <=> java.util.{ Iterator, Enumeration }scala.collection.mutable.Buffer <=> java.util.Listscala.collection.mutable.Set <=> java.util.Setscala.collection.mutable.Map <=> java.util.{ Map, Dictionary }scala.collection.mutable.ConcurrentMap <=> java.util.concurrent.ConcurrentMap

此外,也提供了以下单向转换

scala.collection.Seq => java.util.Listscala.collection.mutable.Seq => java.util.Listscala.collection.Set => java.util.Setscala.collection.Map => java.util.Map

使用 specs 测试

扩展规格

让我们直接开始。

import org.specs._object ArithmeticSpec extends Specification {  "Arithmetic" should {    "add two numbers" in {      1 + 1 mustEqual 2    }    "add three numbers" in {      1 + 1 + 1 mustEqual 3    }  }}

Arithmetic(算术) 是一个 规范约束下的系统

add(加) 是一个上下文。

add two numbers(两个数相加),和 add three numbers(三个数字相加) 是例子。

mustEqual 表示 预期

1 mustEqual 1 是编写实际测试前使用的一种常见的预期占位符。所有的测试用例都应该至少有一个预期。

复制

注意到两个测试都是怎样将 add 加在其名称中的吗?我们可以通过嵌套预期摆脱这种重复。

import org.specs._object ArithmeticSpec extends Specification {  "Arithmetic" should {    "add" in {      "two numbers" in {        1 + 1 mustEqual 2      }      "three numbers" in {        1 + 1 + 1 mustEqual 3      }    }  }}

执行模型

object ExecSpec extends Specification {  "Mutations are isolated" should {    var x = 0    "x equals 1 if we set it." in {      x = 1      x mustEqual 1    }    "x is the default value if we don't change it" in {      x mustEqual 0    }  }}

Setup, Teardown

doBefore & doAfter

"my system" should {  doBefore { resetTheSystem() /** user-defined reset function */ }  "mess up the system" in {...}  "and again" in {...}  doAfter { cleanThingsUp() }}

注意 doBefore/doAfter 只能运行在叶子用例上。

doFirst & doLast

doFirst/doLast 用来做一次性的设置。(需要例子,我不使用这个)

"Foo" should {  doFirst { openTheCurtains() }  "test stateless methods" in {...}  "test other stateless methods" in {...}  doLast { closeTheCurtains() }}

Matchers

你有数据,并且想要确保它是正确的。让我们看看最常用的匹配器是如何帮助你的。

mustEqual

我们已经看到几个 mustEqual 的例子了。

1 mustEqual 1"a" mustEqual "a"

引用相等,值相等。

序列中的元素

val numbers = List(1, 2, 3)numbers must contain(1)numbers must not contain(4)numbers must containAll(List(1, 2, 3))numbers must containInOrder(List(1, 2, 3))List(1, List(2, 3, List(4)), 5) must haveTheSameElementsAs(List(5, List(List(4), 2, 3), 1))

映射中的元素

map must haveKey(k)map must notHaveKey(k)map must haveValue(v)map must notHaveValue(v)

数字

a must beGreaterThan(b)a must beGreaterThanOrEqualTo(b)a must beLessThan(b)a must beLessThanOrEqualTo(b)a must beCloseTo(b, delta)

Options

a must beNonea must beSome[Type]a must beSomethinga must beSome(value)

throwA

a must throwA[WhateverException]

这是一个针对trycatch块中有异常抛出的用例的简写。

您也可以期望一个特定的消息

a must throwA(WhateverException("message"))

您也可以匹配异常:

a must throwA(new Exception) like {  case Exception(m) => m.startsWith("bad")}

编写你自己的匹配器

import org.specs.matcher.Matcher

作为一个不变量

"A matcher" should {  "be created as a val" in {    val beEven = new Matcher[Int] {      def apply(n: => Int) = {        (n % 2 == 0, "%d is even".format(n), "%d is odd".format(n))      }    }    2 must beEven  }}

契约是返回一个包含三个值的元组,分别是期望是否为真、为真时的消息和为假时的消息。

作为一个样本类

case class beEven(b: Int) extends Matcher[Int]() {  def apply(n: => Int) =  (n % 2 == 0, "%d is even".format(n), "%d is odd".format(n))}

使用样本类可以增加代码的重用性。

Mocks

import org.specs.Specificationimport org.specs.mock.Mockitoclass Foo[T] {  def get(i: Int): T}object MockExampleSpec extends Specification with Mockito {  val m = mock[Foo[String]]  m.get(0) returns "one"  m.get(0)  there was one(m).get(0)  there was no(m).get(1)}

参考 Using Mockito

Spies

Spies(间谍)可以对真正的对象做一些“局部 mocking”:

val list = new LinkedList[String]val spiedList = spy(list)// methods can be stubbed on a spyspiedList.size returns 100// other methods can also be usedspiedList.add("one")spiedList.add("two")// and verification can happen on a spythere was one(spiedList).add("one")

然而,使用间谍可能会出现非常诡异的情况:

// if the list is empty, this will throws an IndexOutOfBoundsExceptionspiedList.get(0) returns "one"

这里必须使用 doReturn :

doReturn("one").when(spiedList).get(0)

在 sbt 中运行单个 specs

> test-only com.twitter.yourservice.UserSpec

将只运行那个规范。

> ~ test-only com.twitter.yourservice.UserSpec

将在一个循环中运行该测试,文件的每一次修改都将触发测试运行。

Scala 并发编程

Runnable/Callable

Runnable 接口只有一个没有返回值的方法。

trait Runnable {  def run(): Unit}Callable与之类似,除了它有一个返回值trait Callable[V] {  def call(): V}

线程

Scala 并发是建立在 Java 并发模型基础上的。

在 Sun JVM 上,对 IO 密集的任务,我们可以在一台机器运行成千上万个线程。

一个线程需要一个 Runnable。你必须调用线程的 start 方法来运行 Runnable。

scala> val hello = new Thread(new Runnable {  def run() {    println("hello world")  }})hello: java.lang.Thread = Thread[Thread-3,5,main]scala> hello.starthello world

当你看到一个类实现了 Runnable 接口,你就知道它的目的是运行在一个线程中。

单线程代码

这里有一个可以工作但有问题的代码片断。

import java.net.{Socket, ServerSocket}import java.util.concurrent.{Executors, ExecutorService}import java.util.Dateclass NetworkService(port: Int, poolSize: Int) extends Runnable {  val serverSocket = new ServerSocket(port)  def run() {    while (true) {      // This will block until a connection comes in.      val socket = serverSocket.accept()      (new Handler(socket)).run()    }  }}class Handler(socket: Socket) extends Runnable {  def message = (Thread.currentThread.getName() + "
").getBytes  def run() {    socket.getOutputStream.write(message)    socket.getOutputStream.close()  }}(new NetworkService(2020, 2)).run

每个请求都会回应当前线程的名称,所以结果始终是 main 。

这段代码的主要缺点是在同一时间,只有一个请求可以被相应!

你可以把每个请求放入一个线程中处理。只要简单改变

(new Handler(socket)).run()

(new Thread(new Handler(socket))).start()

但如果你想重用线程或者对线程的行为有其他策略呢?

Executors

随着 Java 5 的发布,它决定提供一个针对线程的更抽象的接口。

你可以通过 Executors 对象的静态方法得到一个 ExecutorService 对象。这些方法为你提供了可以通过各种政策配置的 ExecutorService ,如线程池。

下面改写我们之前的阻塞式网络服务器来允许并发请求。

import java.net.{Socket, ServerSocket}import java.util.concurrent.{Executors, ExecutorService}import java.util.Dateclass NetworkService(port: Int, poolSize: Int) extends Runnable {  val serverSocket = new ServerSocket(port)  val pool: ExecutorService = Executors.newFixedThreadPool(poolSize)  def run() {    try {      while (true) {        // This will block until a connection comes in.        val socket = serverSocket.accept()        pool.execute(new Handler(socket))      }    } finally {      pool.shutdown()    }  }}class Handler(socket: Socket) extends Runnable {  def message = (Thread.currentThread.getName() + "
").getBytes  def run() {    socket.getOutputStream.write(message)    socket.getOutputStream.close()  }}(new NetworkService(2020, 2)).run

这里有一个连接脚本展示了内部线程是如何重用的。

$ nc localhost 2020pool-1-thread-1$ nc localhost 2020pool-1-thread-2$ nc localhost 2020pool-1-thread-1$ nc localhost 2020pool-1-thread-2

Futures

Future 代表异步计算。你可以把你的计算包装在 Future 中,当你需要计算结果的时候,你只需调用一个阻塞的 get() 方法就可以了。一个 Executor 返回一个 Future 。如果使用 Finagle RPC 系统,你可以使用 Future 实例持有可能尚未到达的结果。

一个 FutureTask 是一个 Runnable 实现,就是被设计为由 Executor 运行的

val future = new FutureTask[String](new Callable[String]() {  def call(): String = {    searcher.search(target);}})executor.execute(future)

现在我需要结果,所以阻塞直到其完成。

val blockingResult = future.get()

参考 Scala School 的 Finagle 介绍中大量使用了 Future,包括一些把它们结合起来的不错的方法。以及 Effective Scala 对 [Futures](http://twitter.github.com/effectivescala/#Twitter's standard libraries-Futures) 的意见。

线程安全问题

class Person(var name: String) {  def set(changedName: String) {    name = changedName  }}

这个程序在多线程环境中是不安全的。如果有两个线程有引用到同一个 Person 实例,并调用 set ,你不能预测两个调用结束后 name 的结果。

在 Java 内存模型中,允许每个处理器把值缓存在 L1 或 L2 缓存中,所以在不同处理器上运行的两个线程都可以有自己的数据视图。

让我们来讨论一些工具,来使线程保持一致的数据视图。

三种工具

同步

互斥锁(Mutex)提供所有权语义。当你进入一个互斥体,你拥有它。同步是 JVM 中使用互斥锁最常见的方式。在这个例子中,我们会同步 Person。

在 JVM 中,你可以同步任何不为 null 的实例。

class Person(var name: String) {  def set(changedName: String) {    this.synchronized {      name = changedName    }  }}

volatile

随着 Java 5 内存模型的变化,volatile 和 synchronized 基本上是相同的,除了 volatile 允许空值。

synchronized 允许更细粒度的锁。 而 volatile 则对每次访问同步。

class Person(@volatile var name: String) {  def set(changedName: String) {    name = changedName  }}

AtomicReference

此外,在 Java 5 中还添加了一系列低级别的并发原语。 AtomicReference 类是其中之一

import java.util.concurrent.atomic.AtomicReferenceclass Person(val name: AtomicReference[String]) {  def set(changedName: String) {    name.set(changedName)  }}

这个成本是什么?

AtomicReference 是这两种选择中最昂贵的,因为你必须去通过方法调度(method dispatch)来访问值。

volatile 和 synchronized 是建立在 Java 的内置监视器基础上的。如果没有资源争用,监视器的成本很小。由于 synchronized 允许你进行更细粒度的控制权,从而会有更少的争夺,所以 synchronized 往往是最好的选择。

当你进入同步点,访问 volatile 引用,或去掉 AtomicReferences 引用时, Java 会强制处理器刷新其缓存线从而提供了一致的数据视图。

如果我错了,请大家指正。这是一个复杂的课题,我敢肯定要弄清楚这一点需要一个漫长的课堂讨论。

Java 5 的其他灵巧的工具

正如前面提到的 AtomicReference ,Java 5 带来了许多很棒的工具。

CountDownLatch

CountDownLatch 是一个简单的多线程互相通信的机制。

val doneSignal = new CountDownLatch(2)doAsyncWork(1)doAsyncWork(2)doneSignal.await()println("both workers finished!")

先不说别的,这是一个优秀的单元测试。比方说,你正在做一些异步工作,并要确保功能完成。你的函数只需要 倒数计数(countDown) 并在测试中 等待(await) 就可以了。

AtomicInteger/Long

由于对 Int 和 Long 递增是一个经常用到的任务,所以增加了 AtomicInteger 和 AtomicLong 。

AtomicBoolean

我可能不需要解释这是什么。

ReadWriteLocks

读写锁(ReadWriteLock) 使你拥有了读线程和写线程的锁控制。当写线程获取锁的时候读线程只能等待。

让我们构建一个不安全的搜索引擎

下面是一个简单的倒排索引,它不是线程安全的。我们的倒排索引按名字映射到一个给定的用户。

这里的代码天真地假设只有单个线程来访问。

注意使用了 mutable.HashMap 替代了默认的构造函数 this()

import scala.collection.mutablecase class User(name: String, id: Int)class InvertedIndex(val userMap: mutable.Map[String, User]) {  def this() = this(new mutable.HashMap[String, User])  def tokenizeName(name: String): Seq[String] = {    name.split(" ").map(_.toLowerCase)  }  def add(term: String, user: User) {    userMap += term -> user  }  def add(user: User) {    tokenizeName(user.name).foreach { term =>      add(term, user)    }  }}

这里没有写如何从索引中获取用户。稍后我们会补充。

让我们把它变为线程安全

在上面的倒排索引例子中,userMap 不能保证是线程安全的。多个客户端可以同时尝试添加项目,并有可能出现前面 Person 例子中的视图错误。

由于 userMap 不是线程安全的,那我们怎样保持在同一个时间只有一个线程能改变它呢?

你可能会考虑在做添加操作时锁定 userMap。

def add(user: User) {  userMap.synchronized {    tokenizeName(user.name).foreach { term =>      add(term, user)    }  }}

不幸的是,这个粒度太粗了。一定要试图在互斥锁以外做尽可能多的耗时的工作。还记得我说过如果不存在资源争夺,锁开销就会很小吗。如果在锁代码块里面做的工作越少,争夺就会越少。

def add(user: User) {  // tokenizeName was measured to be the most expensive operation.  val tokens = tokenizeName(user.name)  tokens.foreach { term =>    userMap.synchronized {      add(term, user)    }  }}

SynchronizedMap

我们可以通过 SynchronizedMap 特质将同步混入一个可变的 HashMap。

我们可以扩展现有的 InvertedIndex,提供给用户一个简单的方式来构建同步索引。

import scala.collection.mutable.SynchronizedMapclass SynchronizedInvertedIndex(userMap: mutable.Map[String, User]) extends InvertedIndex(userMap) {  def this() = this(new mutable.HashMap[String, User] with SynchronizedMap[String, User])}

如果你看一下其实现,你就会意识到,它只是在每个方法上加同步锁来保证其安全性,所以它很可能没有你希望的性能。

Java ConcurrentHashMap

Java 有一个很好的线程安全的 ConcurrentHashMap。值得庆幸的是,我们可以通过 JavaConverters 获得不错的 Scala 语义。

事实上,我们可以通过扩展老的不安全的代码,来无缝地接入新的线程安全 InvertedIndex。

import java.util.concurrent.ConcurrentHashMapimport scala.collection.JavaConverters._class ConcurrentInvertedIndex(userMap: collection.mutable.ConcurrentMap[String, User])    extends InvertedIndex(userMap) {  def this() = this(new ConcurrentHashMap[String, User] asScala)}

让我们加载 InvertedIndex

原始方式

trait UserMaker {  def makeUser(line: String) = line.split(",") match {    case Array(name, userid) => User(name, userid.trim().toInt)  }}class FileRecordProducer(path: String) extends UserMaker {  def run() {    Source.fromFile(path, "utf-8").getLines.foreach { line =>      index.add(makeUser(line))    }  }}

对于文件中的每一行,我们可以调用 makeUser 然后 add 到 InvertedIndex中。如果我们使用并发 InvertedIndex,我们可以并行调用 add 因为 makeUser 没有副作用,所以我们的代码已经是线程安全的了。

我们不能并行读取文件,但我们可以并行构造用户并且把它添加到索引中。

一个解决方案:生产者/消费者

异步计算的一个常见模式是把消费者和生产者分开,让他们只能通过队列(Queue) 沟通。让我们看看如何将这个模式应用在我们的搜索引擎索引中。

import java.util.concurrent.{BlockingQueue, LinkedBlockingQueue}// Concrete producerclass Producer[T](path: String, queue: BlockingQueue[T]) extends Runnable {  def run() {    Source.fromFile(path, "utf-8").getLines.foreach { line =>      queue.put(line)    }  }}// Abstract consumerabstract class Consumer[T](queue: BlockingQueue[T]) extends Runnable {  def run() {    while (true) {      val item = queue.take()      consume(item)    }  }  def consume(x: T)}val queue = new LinkedBlockingQueue[String]()// One thread for the producerval producer = new Producer[String]("users.txt", q)new Thread(producer).start()trait UserMaker {  def makeUser(line: String) = line.split(",") match {    case Array(name, userid) => User(name, userid.trim().toInt)  }}class IndexerConsumer(index: InvertedIndex, queue: BlockingQueue[String]) extends Consumer[String](queue) with UserMaker {  def consume(t: String) = index.add(makeUser(t))}// Let's pretend we have 8 cores on this machine.val cores = 8val pool = Executors.newFixedThreadPool(cores)// Submit one consumer per core.for (i <- i to cores) {  pool.submit(new IndexerConsumer[String](index, q))}

Java 与 Scala

Javap

javap 的是 JDK 附带的一个工具。不是 JRE,这里是有区别的。javap 反编译类定义,给你展示里面有什么。用法很简单

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTraitCompiled from "Scalaisms.scala"public interface com.twitter.interop.MyTrait extends scala.ScalaObject{    public abstract java.lang.String traitName();    public abstract java.lang.String upperTraitName();}

如果你是底层控可以看看字节码

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap -c MyTrait$classCompiled from "Scalaisms.scala"public abstract class com.twitter.interop.MyTrait$class extends java.lang.Object{public static java.lang.String upperTraitName(com.twitter.interop.MyTrait);  Code:   0:   aload_0   1:   invokeinterface #12,  1; //InterfaceMethod com/twitter/interop/MyTrait.traitName:()Ljava/lang/String;   6:   invokevirtual   #17; //Method java/lang/String.toUpperCase:()Ljava/lang/String;   9:   areturnpublic static void $init$(com.twitter.interop.MyTrait);  Code:   0:   return}

如果你搞不清为什么程序在 Java 上不起作用,就用 javap 看看吧!

在 Java 中使用 Scala 类 时要考虑的四个要点

  • 类参数
  • 类常量
  • 类变量
  • 异常

我们将构建一个简单的 Scala 类来展示这一系列实体

package com.twitter.interopimport java.io.IOExceptionimport scala.throwsimport scala.reflect.{BeanProperty, BooleanBeanProperty}class SimpleClass(name: String, val acc: String, @BeanProperty var mutable: String) {  val foo = "foo"  var bar = "bar"  @BeanProperty  val fooBean = "foobean"  @BeanProperty  var barBean = "barbean"  @BooleanBeanProperty  var awesome = true  def dangerFoo() = {    throw new IOException("SURPRISE!")  }  @throws(classOf[IOException])  def dangerBar() = {    throw new IOException("NO SURPRISE!")  }}

类参数

  • 默认情况下,类参数都是有效的 Java 构造函数的参数。这意味着你不能从类的外部访问。
  • 声明一个类参数为 val/var 和这段代码是相同的
class SimpleClass(acc_: String) {  val acc = acc_}

这使得它在 Java 代码中就像其他常量一样可以被访问

类常量

  • 常量(val)在 Java 中定义了一个获取方法。你可以通过方法“foo()”访问“val foo”的值

类变量

  • 变量(var)会生成一个 _$eq 方法。你可以这样调用它
foo$_eq("newfoo");

BeanProperty

你可以通过 @BeanProperty 注解 val 和 var 定义。这会按照 POJO 定义生成 getter/setter 方法。如果你想生成 isFoo 方法,使用 BooleanBeanProperty 注解。丑陋的 foo$_eq 将变为

setFoo("newfoo");getFoo();

异常

Scala 没有像 Java 那样有受检异常(checked exception)。需不需要受检异常是一个我们不会进入的哲学辩论,不过当你需要在Java中捕获它时就 很重要 了。dangerFoo 和 dangerBar 将演示这一点。在 Java 中不能这样做

        // exception erasure!        try {            s.dangerFoo();        } catch (IOException e) {            // UGLY        }

Java 会抱怨说 s.dangerFoo 从未抛出过 IOException 异常。我们可以通过捕获 Throwable 来跳过,但是这样不好。

相反,作为一个良好的 Scala 公民,可以很体面地像在 dangerBar 中那样使用 throws 注解。这使我们能够继续在 Java 中使用受检异常。

进一步阅读

支持 Java 互操作的 Scala 注解的完整列表在这里 http://www.scala-lang.org/node/106

特质

你如何获得一个接口+实现?让我们看一个简单的特质定义

trait MyTrait {  def traitName:String  def upperTraitName = traitName.toUpperCase}

这个特质有一个抽象方法(traitName)和一个实现的方法(upperTraitName)。Scala 为我们生成了什么呢?一个名为 MyTrait 的的接口,和一个名为 MyTrait$class 的实现类。

MyTrait 和你期望的一样

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTraitCompiled from "Scalaisms.scala"public interface com.twitter.interop.MyTrait extends scala.ScalaObject{    public abstract java.lang.String traitName();    public abstract java.lang.String upperTraitName();}

MyTrait$class 更有趣

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTrait$classCompiled from "Scalaisms.scala"public abstract class com.twitter.interop.MyTrait$class extends java.lang.Object{    public static java.lang.String upperTraitName(com.twitter.interop.MyTrait);    public static void $init$(com.twitter.interop.MyTrait);}

MyTrait$class 只有以 MyTrait 实例为参数的静态方法。这给了我们一个如何在 Java 中来扩展一个特质的提示。

首先尝试下面的操作

package com.twitter.interop;public class JTraitImpl implements MyTrait {    private String name = null;    public JTraitImpl(String name) {        this.name = name;    }    public String traitName() {        return name;    }}

我们会得到以下错误

[info] Compiling main sources...[error] /Users/mmcbride/projects/interop/src/main/java/com/twitter/interop/JTraitImpl.java:3: com.twitter.interop.JTraitImpl is not abstract and does not override abstract method upperTraitName() in com.twitter.interop.MyTrait[error] public class JTraitImpl implements MyTrait {[error]        ^

我们 可以 自己实现。但有一个鬼鬼祟祟的方式。

package com.twitter.interop;    public String upperTraitName() {        return MyTrait$class.upperTraitName(this);    }

我们只要把调用代理到生成的 Scala 实现上就可以了。如果愿意我们也可以覆盖它。

单例对象

单例对象是 Scala 实现静态方法/单例模式的方式。在 Java 中使用它会有点奇怪。没有一个使用它们的完美风格,但在 Scala2.8 中用起来并不很糟糕

一个 Scala 单例对象会被编译成由“$”结尾的类。让我们创建一个类和一个伴生对象

class TraitImpl(name: String) extends MyTrait {  def traitName = name}object TraitImpl {  def apply = new TraitImpl("foo")  def apply(name: String) = new TraitImpl(name)}

我们可以像这样天真地在 Java 中访问

MyTrait foo = TraitImpl$.MODULE$.apply("foo");

现在你可能会问自己,这是神马玩意?这是一个正常的反应。让我们来看看 TraitImpl$ 里面实际上是什么

local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap TraitImpl$Compiled from "Scalaisms.scala"public final class com.twitter.interop.TraitImpl$ extends java.lang.Object implements scala.ScalaObject{    public static final com.twitter.interop.TraitImpl$ MODULE$;    public static {};    public com.twitter.interop.TraitImpl apply();    public com.twitter.interop.TraitImpl apply(java.lang.String);}

其实它里面没有任何静态方法。取而代之的是一个名为MODULE$的静态成员。方法实现被委托给该成员。这使得访问代码很难看,但却是可行的。

转发方法(Forwarding Methods)

在 Scala2.8 中处理单例对象变得相对容易一点。如果你有一个类与一个伴生对象,2.8 编译器会生成转发方法在伴生类中。所以,如果你用 2.8,你可以像这样调用 TraitImpl 单例对象的方法

MyTrait foo = TraitImpl.apply("foo");

闭包函数

Scala 的最重要的特点之一,就是把函数作为头等公民。让我们来定义一个类,它定义了一些以函数作为参数的方法。

class ClosureClass {  def printResult[T](f: => T) = {    println(f)  }  def printResult[T](f: String => T) = {    println(f("HI THERE"))  }}

在 Scala 中可以像这样调用

val cc = new ClosureClasscc.printResult { "HI MOM" }

在 Java 中却不那么容易,不过也并不可怕。让我们来看看 ClosureClass 实际上被编译成什么:

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap ClosureClassCompiled from "Scalaisms.scala"public class com.twitter.interop.ClosureClass extends java.lang.Object implements scala.ScalaObject{    public void printResult(scala.Function0);    public void printResult(scala.Function1);    public com.twitter.interop.ClosureClass();}

这也不是那么恐怖。“f: => T”被转义成“Function0”,“f: String => T”被转义成“Function1”。Scala 实际上从 Function0 定义到 Function22,最多支持 22 个参数。这真的应该足够了。

现在我们只需要弄清楚如何在 Java 中使用这些东东。我们可以传入 Scala 提供的 AbstractFunction0 和 AbstractFunction1,像这样

    @Test public void closureTest() {        ClosureClass c = new ClosureClass();        c.printResult(new AbstractFunction0() {                public String apply() {                    return "foo";                }            });        c.printResult(new AbstractFunction1<String, String>() {                public String apply(String arg) {                    return arg + "foo";                }            });    }

注意我们可以使用泛型参数。

//www.51coolma.cn/scala/scala-index.html

Scala 教程

Scala 是一门多范式的编程语言,一种类似 Java 的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。

我们认为最有意义的学习方式是,不要把 Scala 看做是改进的 Java,而是把它作为一门新的语言。所以这里不会介绍 Java 的使用经验,而将集中讲解解释器和“对象-函数式”的风格,以及 Scala 编程风格。特别强调了可维护性,并发,常用工具和利用类型系统的优势。

适用人群

本教程是为有经验的工程师准备,所以建议读者学习前,了解下 Java 语言的基础或 Scala 入门教程。


学习前提

Scala 是一种相对较新的语言,但借鉴了许多熟悉的概念,所以在学习前,我们假设您已经对面向对象编程、并发、可维护等基本编程思想有了一定的了解。

鸣谢:http://twitter.github.io/scala_school/zh_cn/index.html


5

版本信息

书中演示代码基于以下版本:

语言版本信息
Scala2.8.0

基础

关于这节课

最初的几个星期将涵盖基本语法和概念,然后我们将通过更多的练习展开这些内容。

有一些例子是以解释器交互的形式给出的,另一些则是以源文件的形式给出的。

安装一个解释器,可以使探索问题空间变得更容易。

为什么选择 Scala?

  • 表达能力
    • 函数是一等公民
    • 闭包
  • 简洁
    • 类型推断
    • 函数创建的文法支持
  • Java互操作性
    • 可重用 Java 库
    • 可重用 Java 工具
    • 没有性能惩罚

Scala 如何工作?

  • 编译成 Java 字节码
  • 可在任何标准 JVM 上运行
    • 甚至是一些不规范的JVM上,如 Dalvik
    • Scala 编译器是 Java 编译器的作者写的

用 Scala 思考

Scala 不仅仅是更好的 Java。你应该用全新的头脑来学习它,你会从这些课程中认识到这一点的。

安装 Scala 请看:Scala 安装及环境配置

启动解释器

使用自带的 sbt console 启动。

$ sbt console[...]Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_20).Type in expressions to have them evaluated.Type :help for more information.scala>

表达式

scala> 1 + 1res0: Int = 2

res0 是解释器自动创建的变量名称,用来指代表达式的计算结果。它是 Int 类型,值为 2。

Scala 中(几乎)一切都是表达式。

你可以给一个表达式的结果起个名字赋成一个不变量(val)。

scala> val two = 1 + 1two: Int = 2

你不能改变这个不变量的值。

变量

如果你需要修改这个名称和结果的绑定,可以选择使用 var

scala> var name = "steve"name: java.lang.String = stevescala> name = "marius"name: java.lang.String = marius

函数

你可以使用 def 创建函数.

scala> def addOne(m: Int): Int = m + 1addOne: (m: Int)Int

在 Scala 中,你需要为函数参数指定类型签名。

scala> val three = addOne(2)three: Int = 3

如果函数不带参数,你可以不写括号。

scala> def three() = 1 + 2three: ()Intscala> three()res2: Int = 3scala> threeres3: Int = 3

匿名函数

你可以创建匿名函数。

scala> (x: Int) => x + 1res2: (Int) => Int = <function1>

这个函数为名为 x 的 Int 变量加 1。

scala> res2(1)res3: Int = 2

你可以传递匿名函数,或将其保存成不变量。

scala> val addOne = (x: Int) => x + 1addOne: (Int) => Int = <function1>scala> addOne(1)res4: Int = 2

如果你的函数有很多表达式,可以使用 {} 来格式化代码,使之易读。

def timesTwo(i: Int): Int = {     println("hello world")  i * 2}

对匿名函数也是这样的。

scala> { i: Int =>      println("hello world")      i * 2}res0: (Int) => Int = <function1>

在将一个匿名函数作为参数进行传递时,这个语法会经常被用到。

部分应用(Partial application)

你可以使用下划线“_”部分应用一个函数,结果将得到另一个函数。Scala 使用下划线表示不同上下文中的不同事物,你通常可以把它看作是一个没有命名的神奇通配符。在`{ + 2 }`的上下文中,它代表一个匿名参数。你可以这样使用它:

scala> def adder(m: Int, n: Int) = m + nadder: (m: Int,n: Int)Intscala> val add2 = adder(2, _:Int)add2: (Int) => Int = <function1>scala> add2(3)res50: Int = 5

你可以部分应用参数列表中的任意参数,而不仅仅是最后一个。

柯里化函数

有时会有这样的需求:允许别人一会在你的函数上应用一些参数,然后又应用另外的一些参数。

例如一个乘法函数,在一个场景需要选择乘数,而另一个场景需要选择被乘数。

scala> def multiply(m: Int)(n: Int): Int = m * nmultiply: (m: Int)(n: Int)Int

你可以直接传入两个参数。

scala> multiply(2)(3)res0: Int = 6

你可以填上第一个参数并且部分应用第二个参数。

scala> val timesTwo = multiply(2) _timesTwo: (Int) => Int = <function1>scala> timesTwo(3)res1: Int = 6

你可以对任何多参数函数执行柯里化。例如之前的 adder 函数

第一次传参是一个加数,返回一个函数,调用第二个函数传参,另一个数,得出结果是和

scala> (adder _).curriedres1: (Int) => (Int) => Int = <function1>

scala> res1(2)

res2: (Int) => Int = <function1>

scala> res2(3)

res3: Int = 5

可变长度参数

这是一个特殊的语法,可以向方法传入任意多个同类型的参数。例如要在多个字符串上执行 String 的 capitalize 函数,可以这样写:

def capitalizeAll(args: String*) = {    args.map { arg =>    arg.capitalize  }}scala> capitalizeAll("rarity", "applejack")res2: Seq[String] = ArrayBuffer(Rarity, Applejack)

scala> class Calculator {     |   val brand: String = "HP"         |   def add(m: Int, n: Int): Int = m + n     | }defined class Calculatorscala> val calc = new Calculatorcalc: Calculator = Calculator@e75a11scala> calc.add(1, 2)res1: Int = 3scala> calc.brandres2: String = "HP"

上面的例子展示了如何在类中用 def 定义方法和用 val 定义字段值。方法就是可以访问类的状态的函数。

构造函数

构造函数不是特殊的方法,他们是除了类的方法定义之外的代码。让我们扩展计算器的例子,增加一个构造函数参数,并用它来初始化内部状态。

class Calculator(brand: String) {  /**   * A constructor.  */  val color: String = if (brand == "TI") {    "blue"  } else if (brand == "HP") {    "black"  } else {    "white"  }  // An instance method.  def add(m: Int, n: Int): Int = m + n}

注意两种不同风格的评论。

你可以使用构造函数来构造一个实例:

scala> val calc = new Calculator("HP")calc: Calculator = Calculator@1e64cc4dscala> calc.colorres0: String = black

表达式

上文的 Calculator 例子说明了 Scala 是如何面向表达式的。颜色的值就是绑定在一个if/else表达式上的。Scala 是高度面向表达式的:大多数东西都是表达式而非指令。

旁白: 函数 vs 方法

函数和方法在很大程度上是可以互换的。由于函数和方法是如此的相似,你可能都不知道你调用的东西是一个函数还是一个方法。而当真正碰到的方法和函数之间的差异的时候,你可能会感到困惑。

scala> class C {     |   var acc = 0     |   def minc = { acc += 1 }     |   val finc = { () => acc += 1 }     | }defined class Cscala> val c = new Cc: C = C@1af1bd6scala> c.minc // calls c.minc()scala> c.finc // returns the function as a value:res2: () => Unit = <function0>

当你可以调用一个不带括号的“函数”,但是对另一个却必须加上括号的时候,你可能会想哎呀,我还以为自己知道 Scala 是怎么工作的呢。也许他们有时需要括号?你可能以为自己用的是函数,但实际使用的是方法。

在实践中,即使不理解方法和函数上的区别,你也可以用 Scala 做伟大的事情。如果你是 Scala 新手,而且在读两者的差异解释,你可能会跟不上。不过这并不意味着你在使用 Scala 上有麻烦。它只是意味着函数和方法之间的差异是很微妙的,只有深入语言内部才能清楚理解它。

继承

class ScientificCalculator(brand: String) extends Calculator(brand) {  def log(m: Double, base: Double) = math.log(m) / math.log(base)}

参考 Effective Scala 指出如果子类与父类实际上没有区别,类型别名是优于继承的。A Tour of Scala 详细介绍了子类化

重载方法

class EvenMoreScientificCalculator(brand: String) extends ScientificCalculator(brand) {  def log(m: Int): Double = log(m, math.exp(1))}

抽象类

你可以定义一个抽象类,它定义了一些方法但没有实现它们。取而代之是由扩展抽象类的子类定义这些方法。你不能创建抽象类的实例。

scala> abstract class Shape {     |   def getArea():Int    //子类应定义为这个     | }defined class Shapescala> class Circle(r: Int) extends Shape {     |   def getArea():Int = { r * r * 3 }     | }defined class Circlescala> val s = new Shape<console>:8: error: class Shape is abstract; cannot be instantiated       val s = new Shape               ^scala> val c = new Circle(2)c: Circle = Circle@65c0035b

特质(Traits)

特质是一些字段和行为的集合,可以扩展或混入(mixin)你的类中。

trait Car {  val brand: String}trait Shiny {  val shineRefraction: Int}class BMW extends Car {  val brand = "BMW"}

通过 with 关键字,一个类可以扩展多个特质:

class BMW extends Car with Shiny {  val brand = "BMW"  val shineRefraction = 12}

参考 Effective Scala 对特质的观点

什么时候应该使用特质而不是抽象类? 如果你想定义一个类似接口的类型,你可能会在特质和抽象类之间难以取舍。这两种形式都可以让你定义一个类型的一些行为,并要求继承者定义一些其他行为。一些经验法则:

  • 优先使用特质。一个类扩展多个特质是很方便的,但却只能扩展一个抽象类。
  • 如果你需要构造函数参数,使用抽象类。因为抽象类可以定义带参数的构造函数,而特质不行。例如,你不能说trait t(i: Int) {},参数i是非法的。

类型

此前,我们定义了一个函数的参数为 Int,表示输入是一个数字类型。其实函数也可以是泛型的,来适用于所有类型。当这种情况发生时,你会看到用方括号语法引入的类型参数。下面的例子展示了一个使用泛型键和值的缓存。

trait Cache[K, V] {  def get(key: K): V  def put(key: K, value: V)  def delete(key: K)}

方法也可以引入类型参数。

def remove[K](key: K)


基础知识(续)

apply 方法

当类或对象有一个主要用途的时候,apply 方法为你提供了一个很好的语法糖。

scala> class Foo {}defined class Fooscala> object FooMaker {     |   def apply() = new Foo     | }defined module FooMakerscala> val newFoo = FooMaker()newFoo: Foo = Foo@5b83f762

scala> class Bar {     |   def apply() = 0     | }defined class Barscala> val bar = new Barbar: Bar = Bar@47711479scala> bar()res8: Int = 0

在这里,我们实例化对象看起来像是在调用一个方法。以后会有更多介绍!

单例对象

单例对象用于持有一个类的唯一实例。通常用于工厂模式。

object Timer {  var count = 0  def currentCount(): Long = {    count += 1    count  }}

可以这样使用:

scala> Timer.currentCount()res0: Long = 1

单例对象可以和类具有相同的名称,此时该对象也被称为“伴生对象”。我们通常将伴生对象作为工厂使用。

下面是一个简单的例子,可以不需要使用new来创建一个实例了。

class Bar(foo: String)object Bar {  def apply(foo: String) = new Bar(foo)}

函数即对象

在 Scala 中,我们经常谈论对象的函数式编程。这是什么意思?到底什么是函数呢?

函数是一些特质的集合。具体来说,具有一个参数的函数是 Function1 特质的一个实例。这个特征定义了 apply()语法糖,让你调用一个对象时就像你在调用一个函数。

scala> object addOne extends Function1[Int, Int] {     |   def apply(m: Int): Int = m + 1     | }defined module addOnescala> addOne(1)res2: Int = 2

这个 Function 特质集合下标从 0 开始一直到 22。为什么是 22?这是一个主观的魔幻数字(magic number)。我从来没有使用过多于 22 个参数的函数,所以这个数字似乎是合理的。

apply 语法糖有助于统一对象和函数式编程的二重性。你可以传递类,并把它们当做函数使用,而函数本质上是类的实例。

这是否意味着,当你在类中定义一个方法时,得到的实际上是一个 Function*的实例?不是的,在类中定义的方法是方法而不是函数。在 repl 中独立定义的方法是 Function* 的实例。

类也可以扩展 Function,这些类的实例可以使用()调用。

scala> class AddOne extends Function1[Int, Int] {     |   def apply(m: Int): Int = m + 1     | }defined class AddOnescala> val plusOne = new AddOne()plusOne: AddOne = <function1>scala> plusOne(1)res0: Int = 2

可以使用更直观快捷的 extends (Int => Int) 代替 extends Function1[Int, Int]

class AddOne extends (Int => Int) {  def apply(m: Int): Int = m + 1}

你可以将代码组织在包里。

package com.twitter.example

在文件头部定义包,会将文件中所有的代码声明在那个包中。

值和函数不能在类或单例对象之外定义。单例对象是组织静态函数(static function)的有效工具。

package com.twitter.exampleobject colorHolder {  val BLUE = "Blue"  val RED = "Red"}

现在你可以直接访问这些成员

println("the color is: " + com.twitter.example.colorHolder.BLUE)

注意在你定义这个对象时 Scala 解释器的返回:

scala> object colorHolder {     |   val Blue = "Blue"     |   val Red = "Red"     | }defined module colorHolder

这暗示了 Scala 的设计者是把对象作为 Scala 的模块系统的一部分进行设计的。

模式匹配

这是 Scala 中最有用的部分之一。

匹配值

val times = 1times match {  case 1 => "one"  case 2 => "two"  case _ => "some other number"}

使用守卫进行匹配

times match {  case i if i == 1 => "one"  case i if i == 2 => "two"  case _ => "some other number"}

注意我们是怎样将值赋给变量i的。

在最后一行指令中的_是一个通配符;它保证了我们可以处理所有的情况。

否则当传进一个不能被匹配的数字的时候,你将获得一个运行时错误。我们以后会继续讨论这个话题的。

参考 Effective Scala 对[什么时候使用模式匹配](http://twitter.github.com/effectivescala/#Functional programming-Pattern matching)和[模式匹配格式化的建议](http://twitter.github.com/effectivescala/#Formatting-Pattern matching)。 A Tour of Scala 也描述了模式匹配

匹配类型

你可以使用 match 来分别处理不同类型的值。

def bigger(o: Any): Any = {  o match {    case i: Int if i < 0 => i - 1    case i: Int => i + 1    case d: Double if d < 0.0 => d - 0.1    case d: Double => d + 0.1    case text: String => text + "s"  }}

匹配类成员

还记得我们之前的计算器吗。

让我们通过类型对它们进行分类。

def calcType(calc: Calculator) = calc match {  case _ if calc.brand == "hp" && calc.model == "20B" => "financial"  case _ if calc.brand == "hp" && calc.model == "48G" => "scientific"  case _ if calc.brand == "hp" && calc.model == "30B" => "business"  case _ => "unknown"}

样本类 Case Classes

使用样本类可以方便得存储和匹配类的内容。你不用 new 关键字就可以创建它们。

scala> case class Calculator(brand: String, model: String)defined class Calculatorscala> val hp20b = Calculator("hp", "20b")hp20b: Calculator = Calculator(hp,20b)

样本类基于构造函数的参数,自动地实现了相等性和易读的 toString 方法。

scala> val hp20b = Calculator("hp", "20b")hp20b: Calculator = Calculator(hp,20b)scala> val hp20B = Calculator("hp", "20b")hp20B: Calculator = Calculator(hp,20b)scala> hp20b == hp20Bres6: Boolean = true

样本类也可以像普通类那样拥有方法。

使用样本类进行模式匹配

样本类就是被设计用在模式匹配中的。让我们简化之前的计算器分类器的例子。

val hp20b = Calculator("hp", "20B")val hp30b = Calculator("hp", "30B")def calcType(calc: Calculator) = calc match {  case Calculator("hp", "20B") => "financial"  case Calculator("hp", "48G") => "scientific"  case Calculator("hp", "30B") => "business"  case Calculator(ourBrand, ourModel) => "Calculator: %s %s is of unknown type".format(ourBrand, ourModel)}

最后一句也可以这样写

  case Calculator(_, _) => "Calculator of unknown type"

或者我们完全可以不将匹配对象指定为 Calculator 类型

  case _ => "Calculator of unknown type"

或者我们也可以将匹配的值重新命名。

  case c@Calculator(_, _) => "Calculator: %s of unknown type".format(c)

异常

Scala 中的异常可以在 try-catch-finally 语法中通过模式匹配使用。

try {  remoteCalculatorService.add(1, 2)} catch {  case e: ServerIsDownException => log.error(e, "the remote calculator service is unavailable. should have kept your trusty HP.")} finally {  remoteCalculatorService.close()}

try 也是面向表达式的

val result: Int = try {  remoteCalculatorService.add(1, 2)} catch {  case e: ServerIsDownException => {    log.error(e, "the remote calculator service is unavailable. should have kept your trusty HP.")    0  }} finally {  remoteCalculatorService.close()}

这并不是一个完美编程风格的展示,而只是一个例子,用来说明 try-catch-finally 和 Scala 中其他大部分事物一样是表达式。

当一个异常被捕获处理了,finally 块将被调用;它不是表达式的一部分。

Finagle 介绍

Finagle-Friendly REPL

我们将要讨论的不是标准 Scala 的代码。如果你喜欢使用 REPL 学习,你可能想知道如何获得一个加入 Finagle 及其依赖的 Scala REPL。

你可以在这里获取 Finagle 源代码。

如果你在 finagle 目录下有 Finagle 的源代码,你可以通过下面的命令得到一个控制台

$ cd finagle$ ./sbt "project finagle-http" console ...build output...scala>

Futures

Finagle 使用 com.twitter.util.Future [1]编码延迟操作。Future 是尚未生成的值的一个句柄。Finagle 使用 Future 作为其异步 API 的返回值。同步 API 会在返回前等待结果;但是异步 API 则不会等待。例如,个对互联网上一些服务的HTTP请求可能半秒都不会返回。你不希望你的程序阻塞等待半秒。“慢”的 API 可以立即返回一个 Future,然后在需要解析其值时“填充”。

val myFuture = MySlowService(request) // returns right away   ...do other things...val serviceResult = myFuture.get() // blocks until service "fills in" myFuture

在实践中,你不会发送一个请求,然后在几行代码后调用 myFuture.get。Future 提供了注册回调的方法,在值变得可用时会调用注册的回调函数。

如果你用过其他异步 API,当看到“回调”你也许会畏缩。你可能会联想到他们难以辨认的代码流,被调用的函数藏在离调用处远远的地方。但是,Future 可以利用 Scala 中“函数是一等公民”的特性编写出更可读的代码流。你可以在调用它的地方简单地定义一个处理函数。

例如,写代码调度请求,然后“处理”回应,你可以保持代码在一起:

val future = dispatch(req) // returns immediately, but future is "empty"future onSuccess { reply => // when the future gets "filled", use its value  println(reply)}

你可以在 REPL 中用体验一下 Future。虽然不是学习如何在实际代码中使用他们的好方法,但可以帮助理解 API。当你使用 REPL,Promise 是一个方便的类。它是 Future 抽象类的一个具体子类。你可以用它来创建一个还没有值的 Future。

scala> import com.twitter.util.{Future,Promise}import com.twitter.util.{Future, Promise}scala> val f6 = Future.value(6) // create already-resolved futuref6: com.twitter.util.Future[Int] = com.twitter.util.ConstFuture@c63a8afscala> f6.get()res0: Int = 6scala> val fex = Future.exception(new Exception) // create resolved sad futurefex: com.twitter.util.Future[Nothing] = com.twitter.util.ConstFuture@38ddab20scala> fex.get()java.lang.Exception  ... stack trace ...scala> val pr7 = new Promise[Int] // create unresolved futurepr7: com.twitter.util.Promise[Int] = Promise@1994943491(...)scala> pr7.get()  ...console hangs, waiting for future to resolve...Ctrl-CExecution interrupted by signal.scala> pr7.setValue(7)scala> pr7.get()res1: Int = 7scala>

在实际代码中使用 Future 时,你通常不会调用 get,而是使用回调函数。get 仅仅是方便在 REPL 修修补补。

顺序组合

Future 有类似集合 API 中的组合子(如 map, flatMap) 。回顾一下集合组合子,它让你可以表达如 “我有一个整数 List 和一个 square 函数:map 那个列表获得整数平方的列表”这样的操作。这种表达方式很灵巧;你可以把组合子函数和另一个函数放在一起有效地组成一个新函数。面向 Future 的组合子可以让你这样表达:“我有一个期望整数的 Future 和一个 square 函数:map 那个 Future 获得一个期望整数平方的 Future”。

如果你在定义一个异步 API,传入一个请求值,你的 API 应该返回一个包装在 Future 中的响应。因此,这些把输入和函数加入 Future 的组合子是相当有用的:它们帮助你根据其它异步 API 定义你自己的异步 API。

最重要的 Future 的组合子是 flatMap[2]

def Future[A].flatMap[B](f: A => Future[B]): Future[B]

flatMap 序列化两个 Future。即,它接受一个Future和一个异步函数,并返回另一个 Future。方法签名中是这样写的:给定一个 Future 成功的值,函数f提供下一个 Future。如果/当输入的 Future 成功完成,flatMap 自动调用f。只有当这两个 Future 都已完成,此操作所代表的 Future才算完成。如果任何一个 Future 失败,则操作确定的 Future 也将失败。这种隐交织的错误让我们只需要在必要时来处理错误,所以语法意义很大。flatMap 是这些语义组合子的标准名称。

如果你有一个 Future 并且想在异步 API 使用其值,使用 flatMap。例如,假设你有一个 Future[User],需要一个 Future[Boolean]表示用户是否已被禁止。有一个 isBanned 的异步 API 来判断一个用户是否已被禁止。此时可以使用 flatMap :

scala> import com.twitter.util.{Future,Promise}import com.twitter.util.{Future, Promise}scala> class User(n: String) { val name = n }defined class Userscala> def isBanned(u: User) = { Future.value(false) }isBanned: (u: User)com.twitter.util.Future[Boolean]scala> val pru = new Promise[User]pru: com.twitter.util.Promise[User] = Promise@897588993(...)scala> val futBan = pru flatMap isBanned // apply isBanned to futurefutBan: com.twitter.util.Future[Boolean] = Promise@1733189548(...)scala> futBan.get()  ...REPL hangs, futBan not resolved yet...Ctrl-CExecution interrupted by signal.scala> pru.setValue(new User("prudence"))scala> futBan.get()res45: Boolean = falsescala>

同样,如果要在 Future 中应用一个同步函数,可以使用 map。例如,假设你有一个 Future[RawCredentials] 需要一个 Future[Credentials]。你有一个的同步的 normalize 函数将 RawCredentials 转换成 Credentials。可以使用 map:

scala> class RawCredentials(u: String, pw: String) {     |   val username = u     |   val password = pw     | }defined class RawCredentialsscala> class Credentials(u: String, pw: String) {     |   val username = u     |   val password = pw     | }defined class Credentialsscala> def normalize(raw: RawCredentials) = {     |   new Credentials(raw.username.toLowerCase(), raw.password)     | }normalize: (raw: RawCredentials)Credentialsscala> val praw = new Promise[RawCredentials]praw: com.twitter.util.Promise[RawCredentials] = Promise@1341283926(...)scala> val fcred = praw map normalize // apply normalize to futurefcred: com.twitter.util.Future[Credentials] = Promise@1309582018(...)scala> fcred.get()   ...REPL hangs, fcred doesn't have a value yet...Ctrl-CExecution interrupted by signal.scala> praw.setValue(new RawCredentials("Florence", "nightingale"))scala> fcred.get().usernameres48: String = florencescala>

Scala 有快捷语法来调用 flatMap:for 表达式。假设你想通过异步 API 验证登录请求,然后通过另一个异步 API 检查用户是否被禁止。在 for 表达式的帮助下,我们可以这样写:

scala> def authenticate(req: LoginRequest) = {     |   // TODO: we should check the password     |   Future.value(new User(req.username))     | }authenticate: (req: LoginRequest)com.twitter.util.Future[User]scala> val f = for {     |  u <- authenticate(request)     |  b <- isBanned(u)     | } yield (u, b)f: com.twitter.util.Future[(User, Boolean)] = Promise@35785606(...)scala>

它产生一个 f: Future[(User, Boolean)],包含用户对象和一个表示该用户是否已被禁止的布尔值。注意这里是怎样实现顺序组合的:isBanned 使用了 authenticate 的输出作为其输入。

并发组合

你可能想一次获取来自多个服务的数据。例如,如果你正在编写一个 Web 服务来显示内容和广告,它可能会从两个服务中分别获取内容和广告。但是,你怎么告诉代码来等待两份答复呢?如果必须自己实现可能会非常棘手,幸运的是你可以使用并发组合子。

Future 提供了一些并发组合子。一般来说,他们都是将 Future 的一个序列转换成包含一个序列的 Future,只是方式略微不同。这是很好的,因为它(本质上)可以让你把几个 Future 封装成一个单一的 Future。

object Future {  …  def collect[A](fs: Seq[Future[A]]): Future[Seq[A]]  def join(fs: Seq[Future[_]]): Future[Unit]  def select(fs: Seq[Future[A]]) : Future[(Try[A], Seq[Future[A]])]}

collect 参数是具有相同类型 Future 的一个集合,返回一个 Future,其类型是包含那个类型值的一个序列。当所有的 Future 都成功完成或者当中任何一个失败,都会使这个 Future 完成。返回序列的顺序和传入序列的顺序相对应。

scala> val f2 = Future.value(2)f2: com.twitter.util.Future[Int] = com.twitter.util.ConstFuture@13ecdec0scala> val f3 = Future.value(3)f3: com.twitter.util.Future[Int] = com.twitter.util.ConstFuture@263bb672scala> val f23 = Future.collect(Seq(f2, f3))f23: com.twitter.util.Future[Seq[Int]] = Promise@635209178(...)scala> val f5 = f23 map (_.sum)f5: com.twitter.util.Future[Int] = Promise@1954478838(...)scala> f5.get()res9: Int = 5

join 参数是混合类型的 Future 序列,返回一个 Future[Unit],当所有的相关 Future 完成时(无论他们是否失败)该 Future 完成。其作用是标识一组异构操作完成。对那个内容和广告的例子来说,这可能是一个很好的解决方案。

scala> val ready = Future.join(Seq(f2, f3))ready: com.twitter.util.Future[Unit] = Promise@699347471(...)scala> ready.get() // doesn't ret value, but I know my futures are donescala>

当传入的 Future 序列的第一个 Future 完成的时候,select 会返回一个 Future。它会将那个完成的 Future 和其它未完成的 Future 一起放在 Seq 中返回。 (它不会做任何事情来取消剩余的 Future。你可以等待更多的回应,或者忽略他们)

scala> val pr7 = new Promise[Int] // unresolved futurepr7: com.twitter.util.Promise[Int] = Promise@1608532943(...)scala> val sel = Future.select(Seq(f2, pr7)) // select from 2 futs, one resolvedsel: com.twitter.util.Future[...] = Promise@1003382737(...)scala> val(complete, stragglers) = sel.get()complete: com.twitter.util.Try[Int] = Return(2)stragglers: Seq[...] = List(...)scala> complete.get()res110: Int = 2scala> stragglers(0).get() // our list of not-yet-finished futures has one item  ...get() hangs the REPL because this straggling future is not finished...Ctrl-CExecution interrupted by signal.scala> pr7.setValue(7)scala> stragglers(0).get()res113: Int = 7scala>

组合例子:缓存速率限制

这些组合子表达了典型的网络服务操作。这段假设的代码在对速率进行限制(为了保持本地速率限制缓存)的同时,将用户的请求调度到后台服务:

// Find out if user is rate-limited. This can be slow; we have to ask// the remote server that keeps track of who is rate-limited.def isRateLimited(u: User): Future[Boolean] = {  ...}// Notice how you can swap this implementation out now with something that might// implement a different, more restrictive policy.// Check the cache to find out if user is rate-limited. This cache// implementation is just a Map, and can return a value right way. But we// return a Future anyhow in case we need to use a slower implementation later.def isLimitedByCache(u: User): Future[Boolean] =  Future.value(limitCache(u))// Update the cachedef setIsLimitedInCache(user: User, v: Boolean) { limitCache(user) = v }// Get a timeline of tweets... unless the user is rate-limited (then throw// an exception instead)def getTimeline(cred: Credentials): Future[Timeline] =  isLimitedByCache(cred.user) flatMap {    case true => Future.exception(new Exception("rate limited"))    case false =>      // First we get auth'd user then we get timeline.      // Sequential composition of asynchronous APIs: use flatMap      val timeline = auth(cred) flatMap(getTimeline)      val limited = isRateLimited(cred.user) onSuccess(                                       setIsLimitedInCache(cred.user, _))      // 'join' concurrently combines differently-typed futures      // 'flatMap' sequentially combines, specifies what to do next      timeline join limited flatMap {        case (_, true) => Future.exception(new Exception("rate limited"))        case (timeline, _) => Future.value(timeline)      }  }}

这个例子结合了顺序和并发组合。请注意,除了给转化速率限制回应一个异常以外,没有明确的错误处理。如果任何 Future 在这里失败,它会自动传播到返回的 Future 中。

组合例子:网络爬虫

你已经看到了怎样使用 Future 组合子的例子,不过也许意犹未尽。假设你有一个简单的互联网模型。该互联网中只有 HTML 网页和图片,其中页面可以链接到图像和其他网页。你可以获取一个页面或图像,但 API 是异步的。这个假设的 API 成这些“可获取”的数据为资源:

import com.twitter.util.{Try,Future,Promise}// a fetchable thingtrait Resource {  def imageLinks(): Seq[String]  def links(): Seq[String]}// HTML pages can link to Imgs and to other HTML pages.class HTMLPage(val i: Seq[String], val l: Seq[String]) extends Resource {  def imageLinks() = i  def links = l}// IMGs don't actually link to anything elseclass Img() extends Resource {  def imageLinks() = Seq()  def links() = Seq()}// profile.html links to gallery.html and has an image link to portrait.jpgval profile = new HTMLPage(Seq("portrait.jpg"), Seq("gallery.html"))val portrait = new Img// gallery.html links to profile.html and two imagesval gallery = new HTMLPage(Seq("kitten.jpg", "puppy.jpg"), Seq("profile.html"))val kitten = new Imgval puppy = new Imgval internet = Map(  "profile.html" -> profile,  "gallery.html" -> gallery,  "portrait.jpg" -> portrait,  "kitten.jpg" -> kitten,  "puppy.jpg" -> puppy)// fetch(url) attempts to fetch a resource from our fake internet.// Its returned Future might contain a Resource or an exceptiondef fetch(url: String) = { new Promise(Try(internet(url))) }

顺序组合

假设给定一个页面 URL,而你希望获取该页面的第一个图。也许你正在做一个网站,在上面用户可以发布有趣的网页链接。为了帮助其他用户决定某个链接是否值得追踪,你打算显示那个链接中第一张图像的缩略图。

即使你不知道组合子,你仍然可以写一个缩略图获取函数:

def getThumbnail(url: String): Future[Resource]={  val returnVal = new Promise[Resource]  fetch(url) onSuccess { page => // callback for successful page fetch    fetch(page.imageLinks()(0)) onSuccess { p => // callback for successful img fetch      returnVal.setValue(p)    } onFailure { exc => // callback for failed img fetch      returnVal.setException(exc)    }  } onFailure { exc => // callback for failed page fetch    returnVal.setException(exc)  }  returnVal}

这个版本的函数能工作。它的大部分内容用来解析 Future,然后把他们的内容传给另一个 Future。

我们希望得到一个页面,然后从该页面获得一个图像。如果你想获得 A,然后再获得 B 的,这通常意味着顺序组合。由于 B 是异步的,所以需要使用 flatMap:

def getThumbnail(url: String): Future[Resource] =  fetch(url) flatMap { page => fetch(page.imageLinks()(0)) }

通过并发组合

抓取页面的第一个图片是好的,但也许我们应该获取所有图片,并让用户自己进行选择。我们可以使用for循环一个个地抓取,但这需要很长时间;所以我们想并行获取它们。如果你想的事情“并行”发生,这通常意味着并发组合。所以我们使用 Future.collect 的提取所有的图像:

def getThumbnails(url:String): Future[Seq[Resource]] =  fetch(url) flatMap { page =>    Future.collect(      page.imageLinks map { u => fetch(u) }    )  }

如果这对你有意义,那太好了。你可能会看不懂这行代码 page.imageLinks map { u => fetch(u) }:它使用 map 和 map 后的函数返回一个 Future。当接下来的事情是返回一个Future时,我们不是应该使用flatMap吗?但是请注意,在 map 前的不是一个 Future;它是一个集合。collection map function 返回一个集合;我们使用 Future.collect 收集 Future 的集合到一个 Future 中。

并发 + 递归

除了页面中的图片以外,我们可能会想获取它链接的其他页面。通过递归我们可以构建一个简单的网络爬虫。

// Returndef crawl(url: String): Future[Seq[Resource]] =  fetch(url) flatMap { page =>    Future.collect(      page.links map { u => crawl(u) }    ) map { pps => pps.flatten }}crawl("profile.html")   ...hangs REPL, infinite loop...Ctrl-CExecution interrupted by signal.scala>// She's gone rogue, captain! Have to take her out!// Calling Thread.stop on runaway Thread[Thread-93,5,main] with offending code:// scala> crawl("profile.html")

在实践中,这个网络爬虫不是很有用:首先我们没有告诉它何时停止爬行;其次即使资源刚刚被获取过,它仍然会不厌其烦地重新获取。

服务

一个 Finagle 服务用来处理 RPC,读取请求并给予回复的。服务是针对请求和回应的一个函数Req => Future[Rep]

abstract class Service[-Req, +Rep] extends (Req => Future[Rep])

在服务中,我们要同时定义客户端和服务器。

一个 Finagle 客户端“引入”一个网络服务。从概念上讲,Finagle 客户端由两部分组成

  • 一个使用服务的函数:分发一个 Req 并处理 Future[Rep]
  • 配置怎样分发这些请求;例如,作为 HTTP 请求发送到 api.twitter.com 的 80 端口

同样,Finagle 服务端“输出”网络服务。一个服务端由两个部分组成:

  • 一个实现服务的函数:传入一个 Req 并返回一个 Future[Rep]
  • 配置如何“监听”输入的 Reqs;例如,在 80 端口的 HTTP 请求。

这种设计分离了服务的“业务逻辑”和数据如何在网络中流动的配置。

我们也谈论 Finagle “过滤器”。过滤器在服务之间,修改流经它的数据。过滤器可以很好地和服务组合在一起。例如,如果你有一个速率限制过滤器和一个 tweet 服务,你可以把它们组合在一起形成有速率限制的 tweet 服务。

客户端

一个 Finagle 客户端“引入”一个网络服务。它有一些配置来设定如何在网络上发送数据。一个简单的 HTTP 客户端可能看起来像这样:

import org.jboss.netty.handler.codec.http.{DefaultHttpRequest, HttpRequest, HttpResponse, HttpVersion, HttpMethod}import com.twitter.finagle.Serviceimport com.twitter.finagle.builder.ClientBuilderimport com.twitter.finagle.http.Http// Don't worry, we discuss this magic "ClientBuilder" laterval client: Service[HttpRequest, HttpResponse] = ClientBuilder()  .codec(Http())  .hosts("twitter.com:80") // If >1 host, client does simple load-balancing  .hostConnectionLimit(1)  .build()val req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")val f = client(req) // Client, send the request// Handle the response:f onSuccess { res =>  println("got response", res)} onFailure { exc =>  println("failed :-(", exc)}

服务端

一个服务端按服务进行定义,并配置如何“监听”网络上的请求。一个简单的 HTTP 服务端可能看起来像这样:

import com.twitter.finagle.Serviceimport com.twitter.finagle.http.Httpimport com.twitter.util.Futureimport org.jboss.netty.handler.codec.http.{DefaultHttpResponse, HttpVersion, HttpResponseStatus, HttpRequest, HttpResponse}import java.net.{SocketAddress, InetSocketAddress}import com.twitter.finagle.builder.{Server, ServerBuilder}import com.twitter.finagle.builder.ServerBuilder// Define our service: OK response for root, 404 for other pathsval rootService = new Service[HttpRequest, HttpResponse] {  def apply(request: HttpRequest) = {    val r = request.getUri match {      case "/" => new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK)      case _ => new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND)    }    Future.value(r)  }}// Serve our service on a portval address: SocketAddress = new InetSocketAddress(10000)val server: Server = ServerBuilder()  .codec(Http())  .bindTo(address)  .name("HttpServer")  .build(rootService)

这个name是我们强加的,虽然没有在例子中使用它,但这个字段对分析和调试是很有用的。

过滤器

过滤器改造服务,它们可以提供通用的服务功能。例如你有几个服务需要支持速率限制,这时可以写一个限速过滤器并将其应用于所有的服务就解决问题了。过滤器也可以将服务分解成不同的阶段。

一个简单的代理可能看起来像这样:

class MyService(client: Service[..]) extends Service[HttpRequest, HttpResponse]{  def apply(request: HttpRequest) = {    client(rewriteReq(request)) map { res =>      rewriteRes(res)    }  }}

其中 rewriteReq 和 rewriteRes 可以提供协议翻译,例如。

abstract class Filter[-ReqIn, +RepOut, +ReqOut, -RepIn]  extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut])

通过图示可以更清晰地看出其类型:

    ((ReqIn, Service[ReqOut, RepIn])         => Future[RepOut])          (*   Service   *)[ReqIn -> (ReqOut -> RepIn) -> RepOut]

下面的例子展示了怎样通过过滤器来提供服务超时机制。

class TimeoutFilter[Req, Rep](  timeout: Duration,  exception: RequestTimeoutException,  timer: Timer)  extends Filter[Req, Rep, Req, Rep]{  def this(timeout: Duration, timer: Timer) =    this(timeout, new IndividualRequestTimeoutException(timeout), timer)  def apply(request: Req, service: Service[Req, Rep]): Future[Rep] = {    val res = service(request)    res.within(timer, timeout) rescue {      case _: java.util.concurrent.TimeoutException =>        res.cancel()        Trace.record(TimeoutFilter.TimeoutAnnotation)        Future.exception(exception)    }  }}

这个例子展示了怎样(通过认证服务)提供身份验证来将 Service[AuthHttpReq, HttpRep] 转换为 Service[HttpReq, HttpRep]

class RequireAuthentication(authService: AuthService)  extends Filter[HttpReq, HttpRep, AuthHttpReq, HttpRep] {  def apply(    req: HttpReq,    service: Service[AuthHttpReq, HttpRep]  ) = {    authService.auth(req) flatMap {      case AuthResult(AuthResultCode.OK, Some(passport), _) =>        service(AuthHttpReq(req, passport))      case ar: AuthResult =>        Future.exception(          new RequestUnauthenticated(ar.resultCode))    }  }}

这样使用过滤器是有好处的。它可以帮助你将“身份验证逻辑”固定在一个地方。拥有一个独立的类型执行请求授权,会使追查程序安全问题变得更容易。

过滤器可以使用 andThen 组合在一起。传入一个 Service 参数给 andThen 将创建一个(添加了过滤功能)的Service(类型用来做说明)。

val authFilter: Filter[HttpReq, HttpRep, AuthHttpReq, HttpRep]val timeoutfilter[Req, Rep]: Filter[Req, Rep, Req, Rep]val serviceRequiringAuth: Service[AuthHttpReq, HttpRep]val authenticateAndTimedOut: Filter[HttpReq, HttpRep, AuthHttpReq, HttpRep] =  authFilter andThen timeoutFilterval authenticatedTimedOutService: Service[HttpReq, HttpRep] =  authenticateAndTimedOut andThen serviceRequiringAuth

生成器(Builder)

生成器把所有组件组合在一起。一个 ClientBuilder 对给定的一组参数生成一个 Service,而一个 ServerBuilder 获取一个 Service 的实例,并调度传入请求给它。为了确定 Service 的类型,我们必须提供一个编解码器(Codec)。编解码器提供底层协议的实现(如 HTTP,thrift,memcached)。这两个 Builder 都有很多参数,其中一些是必填的。

下面是一个调用 ClientBuilder 的例子(类型用来做说明)

val client: Service[HttpRequest, HttpResponse] = ClientBuilder()  .codec(Http)  .hosts("host1.twitter.com:10000,host2.twitter.com:10001,host3.twitter.com:10003")  .hostConnectionLimit(1)  .tcpConnectTimeout(1.second)  .retries(2)  .reportTo(new OstrichStatsReceiver)  .build()

这将构建一个客户端在三个主机上进行负载平衡,最多在每台主机建立一个连接,并在两次失败尝试后放弃。统计数据会报给 ostrich 。以下生成器选项是必须的(而且它们也被静态强制填写了):hosts 或 cluster, codec 和 hostConnectionLimit。

同样的,你也可以使用一个 ServerBuilder 来创建“监听”传入请求的服务:

val service = new MyService(...) // construct instance of your Finagle servicevar filter = new MyFilter(...) // and maybe some filtersvar filteredServce = filter andThen serviceval  server = ServerBuilder()  .bindTo(new InetSocketAddress(port))  .codec(ThriftServerFramedCodec())  .name("my filtered service")//  .hostConnectionMaxLifeTime(5.minutes)//  .readTimeout(2.minutes)  .build(filteredService)

通过这些参数会生成一个Thrift服务器监听端口 port,并将请求分发给 service。如果我们去掉 hostConnectionMaxLifeTime 的注释,每个连接将被允许留存长达 5 分钟。如果我们去掉 readTimeout 的注释,那么我们就需要在 2 分钟之内发送请求。ServerBuilder 必选项有:name, bindTo 和 codec。

不要阻塞(除非你用正确的方式)

Finagle 自动操纵线程来保证服务顺利运行。但是,如果你的服务阻塞了,它会阻塞所有 Finagle 线程。

如果你的代码调用了一个阻塞操作(apply 或 get),使用 Future 池来包装阻塞代码。阻塞操作将运行在自己的线程池中,返回一个 Future 来完成(或失败)这个操作,并可以和其它 Future 组合。如果你的代码中使用 Future 的顺序组合,不用担心它会“阻塞”组合中的 Future。

[1]小心,还有其它“Future”类。不要将 com.twitter.util.Future 和scala.actor.Future 或 java.util.concurrent.Future 混淆起来!

[2] 如果你学习类型系统和/或分类理论,你会高兴地发现 flatMap 相当于一元绑定。

集合

基本数据结构

Scala 提供了一些不错的集合。

参考 Effective Scala 对怎样使用集合的观点

列表 List

scala> val numbers = List(1, 2, 3, 4)numbers: List[Int] = List(1, 2, 3, 4)

集 Set

集没有重复

scala> Set(1, 1, 2)res0: scala.collection.immutable.Set[Int] = Set(1, 2)

元组 Tuple

元组是在不使用类的前提下,将元素组合起来形成简单的逻辑集合。

scala> val hostPort = ("localhost", 80)hostPort: (String, Int) = (localhost, 80)

与样本类不同,元组不能通过名称获取字段,而是使用位置下标来读取对象;而且这个下标基于 1,而不是基于 0。

scala> hostPort._1res0: String = localhostscala> hostPort._2res1: Int = 80

元组可以很好得与模式匹配相结合。

hostPort match {  case ("localhost", port) => ...  case (host, port) => ...}

在创建两个元素的元组时,可以使用特殊语法:->

scala> 1 -> 2res0: (Int, Int) = (1,2)

参考 Effective Scala 对 [解构绑定](http://twitter.github.com/effectivescala/#Functional programming-Destructuring bindings) (“拆解”一个元组)的观点。

映射 Map

它可以持有基本数据类型。

Map(1 -> 2)Map("foo" -> "bar")

这看起来像是特殊的语法,不过不要忘了上文讨论的->可以用来创建二元组。

Map()方法也使用了从第一节课学到的变参列表:Map(1 -> "one", 2 -> "two")将变为 Map((1, "one"), (2, "two")),其中第一个参数是映射的键,第二个参数是映射的值。

映射的值可以是映射甚或是函数。

Map(1 -> Map("foo" -> "bar"))Map("timesTwo" -> { timesTwo(_) })

选项 Option

Option 是一个表示有可能包含值的容器。

Option基本的接口是这样的:

trait Option[T] {  def isDefined: Boolean  def get: T  def getOrElse(t: T): T}

Option 本身是泛型的,并且有两个子类: Some[T] 或 None

我们看一个使用 Option 的例子:

Map.get 使用 Option 作为其返回值,表示这个方法也许不会返回你请求的值。

scala> val numbers = Map("one" -> 1, "two" -> 2)numbers: scala.collection.immutable.Map[java.lang.String,Int] = Map(one -> 1, two -> 2)scala> numbers.get("two")res0: Option[Int] = Some(2)scala> numbers.get("three")res1: Option[Int] = None

现在我们的数据似乎陷在 Option 中了,我们怎样获取这个数据呢?

直觉上想到的可能是在 isDefined 方法上使用条件判断来处理。

// We want to multiply the number by two, otherwise return 0.val result = if (res1.isDefined) {  res1.get * 2} else {  0}

我们建议使用 getOrElse 或模式匹配处理这个结果。

getOrElse 让你轻松地定义一个默认值。

val result = res1.getOrElse(0) * 2

模式匹配能自然地配合 Option 使用。

val result = res1 match {  case Some(n) => n * 2  case None => 0}

参考 Effective Scala 对使用 [Options](http://twitter.github.com/effectivescala/#Functional programming-Options) 的意见。

函数组合子

List(1, 2, 3) map squared 对列表中的每一个元素都应用了squared 平方函数,并返回一个新的列表 List(1, 4, 9)。我们称这个操作 map 组合子。 (如果想要更好的定义,你可能会喜欢 Stackoverflow 上对组合子的说明。)他们常被用在标准的数据结构上。

map

map 对列表中的每个元素应用一个函数,返回应用后的元素所组成的列表。

scala> numbers.map((i: Int) => i * 2)res0: List[Int] = List(2, 4, 6, 8)

或传入一个部分应用函数

scala> def timesTwo(i: Int): Int = i * 2timesTwo: (i: Int)Intscala> numbers.map(timesTwo _)res0: List[Int] = List(2, 4, 6, 8)

foreach

foreach 很像 map,但没有返回值。foreach 仅用于有副作用[side-effects]的函数。

scala> numbers.foreach((i: Int) => i * 2)

什么也没有返回。

你可以尝试存储返回值,但它会是 Unit 类型(即void)

scala> val doubled = numbers.foreach((i: Int) => i * 2)doubled: Unit = ()

filter

filter 移除任何对传入函数计算结果为 false 的元素。返回一个布尔值的函数通常被称为谓词函数[或判定函数]

scala> numbers.filter((i: Int) => i % 2 == 0)res0: List[Int] = List(2, 4)scala> def isEven(i: Int): Boolean = i % 2 == 0isEven: (i: Int)Booleanscala> numbers.filter(isEven _)res2: List[Int] = List(2, 4)

zip

zip 将两个列表的内容聚合到一个对偶列表中。

scala> List(1, 2, 3).zip(List("a", "b", "c"))res0: List[(Int, String)] = List((1,a), (2,b), (3,c))

partition

partition 将使用给定的谓词函数分割列表。

scala> val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)scala> numbers.partition(_ % 2 == 0)res0: (List[Int], List[Int]) = (List(2, 4, 6, 8, 10),List(1, 3, 5, 7, 9))

find

find 返回集合中第一个匹配谓词函数的元素。

scala> numbers.find((i: Int) => i > 5)res0: Option[Int] = Some(6)

drop & dropWhile

drop 将删除前 i 个元素

scala> numbers.drop(5)res0: List[Int] = List(6, 7, 8, 9, 10)

dropWhile 将删除元素直到找到第一个匹配谓词函数的元素。例如,如果我们在 numbers 列表上使用 dropWhile 奇数的函数, 1 将被丢弃(但 3 不会被丢弃,因为他被 2 “保护”了)。

scala> numbers.dropWhile(_ % 2 != 0)res0: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)

foldLeft

scala> numbers.foldLeft(0)((m: Int, n: Int) => m + n)res0: Int = 55

0 为初始值(记住 numbers 是 List[Int] 类型),m 作为一个累加器。

直接观察运行过程:

scala> numbers.foldLeft(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }m: 0 n: 1m: 1 n: 2m: 3 n: 3m: 6 n: 4m: 10 n: 5m: 15 n: 6m: 21 n: 7m: 28 n: 8m: 36 n: 9m: 45 n: 10res0: Int = 55

foldRight

和 foldLeft 一样,只是运行过程相反。

scala> numbers.foldRight(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }m: 10 n: 0m: 9 n: 10m: 8 n: 19m: 7 n: 27m: 6 n: 34m: 5 n: 40m: 4 n: 45m: 3 n: 49m: 2 n: 52m: 1 n: 54res0: Int = 55

flatten

flatten 将嵌套结构扁平化为一个层次的集合。

scala> List(List(1, 2), List(3, 4)).flattenres0: List[Int] = List(1, 2, 3, 4)

flatMap

flatMap 是一种常用的组合子,结合映射 [mapping] 和扁平化 [flattening]。flatMap 需要一个处理嵌套列表的函数,然后将结果串连起来。

scala> val nestedNumbers = List(List(1, 2), List(3, 4))nestedNumbers: List[List[Int]] = List(List(1, 2), List(3, 4))scala> nestedNumbers.flatMap(x => x.map(_ * 2))res0: List[Int] = List(2, 4, 6, 8)

可以把它看做是“先映射后扁平化”的快捷操作:

scala> nestedNumbers.map((x: List[Int]) => x.map(_ * 2)).flattenres1: List[Int] = List(2, 4, 6, 8)

这个例子先调用 map,然后可以马上调用 flatten,这就是“组合子”的特征,也是这些函数的本质。

参考 Effective Scala 对 [flatMap](http://twitter.github.com/effectivescala/#Functional programming-flatMap) 的意见。

扩展函数组合子

现在我们已经学过集合上的一些函数。

我们将尝试写自己的函数组合子。

有趣的是,上面所展示的每一个函数组合子都可以用 fold 方法实现。让我们看一些例子。

def ourMap(numbers: List[Int], fn: Int => Int): List[Int] = {  numbers.foldRight(List[Int]()) { (x: Int, xs: List[Int]) =>    fn(x) :: xs  }}scala> ourMap(numbers, timesTwo(_))res0: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

为什么是List[Int]()?Scala没有聪明到理解你的目的是将结果积聚在一个空的 Int 类型的列表中。

Map?

所有展示的函数组合子都可以在 Map 上使用。Map 可以被看作是一个二元组的列表,所以你写的函数要处理一个键和值的二元组。

scala> val extensions = Map("steve" -> 100, "bob" -> 101, "joe" -> 201)extensions: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101), (joe,201))

现在筛选出电话分机号码低于 200 的条目。

scala> extensions.filter((namePhone: (String, Int)) => namePhone._2 < 200)res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101))

因为参数是元组,所以你必须使用位置获取器来读取它们的键和值。

幸运的是,我们其实可以使用模式匹配更优雅地提取键和值。

scala> extensions.filter({case (name, extension) => extension < 200})res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101))

Searchbird

我们要使用 Scala 和先前介绍的 Finagle 框架构建一个简单的分布式搜索引擎。

设计目标:大图景

从广义上讲,我们的设计目标包括 抽象 (abstraction:在不知道其内部的所有细节的前提下,利用该系统功能的能力)、 模块化 (modularity:把系统分解为小而简单的片段,从而更容易被理解和/或被更换的能力)和 扩展性 (scalability:用简单直接的方法给系统扩容的能力)。

我们要描述的系统有三个部分: (1) 客户端 发出请求,(2) 服务端 接收请求并应答,和(3) 传送 机制来这些通信包装起来。通常情况下,客户端和服务器位于不同的机器上,通过网络上的一个特定的端口进行通信,但在这个例子中,它们将运行在同一台机器上(而且仍然使用端口进行通信) 。在我们的例子中,客户端和服务器将用 Scala 编写,传送协议将使用 Thrift 处理。本教程的主要目的是展示一个简单的具有良好可扩展性的服务器和客户端。

探索默认的引导程序项目

首先,使用 scala-bootstrapper 创建一个骨架项目( “ Searchbird ” )。这将创建一个简单的基于 Finagle 和 key-value 内存存储的 Scala 服务。我们将扩展这个工程以支持搜索值,并进而支持多进程多个内存存储的搜索。

$ mkdir searchbird ; cd searchbird$ scala-bootstrapper searchbirdwriting build.sbtwriting config/development.scalawriting config/production.scalawriting config/staging.scalawriting config/test.scalawriting consolewriting Gemfilewriting project/plugins.sbtwriting README.mdwriting sbtwriting src/main/scala/com/twitter/searchbird/SearchbirdConsoleClient.scalawriting src/main/scala/com/twitter/searchbird/SearchbirdServiceImpl.scalawriting src/main/scala/com/twitter/searchbird/config/SearchbirdServiceConfig.scalawriting src/main/scala/com/twitter/searchbird/Main.scalawriting src/main/thrift/searchbird.thriftwriting src/scripts/searchbird.shwriting src/scripts/config.shwriting src/scripts/devel.shwriting src/scripts/server.shwriting src/scripts/service.shwriting src/test/scala/com/twitter/searchbird/AbstractSpec.scalawriting src/test/scala/com/twitter/searchbird/SearchbirdServiceSpec.scalawriting TUTORIAL.md

首先,来看下 scala-bootstrapper 为我们创建的默认项目。这是一个模板。虽然最终将替换它的大部分内容,不过作为支架它还是很方便的。它定义了一个简单(但完整)的 key-value 存储,并包含了配置、thrift 接口、统计输出和日志记录。

在我们看代码之前,先运行一个客户端和服务器,看看它是如何工作的。这里是我们构建的:

这里是我们的服务输出的接口。由于 Searchbird 服务是一个 Thrift 服务(和我们大部分服务一样),因而其外部接口使用 Thrift IDL(“接口描述语言”)定义。

src/main/thrift/searchbird.thrift

service SearchbirdService {  string get(1: string key) throws(1: SearchbirdException ex)  void put(1: string key, 2: string value)}

这是非常直观的:我们的服务 SearchbirdService 输出两个 RPC 方法 get 和 put 。他们组成了一个到 key-value 存储的简单接口。

现在,让我们运行默认的服务,启动客户端连接到这个服务,并通过这个接口来探索他们。打开两个窗口,一个用于服务器,一个用于客户端。

在第一个窗口中,用交互模式启动 SBT(在命令行中运行 ./sbt [1]),然后构建和运行项目内 SBT。这会运行 Main.scala 定义的 主 进程。

$ ./sbt...> compile> run -f config/development.scala...[info] Running com.twitter.searchbird.Main -f config/development.scala

配置文件 (development.scala) 实例化一个新的服务,并监听 9999 端口。客户端可以连接到 9999 端口使用此服务。

现在,我们将使用 控制台 shell脚本初始化和运行一个客户端实例,即 SearchbirdConsoleClient 实例 (SearchbirdConsoleClient.scala) 。在另一个窗口中运行此脚本:

$ ./console 127.0.0.1 9999[info] Running com.twitter.searchbird.SearchbirdConsoleClient 127.0.0.1 9999'client' is bound to your thrift client.finagle-client> 

客户端对象 client 现在连接到本地计算机上的 9999 端口,并可以跟服务交互了。接下来我们发送一些请求:

scala> client.put("marius", "Marius Eriksen")res0: ...scala> client.put("stevej", "Steve Jenson")res1: ...scala> client.get("marius")res2: com.twitter.util.Future[String] = ...scala> client.get("marius").get()res3: String = Marius Eriksen

(第二个 get() 调用解析 client.get() 返回的 Future 类型值,阻塞直到该值准备好。)

该服务器还输出运行统计(配置文件中指定这些信息在 9900 端口)。这不仅方便对各个服务器进行检查,也利于聚集全局的服务统计(以机器可读的 JSON 接口)。打开第三个窗口来查看这些统计:

$ curl localhost:9900/stats.txtcounters:  Searchbird/connects: 1  Searchbird/received_bytes: 264  Searchbird/requests: 3  Searchbird/sent_bytes: 128  Searchbird/success: 3  jvm_gc_ConcurrentMarkSweep_cycles: 1  jvm_gc_ConcurrentMarkSweep_msec: 15  jvm_gc_ParNew_cycles: 24  jvm_gc_ParNew_msec: 191  jvm_gc_cycles: 25  jvm_gc_msec: 206gauges:  Searchbird/connections: 1  Searchbird/pending: 0  jvm_fd_count: 135  jvm_fd_limit: 10240  jvm_heap_committed: 85000192  jvm_heap_max: 530186240  jvm_heap_used: 54778640  jvm_nonheap_committed: 89657344  jvm_nonheap_max: 136314880  jvm_nonheap_used: 66238144  jvm_num_cpus: 4  jvm_post_gc_CMS_Old_Gen_used: 36490088  jvm_post_gc_CMS_Perm_Gen_used: 54718880  jvm_post_gc_Par_Eden_Space_used: 0  jvm_post_gc_Par_Survivor_Space_used: 1315280  jvm_post_gc_used: 92524248  jvm_start_time: 1345072684280  jvm_thread_count: 16  jvm_thread_daemon_count: 7  jvm_thread_peak_count: 16  jvm_uptime: 1671792labels:metrics:  Searchbird/handletime_us: (average=9598, count=4, maximum=19138, minimum=637, p25=637, p50=4265, p75=14175, p90=19138, p95=19138, p99=19138, p999=19138, p9999=19138, sum=38393)  Searchbird/request_latency_ms: (average=4, count=3, maximum=9, minimum=0, p25=0, p50=5, p75=9, p90=9, p95=9, p99=9, p999=9, p9999=9, sum=14)

除了我们自己的服务统计信息以外,还有一些通用的 JVM 统计。

现在,让我们来看看配置、服务器和客户端的实现代码。

…/config/SearchbirdServiceConfig.scala

配置是一个 Scala 的特质,有一个方法 apply: RuntimeEnvironment => T 来创建一些 T 。在这个意义上,配置是“工厂” 。在运行时,配置文件(通过使用Scala编译器库)被取值为一个脚本,并产生一个配置对象。 RuntimeEnvironment 是一个提供各种运行参数(命令行标志, JVM 版本,编译时间戳等)查询的一个对象。

SearchbirdServiceConfig 类就是这样一个配置类。它使用其默认值一起指定配置参数。 (Finagle 支持一个通用的跟踪系统,我们在本教程将不会介绍: Zipkin 一个集合/聚合轨迹的 分布式跟踪系统。)

class SearchbirdServiceConfig extends ServerConfig[SearchbirdService.ThriftServer] {  var thriftPort: Int = 9999  var tracerFactory: Tracer.Factory = NullTracer.factory  def apply(runtime: RuntimeEnvironment) = new SearchbirdServiceImpl(this)}

在我们的例子中,我们要创建一个 SearchbirdService.ThriftServer。这是由 thrift 代码生成器生成的服务器类型[2]

…/Main.scala

在 SBT 控制台中键入“run”调用 main ,这将配置和初始化服务器。它读取配置(在 development.scala 中指定,并会作为参数传给“run”),创建 SearchbirdService.ThriftServer ,并启动它。 RuntimeEnvironment.loadRuntimeConfig 执行配置赋值,并把自身作为一个参数来调用 apply [3]

object Main {  private val log = Logger.get(getClass)  def main(args: Array[String]) {    val runtime = RuntimeEnvironment(this, args)    val server = runtime.loadRuntimeConfig[SearchbirdService.ThriftServer]    try {      log.info("Starting SearchbirdService")      server.start()    } catch {      case e: Exception =>        log.error(e, "Failed starting SearchbirdService, exiting")        ServiceTracker.shutdown()        System.exit(1)    }  }}

…/SearchbirdServiceImpl.scala

这是实质的服务:我们用自己的实现扩展 SearchbirdService.ThriftServer 。回忆一下 thrift 为我们生成的 SearchbirdService.ThriftServer 。它为每一个 thrift 方法生成一个 Scala 方法。到目前为止,在我们的例子中生成的接口是:

trait SearchbirdService {  def put(key: String, value: String): Future[Void]  def get(key: String): Future[String]}

返回值是 Future[Value] 而不是直接返回值,可以推迟它们的计算(finagle 的文档有 Future 更多的细节)。对本教程的目的来说,你唯一需要知道的有关 Future 的知识点是,可以通过 get() 获取其值。

scala-bootstrapper 默认实现的 key-value 存储很简单:它提供了一个通过 get 和 put 访问的 数据库 数据结构。

class SearchbirdServiceImpl(config: SearchbirdServiceConfig) extends SearchbirdService.ThriftServer {  val serverName = "Searchbird"  val thriftPort = config.thriftPort  override val tracerFactory = config.tracerFactory  val database = new mutable.HashMap[String, String]()  def get(key: String) = {    database.get(key) match {      case None =>        log.debug("get %s: miss", key)        Future.exception(SearchbirdException("No such key"))      case Some(value) =>        log.debug("get %s: hit", key)        Future(value)    }  }  def put(key: String, value: String) = {    log.debug("put %s", key)    database(key) = value    Future.Unit  }  def shutdown() = {    super.shutdown(0.seconds)  }}

其结果是构建在 Scala HashMap 上的一个简单 thrift 接口。

一个简单的搜索引擎

现在,我们将扩展现有的例子,来创建一个简单的搜索引擎。然后,我们将进一步扩展它成为由多个分片组成的 分布式 搜索引擎,使我们能够适应比单台机器内存更大的语料库。

为了简单起见,我们将最小化扩展目前的 thrift 服务,以支持搜索操作。使用模型是用 put 把文件加入搜索引擎,其中每个文件包含了一系列的记号(词),那么我们就可以输入一串记号,然后搜索会返回包含这个串中所有记号的所有文件。该体系结构是与前面的例子相同,但增加了一个新的 @search@ 调用。

要实现这样一个搜索引擎需要修改以下两个文件:

src/main/thrift/searchbird.thrift

service SearchbirdService {  string get(1: string key) throws(1: SearchbirdException ex)  void put(1: string key, 2: string value)  list<string> search(1: string query)}

我们增加了一个 search 方法来搜索当前哈希表,返回其值与查询匹配的键列表。实现也很简单直观:

…/SearchbirdServiceImpl.scala

大部分修改都在这个文件中。

现在的 数据库 HashMap 保存一个正向索引来持有到文档的键映射。我们重命名它为 forward 并增加一个 倒排(reverse) 索引(映射记号到所有包含该记号的文件)。所以在 SearchbirdServiceImpl.scala 中,更换 database 定义:

val forward = new mutable.HashMap[String, String]  with mutable.SynchronizedMap[String, String]val reverse = new mutable.HashMap[String, Set[String]]  with mutable.SynchronizedMap[String, Set[String]]

在 get 调用中,使用 forward 替换 数据库 即可,在其他方面 get 保持不变(仅执行正向查找)。不过 put 还需要改变:我们还需要为文件中的每个令牌填充反向索引,把文件的键附加到令牌关联的列表中。用下面的代码替换 put 调用。给定一个特定的搜索令牌,我们现在可以使用反向映射来查找文件。

def put(key: String, value: String) = {  log.debug("put %s", key)  forward(key) = value  // serialize updaters  synchronized {    value.split(" ").toSet foreach { token =>      val current = reverse.getOrElse(token, Set())      reverse(token) = current + key    }  }  Future.Unit}

需要注意的是(即使 HashMap 是线程安全的)同时只能有一个线程可以更新倒排索引,以确保对映射条目的 读-修改-写 是一个原子操作。 (这段代码过于保守;在进行 检索-修改-写 操作时,它锁定了整个映射,而不是锁定单个条目。)。另外还要注意使用 Set 作为数据结构;这可以确保即使一个文件中两次出现同样的符号,它也只会被 foreach 循环处理一次。

这个实现仍然有一个问题,作为留给读者的一个练习:当我们用一个新文档覆盖的一个键的时候,我们诶有删除任何倒排索引中引用的旧文件。

现在进入搜索引擎的核心:新的 search 方法。他应该解析查询,寻找匹配的文档,然后对这些列表做相交操作。这将产生包含所有查询中的标记的文件列表。在 Scala 中可以很直接地表达;添加这段代码到SearchbirdServiceImpl 类中:

def search(query: String) = Future.value {  val tokens = query.split(" ")  val hits = tokens map { token => reverse.getOrElse(token, Set()) }  val intersected = hits reduceLeftOption { _ & _ } getOrElse Set()  intersected.toList}

在这段短短的代码中有几件事情是值得关注的。在构建命中列表时,如果键( token )没有被发现, getOrElse 会返回其第二个参数(在这种情况下,一个空 Set )。我们使用 left-reduce 执行实际的相交操作。特别是当 reduceLeftOption 发现 hits 为空时将不会继续尝试执行 reduce 操作。这使我们能够提供一个默认值,而不是抛出一个异常。其实这相当于:

def search(query: String) = Future.value {  val tokens = query.split(" ")  val hits = tokens map { token => reverse.getOrElse(token, Set()) }  if (hits.isEmpty)    Nil  else    hits reduceLeft { _ & _ } toList}

使用哪种方式大多是个人喜好的问题,虽然函数式风格往往会避开带有合理默认值的条件语句。

现在,我们可以尝试在控制台中实验我们新的实现。重启服务器:

$ ./sbt...> compile> run -f config/development.scala...[info] Running com.twitter.searchbird.Main -f config/development.scala

然后再从 searchbird 目录,启动客户端:

$ ./console 127.0.0.1 9999...[info] Running com.twitter.searchbird.SearchbirdConsoleClient 127.0.0.1 9999'client' is bound to your thrift client.finagle-client> 

粘贴以下说明到控制台:

client.put("basics", " values functions classes methods inheritance try catch finally expression oriented")client.put("basics", " case classes objects packages apply update functions are objects (uniform access principle) pattern")client.put("collections", " lists maps functional combinators (map foreach filter zip")client.put("pattern", " more functions! partialfunctions more pattern")client.put("type", " basic types and type polymorphism type inference variance bounds")client.put("advanced", " advanced types view bounds higher kinded types recursive types structural")client.put("simple", " all about sbt the standard scala build")client.put("more", " tour of the scala collections")client.put("testing", " write tests with specs a bdd testing framework for")client.put("concurrency", " runnable callable threads futures twitter")client.put("java", " java interop using scala from")client.put("searchbird", " building a distributed search engine using")

现在,我们可以执行一些搜索,返回包含搜索词的文件的键。

> client.search("functions").get()res12: Seq[String] = ArrayBuffer(basics)> client.search("java").get()res13: Seq[String] = ArrayBuffer(java)> client.search("java scala").get()res14: Seq[String] = ArrayBuffer(java)> client.search("functional").get()res15: Seq[String] = ArrayBuffer(collections)> client.search("sbt").get()res16: Seq[String] = ArrayBuffer(simple)> client.search("types").get()res17: Seq[String] = ArrayBuffer(type, advanced)

回想一下,如果调用返回一个 Future ,我们必须使用一个阻塞的 get() 来获取其中包含的值。我们可以使用 Future.collect 命令来创建多个并发请求,并等待所有请求成功返回:

> import com.twitter.util.Future...> Future.collect(Seq(    client.search("types"),    client.search("sbt"),    client.search("functional")  )).get()res18: Seq[Seq[String]] = ArrayBuffer(ArrayBuffer(type, advanced), ArrayBuffer(simple), ArrayBuffer(collections))

分发我们的服务

单台机器上一个简单的内存搜索引擎将无法搜索超过内存大小的语料库。现在,我们要大胆改进,用一个简单的分片计划来构建分布式节点。下面是框图:

抽象

为了帮助我们的工作,我们会先介绍另一个抽象索引来解耦 SearchbirdService 对索引实现的依赖。这是一个直观的重构。我们首先添加一个索引文件到构建 (创建文件 searchbird/src/main/scala/com/twitter/searchbird/Index.scala ):

…/Index.scala

package com.twitter.searchbirdimport scala.collection.mutableimport com.twitter.util._import com.twitter.conversions.time._import com.twitter.logging.Loggerimport com.twitter.finagle.builder.ClientBuilderimport com.twitter.finagle.thrift.ThriftClientFramedCodectrait Index {  def get(key: String): Future[String]  def put(key: String, value: String): Future[Unit]  def search(key: String): Future[List[String]]}class ResidentIndex extends Index {  val log = Logger.get(getClass)  val forward = new mutable.HashMap[String, String]    with mutable.SynchronizedMap[String, String]  val reverse = new mutable.HashMap[String, Set[String]]    with mutable.SynchronizedMap[String, Set[String]]  def get(key: String) = {    forward.get(key) match {      case None =>        log.debug("get %s: miss", key)        Future.exception(SearchbirdException("No such key"))      case Some(value) =>        log.debug("get %s: hit", key)        Future(value)    }  }  def put(key: String, value: String) = {    log.debug("put %s", key)    forward(key) = value    // admit only one updater.    synchronized {      (Set() ++ value.split(" ")) foreach { token =>        val current = reverse.get(token) getOrElse Set()        reverse(token) = current + key      }    }    Future.Unit  }  def search(query: String) = Future.value {    val tokens = query.split(" ")    val hits = tokens map { token => reverse.getOrElse(token, Set()) }    val intersected = hits reduceLeftOption { _ & _ } getOrElse Set()    intersected.toList  }}

现在,我们把 thrift 服务转换成一个简单的调度机制:为每一个索引实例提供一个 thrift 接口。这是一个强大的抽象,因为它分离了索引实现和服务实现。服务不再知道索引的任何细节;索引可以是本地的或远程的,甚至可能是许多索引的组合,但服务并不关心,索引实现可能会更改但是不用修改服务。

将 SearchbirdServiceImpl 类定义更换为以下(简单得多)的代码(其中不再包含索引实现细节)。注意初始化服务器现在需要第二个参数 Index 。

…/SearchbirdServiceImpl.scala

class SearchbirdServiceImpl(config: SearchbirdServiceConfig, index: Index) extends SearchbirdService.ThriftServer {  val serverName = "Searchbird"  val thriftPort = config.thriftPort  def get(key: String) = index.get(key)  def put(key: String, value: String) =    index.put(key, value) flatMap { _ => Future.Unit }  def search(query: String) = index.search(query)  def shutdown() = {    super.shutdown(0.seconds)  }}

…/config/SearchbirdServiceConfig.scala

相应地更新 SearchbirdServiceConfig 的 apply 调用:

class SearchbirdServiceConfig extends ServerConfig[SearchbirdService.ThriftServer] {  var thriftPort: Int = 9999  var tracerFactory: Tracer.Factory = NullTracer.factory  def apply(runtime: RuntimeEnvironment) = new SearchbirdServiceImpl(this, new ResidentIndex)}

我们将建立一个简单的分布式系统,一个主节点组织查询其子节点。为了实现这一目标,我们将需要两个新的 Index 类型。一个代表远程索引,另一种是其他多个 Index 实例的组合索引。这样我们的服务就可以实例化多个远程索引的复合索引来构建分布式索引。请注意这两个 Index 类型具有相同的接口,所以服务器不需要知道它们所连接的索引是远程的还是复合的。

…/Index.scala

在 Index.scala 中定义了 CompositeIndex :

class CompositeIndex(indices: Seq[Index]) extends Index {  require(!indices.isEmpty)  def get(key: String) = {    val queries = indices.map { idx =>      idx.get(key) map { r => Some(r) } handle { case e => None }    }    Future.collect(queries) flatMap { results =>      results.find { _.isDefined } map { _.get } match {        case Some(v) => Future.value(v)        case None => Future.exception(SearchbirdException("No such key"))      }    }  }  def put(key: String, value: String) =    Future.exception(SearchbirdException("put() not supported by CompositeIndex"))  def search(query: String) = {    val queries = indices.map { _.search(query) rescue { case _=> Future.value(Nil) } }    Future.collect(queries) map { results => (Set() ++ results.flatten) toList }  }}

组合索引构建在一组相关 Index 实例的基础上。注意它并不关心这些实例实际上是如何实现的。这种组合类型在构建不同查询机制的时候具有极大的灵活性。我们没有定义拆分机制,所以复合索引不支持 put 操作。这些请求被直接交由子节点处理。 get 的实现是查询所有子节点,并提取第一个成功的结果。如果没有成功结果的话,则抛出一个异常。注意因为没有结果是通过抛出一个异常表示的,所以我们 处理Future ,是将任何异常转换成 None 。在实际系统中,我们很可能会为遗漏值填入适当的错误码,而不是使用异常。异常在构建原型时是方便和适宜的,但不能很好地组合。为了把真正的例外和遗漏值区分开,必须要检查异常本身。相反,把这种区别直接嵌入在返回值的类型中是更好的风格。

search 像以前一样工作。和提取第一个结果不同,我们把它们组合起来,通过使用 Set 确保其唯一性。

RemoteIndex 提供了到远程服务器的一个 Index 接口。

class RemoteIndex(hosts: String) extends Index {  val transport = ClientBuilder()    .name("remoteIndex")    .hosts(hosts)    .codec(ThriftClientFramedCodec())    .hostConnectionLimit(1)    .timeout(500.milliseconds)    .build()  val client = new SearchbirdService.FinagledClient(transport)  def get(key: String) = client.get(key)  def put(key: String, value: String) = client.put(key, value) map { _ => () }  def search(query: String) = client.search(query) map { _.toList }}

这样就使用一些合理的默认值,调用代理,稍微调整类型,就构造出一个 finagle thrift 客户端。

全部放在一起

现在我们拥有了需要的所有功能。我们需要调整配置,以便能够调用一个给定的节点,不管是主节点亦或是数据分片节点。为了做到这一点,我们将通过创建一个新的配置项来在系统中枚举分片。我们还需要添加 Index 参数到我们的 SearchbirdServiceImpl 实例。然后,我们将使用命令行参数(还记得 Config 是如何做到的吗)在这两种模式中启动服务器。

…/config/SearchbirdServiceConfig.scala

class SearchbirdServiceConfig extends ServerConfig[SearchbirdService.ThriftServer] {  var thriftPort: Int = 9999  var shards: Seq[String] = Seq()  def apply(runtime: RuntimeEnvironment) = {    val index = runtime.arguments.get("shard") match {      case Some(arg) =>        val which = arg.toInt        if (which >= shards.size || which < 0)          throw new Exception("invalid shard number %d".format(which))        // override with the shard port        val Array(_, port) = shards(which).split(":")        thriftPort = port.toInt        new ResidentIndex      case None =>        require(!shards.isEmpty)        val remotes = shards map { new RemoteIndex(_) }        new CompositeIndex(remotes)    }    new SearchbirdServiceImpl(this, index)  }}

现在,我们将调整配置:添加“分片”初始化到 SearchbirdServiceConfig 的初始化中(我们可以通过端口 9000 访问分片 0,9001 访问分片 1,依次类推)。

config/development.scala

new SearchbirdServiceConfig {  // Add your own config here  shards = Seq(    "localhost:9000",    "localhost:9001",    "localhost:9002"  )  ...

注释掉 admin.httpPort 的设置(我们不希望在同一台机器上运行多个服务,而不注释的话这些服务都会试图打开相同的端口):

  // admin.httpPort = 9900

现在,如果我们不带任何参数调用我们的服务器程序,它会启动一个主节点来和所有分片通信。如果我们指定一个分片参数,它会在指定端口启动一个分片服务器。

让我们试试吧!我们将启动 3 个服务:2 个分片和 1 个主节点。首先编译改动:

$ ./sbt> compile...> exit

然后启动三个服务:

$ ./sbt 'run -f config/development.scala -D shard=0'$ ./sbt 'run -f config/development.scala -D shard=1'$ ./sbt 'run -f config/development.scala'

您可以在 3 个不同的窗口中分别运行,或在同一窗口开始依次逐个运行,等待其启动后,只用 ctrl-z 悬挂这个命令,并使用 bg 将它放在后台执行。

然后,我们将通过控制台与它们进行互动。首先,让我们填充一些数据在两个分片节点。从 searchbird 目录运行:

$ ./console localhost 9000...> client.put("fromShardA", "a value from SHARD_A")> client.put("hello", "world")
$ ./console localhost 9001...> client.put("fromShardB", "a value from SHARD_B")> client.put("hello", "world again")

一旦完成就可以退出这些控制台会话。现在通过主节点查询我们的数据库(9999 端口):

$ ./console localhost 9999[info] Running com.twitter.searchbird.SearchbirdConsoleClient localhost 9999'client' is bound to your thrift client.finagle-client> client.get("hello").get()res0: String = worldfinagle-client> client.get("fromShardC").get()SearchbirdException(No such key)...finagle-client> client.get("fromShardA").get()res2: String = a value from SHARD_Afinagle-client> client.search("hello").get()res3: Seq[String] = ArrayBuffer()finagle-client> client.search("world").get()res4: Seq[String] = ArrayBuffer(hello)finagle-client> client.search("value").get()res5: Seq[String] = ArrayBuffer(fromShardA, fromShardB)

这个设计有多个数据抽象,允许更加模块化和可扩展的实现:

  • ResidentIndex 数据结构对网络、服务器或客户端一无所知。
  • CompositeIndex 对其索引构成的底层数据结构和组合方式一无所知;它只是简单地把请求分配给他们。
  • 服务器相同的 search 接口(特质)允许服务器查询其本地数据结构(ResidentIndex) ,或分发到其他服务器(CompositeIndex) 查询,而不需要知道这个区别,这是从调用隐藏的。
  • SearchbirdServiceImpl 和 Index 现在是相互独立的模块,这使服务实现变得简单,同时实现了服务和其数据结构之间的分离。
  • 这个设计灵活到允许一个或多个远程索引运行在本地机器或远程机器上。

这个实现的可能改进将包括:

  • 当前的实现将 put() 调用发送到所有节点。取而代之,我们可以使用一个哈希表,将 put()调用只发送到一个节点,而在所有节点之间分配存储。
    • 但是值得注意的是,在这个策略下我们失去了冗余。我们怎样在不需要完全复制的前提下保持一定的冗余度呢?
  • 当系统出错时我们没有做任何有趣的处理(例如我们没有处理任何异常)。

[1]本地 ./sbt 脚本只是保证该 SBT 版本和我们知道的所有库是一致的。

[2]target/gen-scala/com/twitter/searchbird/SearchbirdService.scala

[3] 更多信息见 Ostrich’s README

模式匹配与函数组合

函数组合

让我们创建两个函数:

scala> def f(s: String) = "f(" + s + ")"f: (String)java.lang.Stringscala> def g(s: String) = "g(" + s + ")"g: (String)java.lang.String

compose

compose 组合其他函数形成一个新的函数 f(g(x))

scala> val fComposeG = f _ compose g _fComposeG: (String) => java.lang.String = <function>scala> fComposeG("yay")res0: java.lang.String = f(g(yay))

andThen

andThen 和 compose很像,但是调用顺序是先调用第一个函数,然后调用第二个,即g(f(x))

scala> val fAndThenG = f _ andThen g _fAndThenG: (String) => java.lang.String = <function>scala> fAndThenG("yay")res1: java.lang.String = g(f(yay))

柯里化 vs 偏应用

case 语句

那么究竟什么是 case 语句?

这是一个名为 PartialFunction 的函数的子类。

多个 case 语句的集合是什么?

他们是共同组合在一起的多个 PartialFunction。

理解 PartialFunction(偏函数)

对给定的输入参数类型,函数可接受该类型的任何值。换句话说,一个(Int) => String的函数可以接收任意 Int 值,并返回一个字符串。

对给定的输入参数类型,偏函数只能接受该类型的某些特定的值。一个定义为(Int) => String 的偏函数可能不能接受所有 Int 值为输入。

isDefinedAt 是 PartialFunction 的一个方法,用来确定 PartialFunction 是否能接受一个给定的参数。

注意:偏函数 PartialFunction 和我们前面提到的部分应用函数是无关的。

参考 Effective Scala 对 [PartialFunction](http://twitter.github.com/effectivescala/#Functional programming-Partial functions) 的意见。

scala> val one: PartialFunction[Int, String] = { case 1 => "one" }one: PartialFunction[Int,String] = <function1>scala> one.isDefinedAt(1)res0: Boolean = truescala> one.isDefinedAt(2)res1: Boolean = false

您可以调用一个偏函数。

scala> one(1)res2: String = one

PartialFunctions 可以使用 orElse 组成新的函数,得到的 PartialFunction 反映了是否对给定参数进行了定义。

scala> val two: PartialFunction[Int, String] = { case 2 => "two" }two: PartialFunction[Int,String] = <function1>scala> val three: PartialFunction[Int, String] = { case 3 => "three" }three: PartialFunction[Int,String] = <function1>scala> val wildcard: PartialFunction[Int, String] = { case _ => "something else" }wildcard: PartialFunction[Int,String] = <function1>scala> val partial = one orElse two orElse three orElse wildcardpartial: PartialFunction[Int,String] = <function1>scala> partial(5)res24: String = something elsescala> partial(3)res25: String = threescala> partial(2)res26: String = twoscala> partial(1)res27: String = onescala> partial(0)res28: String = something else

case 之谜

上周我们看到一些新奇的东西。我们在通常应该使用函数的地方看到了一个 case 语句。

scala> case class PhoneExt(name: String, ext: Int)defined class PhoneExtscala> val extensions = List(PhoneExt("steve", 100), PhoneExt("robey", 200))extensions: List[PhoneExt] = List(PhoneExt(steve,100), PhoneExt(robey,200))scala> extensions.filter { case PhoneExt(name, extension) => extension < 200 }res0: List[PhoneExt] = List(PhoneExt(steve,100))

为什么这段代码可以工作?

filter 使用一个函数。在这个例子中是一个谓词函数(PhoneExt) => Boolean

PartialFunction 是 Function 的子类型,所以 filter 也可以使用 PartialFunction!

类型和多态基础

什么是静态类型?

按 Pierce 的话讲:“类型系统是一个语法方法,它们根据程序计算的值的种类对程序短语进行分类,通过分类结果错误行为进行自动检查。”

类型允许你表示函数的定义域和值域。例如,从数学角度看这个定义:

f: R -> N

它告诉我们函数“f”是从实数集到自然数集的映射。

抽象地说,这就是具体类型的准确定义。类型系统给我们提供了一些更强大的方式来表达这些集合。

鉴于这些注释,编译器可以静态地 (在编译时)验证程序是合理的。也就是说,如果值(在运行时)不符合程序规定的约束,编译将失败。

一般说来,类型检查只能保证不合理的程序不能编译通过。它不能保证每一个合理的程序都可以编译通过。

随着类型系统表达能力的提高,我们可以生产更可靠的代码,因为它能够在我们运行程序之前验证程序的不变性(当然是发现类型本身的模型 bug!)。学术界一直很努力地提高类型系统的表现力,包括值依赖(value-dependent)类型!

需要注意的是,所有的类型信息会在编译时被删去,因为它已不再需要。这就是所谓的擦除。

Scala 中的类型

Scala 强大的类型系统拥有非常丰富的表现力。其主要特性有:

  • 参数化多态性 粗略地说,就是泛型编程
  • (局部)类型推断 粗略地说,就是为什么你不需要这样写代码 val i: Int = 12: Int
  • 存在量化 粗略地说,为一些没有名称的类型进行定义
  • 视窗 我们将下周学习这些;粗略地说,就是将一种类型的值“强制转换”为另一种类型

参数化多态性

多态性是在不影响静态类型丰富性的前提下,用来(给不同类型的值)编写通用代码的。

例如,如果没有参数化多态性,一个通用的列表数据结构总是看起来像这样(事实上,它看起来很像使用泛型前的Java):

scala> 2 :: 1 :: "bar" :: "foo" :: Nilres5: List[Any] = List(2, 1, bar, foo)

现在我们无法恢复其中成员的任何类型信息。

scala> res5.headres6: Any = 2

所以我们的应用程序将会退化为一系列类型转换(“asInstanceOf[]”),并且会缺乏类型安全的保障(因为这些都是动态的)。

多态性是通过指定 类型变量 实现的。

scala> def drop1[A](l: List[A]) = l.taildrop1: [A](l: List[A])List[A]scala> drop1(List(1,2,3))res1: List[Int] = List(2, 3)

Scala 有秩 1 多态性

粗略地说,这意味着在 Scala 中,有一些你想表达的类型概念“过于泛化”以至于编译器无法理解。假设你有一个函数

def toList[A](a: A) = List(a)

你希望继续泛型地使用它:

def foo[A, B](f: A => List[A], b: B) = f(b)

这段代码不能编译,因为所有的类型变量只有在调用上下文中才被固定。即使你“钉住”了类型 B:

def foo[A](f: A => List[A], i: Int) = f(i)

…你也会得到一个类型不匹配的错误。

类型推断

静态类型的一个传统反对意见是,它有大量的语法开销。Scala 通过 类型推断 来缓解这个问题。

在函数式编程语言中,类型推断的经典方法是 Hindley Milner 算法,它最早是实现在 ML 中的。

Scala 类型推断系统的实现稍有不同,但本质类似:推断约束,并试图统一类型。

例如,在 Scala 中你无法这样做:

scala> { x => x }<console>:7: error: missing parameter type       { x => x }

而在 OCaml 中你可以:

# fun x -> x;;- : 'a -> 'a = <fun>

在 Scala 中所有类型推断是 局部的 。Scala 一次分析一个表达式。例如:

scala> def id[T](x: T) = xid: [T](x: T)Tscala> val x = id(322)x: Int = 322scala> val x = id("hey")x: java.lang.String = heyscala> val x = id(Array(1,2,3,4))x: Array[Int] = Array(1, 2, 3, 4)

类型信息都保存完好,Scala 编译器为我们进行了类型推断。请注意我们并不需要明确指定返回类型。

变性 Variance

Scala 的类型系统必须同时解释类层次和多态性。类层次结构可以表达子类关系。在混合 OO 和多态性时,一个核心问题是:如果 T’T 一个子类,Container[T’]应该被看做是 Container[T] 的子类吗?变性(Variance)注解允许你表达类层次结构和多态类型之间的关系:

名称含义Scala 标记
协变covariantC[T’]是 C[T] 的子类[+T]
逆变contravariantC[T] 是 C[T’]的子类[-T]
不变invariantC[T] 和 C[T’]无关[T]

子类型关系的真正含义:对一个给定的类型T,如果T’是其子类型,你能替换它吗?

scala> class Covariant[+A]defined class Covariantscala> val cv: Covariant[AnyRef] = new Covariant[String]cv: Covariant[AnyRef] = Covariant@4035acf6scala> val cv: Covariant[String] = new Covariant[AnyRef]<console>:6: error: type mismatch; found   : Covariant[AnyRef] required: Covariant[String]       val cv: Covariant[String] = new Covariant[AnyRef]                                   ^
scala> class Contravariant[-A]defined class Contravariantscala> val cv: Contravariant[String] = new Contravariant[AnyRef]cv: Contravariant[AnyRef] = Contravariant@49fa7bascala> val fail: Contravariant[AnyRef] = new Contravariant[String]<console>:6: error: type mismatch; found   : Contravariant[String] required: Contravariant[AnyRef]       val fail: Contravariant[AnyRef] = new Contravariant[String]                                     ^

逆变似乎很奇怪。什么时候才会用到它呢?令人惊讶的是,函数特质的定义就使用了它!

trait Function1 [-T1, +R] extends AnyRef

如果你仔细从替换的角度思考一下,会发现它是非常合理的。让我们先定义一个简单的类层次结构:

scala> class Animal { val sound = "rustle" }defined class Animalscala> class Bird extends Animal { override val sound = "call" }defined class Birdscala> class Chicken extends Bird { override val sound = "cluck" }defined class Chicken

假设你需要一个以 Bird 为参数的函数:

scala> val getTweet: (Bird => String) = // TODO

标准动物库有一个函数满足了你的需求,但它的参数是 Animal。在大多数情况下,如果你说“我需要一个___,我有一个___的子类”是可以的。但是,在函数参数这里是逆变的。如果你需要一个接受参数类型 Bird 的函数变量,但却将这个变量指向了接受参数类型为 Chicken 的函数,那么给它传入一个 Duck 时就会出错。然而,如果将该变量指向一个接受参数类型为 Animal 的函数就不会有这种问题:

scala> val getTweet: (Bird => String) = ((a: Animal) => a.sound )getTweet: Bird => String = <function1>

函数的返回值类型是协变的。如果你需要一个返回 Bird 的函数,但指向的函数返回类型是 Chicken,这当然是可以的。

scala> val hatch: (() => Bird) = (() => new Chicken )hatch: () => Bird = <function0>

边界

Scala 允许你通过边界来限制多态变量。这些边界表达了子类型关系。

scala> def cacophony[T](things: Seq[T]) = things map (_.sound)<console>:7: error: value sound is not a member of type parameter T       def cacophony[T](things: Seq[T]) = things map (_.sound)                                                        ^scala> def biophony[T <: Animal](things: Seq[T]) = things map (_.sound)biophony: [T <: Animal](things: Seq[T])Seq[java.lang.String]scala> biophony(Seq(new Chicken, new Bird))res5: Seq[java.lang.String] = List(cluck, call)

类型下界也是支持的,这让逆变和巧妙协变的引入得心应手。List[+T] 是协变的;一个 Bird 的列表也是 Animal 的列表。List 定义一个操作::(elem T)返回一个加入了 elem 的新的 List。新的 List 和原来的列表具有相同的类型:

scala> val flock = List(new Bird, new Bird)flock: List[Bird] = List(Bird@7e1ec70e, Bird@169ea8d2)scala> new Chicken :: flockres53: List[Bird] = List(Chicken@56fbda05, Bird@7e1ec70e, Bird@169ea8d2)

List 同样定义了::[B >: T](x: B) 来返回一个List[B]。请注意B >: T,这指明了类型B为类型T的超类。这个方法让我们能够做正确地处理在一个List[Bird]前面加一个 Animal 的操作:

scala> new Animal :: flockres59: List[Animal] = List(Animal@11f8d3a8, Bird@7e1ec70e, Bird@169ea8d2)

注意返回类型是 Animal。

量化

有时候,你并不关心是否能够命名一个类型变量,例如:

scala> def count[A](l: List[A]) = l.sizecount: [A](List[A])Int

这时你可以使用“通配符”取而代之:

scala> def count(l: List[_]) = l.sizecount: (List[_])Int

这相当于是下面代码的简写:

scala> def count(l: List[T forSome { type T }]) = l.sizecount: (List[T forSome { type T }])Int

注意量化会的结果会变得非常难以理解:

scala> def drop1(l: List[_]) = l.taildrop1: (List[_])List[Any]

突然,我们失去了类型信息!让我们细化代码看看发生了什么:

scala> def drop1(l: List[T forSome { type T }]) = l.taildrop1: (List[T forSome { type T }])List[T forSome { type T }]

我们不能使用 T 因为类型不允许这样做。

你也可以为通配符类型变量应用边界:

scala> def hashcodes(l: Seq[_ <: AnyRef]) = l map (_.hashCode)hashcodes: (Seq[_ <: AnyRef])Seq[Int]scala> hashcodes(Seq(1,2,3))<console>:7: error: type mismatch; found   : Int(1) required: AnyRefNote: primitive types are not implicitly converted to AnyRef.You can safely force boxing by casting x.asInstanceOf[AnyRef].       hashcodes(Seq(1,2,3))                     ^scala> hashcodes(Seq("one", "two", "three"))res1: Seq[Int] = List(110182, 115276, 110339486)

参考 D. R. MacIver 写的 Scala 中的存在类型

高级类型

视界(“类型类”)

有时候,你并不需要指定一个类型是等/子/超于另一个类,你可以通过转换这个类来伪装这种关联关系。一个视界指定一个类型可以被“看作是”另一个类型。这对对象的只读操作是很有用的。

隐函数允许类型自动转换。更确切地说,在隐式函数可以帮助满足类型推断时,它们允许按需的函数应用。例如:

scala> implicit def strToInt(x: String) = x.toIntstrToInt: (x: String)Intscala> "123"res0: java.lang.String = 123scala> val y: Int = "123"y: Int = 123scala> math.max("123", 111)res1: Int = 123

视界,就像类型边界,要求对给定的类型存在这样一个函数。您可以使用<%指定类型限制,例如:

scala> class Container[A <% Int] { def addIt(x: A) = 123 + x }defined class Container

这是说 A 必须“可被视”为 Int 。让我们试试。

scala> (new Container[String]).addIt("123")res11: Int = 246scala> (new Container[Int]).addIt(123) res12: Int = 246scala> (new Container[Float]).addIt(123.2F)<console>:8: error: could not find implicit value for evidence parameter of type (Float) => Int       (new Container[Float]).addIt(123.2)        ^

其他类型限制

方法可以通过隐含参数执行更复杂的类型限制。例如,List 支持对数字内容执行 sum,但对其他内容却不行。可是 Scala 的数字类型并不都共享一个超类,所以我们不能使用T <: Number。相反,要使之能工作,Scala 的 math 库对适当的类型 T 定义了一个隐含的 Numeric[T]。 然后在 List 定义中使用它:

sum[B >: A](implicit num: Numeric[B]): B

如果你调用List(1,2).sum(),你并不需要传入一个 num 参数;它是隐式设置的。但如果你调用 List("whoop").sum(),它会抱怨无法设置 num。

在没有设定陌生的对象为 Numeric 的时候,方法可能会要求某种特定类型的“证据”。这时可以使用以下类型-关系运算符:

  • A =:= B A 必须和 B 相等
  • A <:< B A 必须是 B 的子类
  • A <%< B A 必须可以被看做是 B
scala> class Container[A](value: A) { def addIt(implicit evidence: A =:= Int) = 123 + value }defined class Containerscala> (new Container(123)).addItres11: Int = 246scala> (new Container("123")).addIt<console>:10: error: could not find implicit value for parameter evidence: =:=[java.lang.String,Int]

类似地,根据之前的隐式转换,我们可以放松约束为可视性:

scala> class Container[A](value: A) { def addIt(implicit evidence: A <%< Int) = 123 + value }defined class Containerscala> (new Container("123")).addItres15: Int = 246

使用视图进行泛型编程

在 Scala 标准库中,视图主要用于实现集合的通用函数。例如“min”函数(在 Seq[] 上)就使用了这种技术:

def min[B >: A](implicit cmp: Ordering[B]): A = {  if (isEmpty)    throw new UnsupportedOperationException("empty.min")  reduceLeft((x, y) => if (cmp.lteq(x, y)) x else y)}

其主要优点是:

  • 集合中的元素并不是必须实现 Ordered 特质,但 Ordered 的使用仍然可以执行静态类型检查。
  • 无需任何额外的库支持,你也可以定义自己的排序:
scala> List(1,2,3,4).minres0: Int = 1scala> List(1,2,3,4).min(new Ordering[Int] { def compare(a: Int, b: Int) = b compare a })res3: Int = 4

作为旁注,标准库中有视图来将 Ordered 转换为 Ordering (反之亦然)。

trait LowPriorityOrderingImplicits {  implicit def ordered[A <: Ordered[A]]: Ordering[A] = new Ordering[A] {    def compare(x: A, y: A) = x.compare(y)  }}

上下文边界和implicitly[]

Scala2.8 引入了一种串联和访问隐式参数的快捷方式。

scala> def foo[A](implicit x: Ordered[A]) {}foo: [A](implicit x: Ordered[A])Unitscala> def foo[A : Ordered] {}                        foo: [A](implicit evidence$1: Ordered[A])Unit

隐式值可能会通过 implicitly 被访问

scala> implicitly[Ordering[Int]]res37: Ordering[Int] = scala.math.Ordering$Int$@3a9291cf

相结合后往往会使用更少的代码,尤其是串联视图的时候。

更高级多态性类型和特设多态性

Scala 可以对“更高阶”的类型进行抽象。例如,假设您需要用几种类型的容器处理几种类型的数据。你可能定义了一个 Container 的接口,它可以被实现为几种类型的容器:Option、List 等。你要定义可以使用这些容器里的值的接口,但不想确定值的类型。

这类似与函数柯里化。例如,尽管“一元类型”有类似List[A]的构造函数,这意味着我们必须满足一个“级别”的类型变量来产生一个具体的类型(就像一个没有柯里化的函数需要只提供一个参数列表来被调用),更高阶的类型需要更多。

scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }scala> val container = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }container: java.lang.Object with Container[List] = $anon$1@7c8e3f75scala> container.put("hey")res24: List[java.lang.String] = List(hey)scala> container.put(123)res25: List[Int] = List(123)

注意:Container是参数化类型的多态(“容器类型”)。

如果我们结合隐式转换 implicits 使用容器,我们会得到“特设的”多态性:即对容器写泛型函数的能力。

scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }scala> implicit val listContainer = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }scala> implicit val optionContainer = new Container[Some] { def put[A](x: A) = Some(x); def get[A](m: Some[A]) = m.get }scala> def tupleize[M[_]: Container, A, B](fst: M[A], snd: M[B]) = {     | val c = implicitly[Container[M]]                                  | c.put(c.get(fst), c.get(snd))     | }tupleize: [M[_],A,B](fst: M[A],snd: M[B])(implicit evidence$1: Container[M])M[(A, B)]scala> tupleize(Some(1), Some(2))res33: Some[(Int, Int)] = Some((1,2))scala> tupleize(List(1), List(2))res34: List[(Int, Int)] = List((1,2))

F-界多态性

通常有必要来访问一个(泛型)特质的具体子类。例如,想象你有一些泛型特质,但需要可以与它的某一子类进行比较。

trait Container extends Ordered[Container]

然而,现在比较方法是必须的了

def compare(that: Container): Int

因此,我们不能访问具体子类型,例如:

class MyContainer extends Container {  def compare(that: MyContainer): Int}

编译失败,因为我们对 Container 指定了 Ordered 特质,而不是对特定子类型指定的。

为了调和这一点,我们改用F-界的多态性。

trait Container[A <: Container[A]] extends Ordered[A]

奇怪的类型!但可以看到怎样对 A 实现了 Ordered 参数化,它本身就是 Container[A]

所以,现在

class MyContainer extends Container[MyContainer] {   def compare(that: MyContainer) = 0 }

他们是有序的了:

scala> List(new MyContainer, new MyContainer, new MyContainer)res3: List[MyContainer] = List(MyContainer@30f02a6d, MyContainer@67717334, MyContainer@49428ffa)scala> List(new MyContainer, new MyContainer, new MyContainer).minres4: MyContainer = MyContainer@33dfeb30

鉴于他们都是 Container[_] 的子类型,我们可以定义另一个子类并创建 Container[_] 的一个混合列表:

scala> class YourContainer extends Container[YourContainer] { def compare(that: YourContainer) = 0 }defined class YourContainerscala> List(new MyContainer, new MyContainer, new MyContainer, new YourContainer)                   res2: List[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]]   = List(MyContainer@3be5d207, MyContainer@6d3fe849, MyContainer@7eab48a7, YourContainer@1f2f0ce9)

注意结果类型是怎样成为 YourContainerMyContainer 类型确定的下界。这是类型推断的工作。有趣的是,这种类型甚至不需要是有意义的,它只是提供了一个合乎逻辑的最大下界为列表的统一类型。如果现在我们尝试使用 Ordered 会发生什么?

(new MyContainer, new MyContainer, new MyContainer, new YourContainer).min<console>:9: error: could not find implicit value for parameter cmp:  Ordering[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]]

对统一的类型 Ordered[] 不存在了。

结构类型

Scala 支持结构类型 structural types — 类型需求由接口构造表示,而不是由具体的类型表示。

scala> def foo(x: { def get: Int }) = 123 + x.getfoo: (x: AnyRef{def get: Int})Intscala> foo(new { def get = 10 })                 res0: Int = 133

这可能在很多场景都是相当不错的,但这个实现中使用了反射,所以要注意性能!

抽象类型成员

在特质中,你可以让类型成员保持抽象。

scala> trait Foo { type A; val x: A; def getX: A = x }defined trait Fooscala> (new Foo { type A = Int; val x = 123 }).getX   res3: Int = 123scala> (new Foo { type A = String; val x = "hey" }).getXres4: java.lang.String = hey

在做依赖注入等情况下,这往往是一个有用的技巧。

您可以使用 hash 操作符来引用一个抽象类型的变量:

scala> trait Foo[M[_]] { type t[A] = M[A] }defined trait Fooscala> val x: Foo[List]#t[Int] = List(1)x: List[Int] = List(1)

类型擦除和清单

正如我们所知道的,类型信息在编译的时候会因为擦除而丢失。 Scala 的清单(Manifests)功能,使我们能够选择性地恢复类型信息。清单提供了一个隐含值,根据需要由编译器生成。

scala> class MakeFoo[A](implicit manifest: Manifest[A]) { def make: A = manifest.erasure.newInstance.asInstanceOf[A] }scala> (new MakeFoo[String]).makeres10: String = ""

案例分析: Finagle

参见: https://github.com/twitter/finagle

trait Service[-Req, +Rep] extends (Req => Future[Rep])trait Filter[-ReqIn, +RepOut, +ReqOut, -RepIn]  extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut]){  def andThen[Req2, Rep2](next: Filter[ReqOut, RepIn, Req2, Rep2]) =    new Filter[ReqIn, RepOut, Req2, Rep2] {      def apply(request: ReqIn, service: Service[Req2, Rep2]) = {        Filter.this.apply(request, new Service[ReqOut, RepIn] {          def apply(request: ReqOut): Future[RepIn] = next(request, service)          override def release() = service.release()          override def isAvailable = service.isAvailable        })      }    }  def andThen(service: Service[ReqOut, RepIn]) = new Service[ReqIn, RepOut] {    private[this] val refcounted = new RefcountedService(service)    def apply(request: ReqIn) = Filter.this.apply(request, refcounted)    override def release() = refcounted.release()    override def isAvailable = refcounted.isAvailable  }    }

一个服务可以通过过滤器对请求进行身份验证。

trait RequestWithCredentials extends Request {  def credentials: Credentials}class CredentialsFilter(credentialsParser: CredentialsParser)  extends Filter[Request, Response, RequestWithCredentials, Response]{  def apply(request: Request, service: Service[RequestWithCredentials, Response]): Future[Response] = {    val requestWithCredentials = new RequestWrapper with RequestWithCredentials {      val underlying = request      val credentials = credentialsParser(request) getOrElse NullCredentials    }    service(requestWithCredentials)  }}

注意底层服务是如何需要对请求进行身份验证的,而且还是静态验证。因此,过滤器可以被看作是服务转换器。

许多过滤器可以被组合在一起:

val upFilter =  logTransaction     andThen  handleExceptions   andThen  extractCredentials andThen  homeUser           andThen  authenticate       andThen  route

享用安全的类型吧!

简单构建工具

关于 SBT

SBT 是一个现代化的构建工具。虽然它由 Scala 编写并提供了很多 Scala 便利,但它是一个通用的构建工具。

为什么选择 SBT?

  • 明智的依赖管理
  • 使用 Ivy 做依赖管理
  • “只在请求时更新”的模型
  • 对创建任务全面的 Scala 语言支持
  • 连续执行命令
  • 在项目上下文内启动解释器

入门

译注:最新的 SBT 安装方式请参考 scala-sbt 的文档

  • 下载 jar 包地址
  • 创建一个调用这个 jar 的 SBT shell 脚本,例如
java -Xmx512M -jar sbt-launch.jar "$@"
  • 确保它是可执行的,并在你的 path 下
  • 运行 sbt 来创建项目
[local ~/projects]$ sbtProject does not exist, create new project? (y/N/s) yName: sampleOrganization: com.twitterVersion [1.0]: 1.0-SNAPSHOTScala version [2.7.7]: 2.8.1sbt version [0.7.4]:      Getting Scala 2.7.7 ...:: retrieving :: org.scala-tools.sbt#boot-scala    confs: [default]    2 artifacts copied, 0 already retrieved (9911kB/221ms)Getting org.scala-tools.sbt sbt_2.7.7 0.7.4 ...:: retrieving :: org.scala-tools.sbt#boot-app    confs: [default]    15 artifacts copied, 0 already retrieved (4096kB/167ms)[success] Successfully initialized directory structure.Getting Scala 2.8.1 ...:: retrieving :: org.scala-tools.sbt#boot-scala    confs: [default]    2 artifacts copied, 0 already retrieved (15118kB/386ms)[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1[info]    using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7

可以看到它已经以较好的形式创建了项目的快照版本。

项目布局

  • 项目 – 项目定义文件
    • project/build/.scala – 主项目定义文件
    • project/build.properties – 项目、sbt 和 Scala 版本定义
  • src/main – 你的应用程序代码出现在这里,在子目录表明代码的语言(如src/main/scala, src/main/java
  • src/main/resources – 你想要添加到 jar 包中的静态文件(如日志配置)
  • src/test – 就像 src/main,不过是对测试
  • lib_managed – 你的项目依赖的 jar文件。由 sbt update 时填充
  • target – 生成物的目标路径(如自动生成的 thrift 代码,类文件,jar包)

添加一些代码

我们将为简单的 tweet 消息创建一个简单的 JSON 解析器。将以下代码加在这个文件中src/main/scala/com/twitter/sample/SimpleParser.scala

package com.twitter.samplecase class SimpleParsed(id: Long, text: String)class SimpleParser {  val tweetRegex = ""id":(.*),"text":"(.*)"".r  def parse(str: String) = {    tweetRegex.findFirstMatchIn(str) match {      case Some(m) => {        val id = str.substring(m.start(1), m.end(1)).toInt        val text = str.substring(m.start(2), m.end(2))        Some(SimpleParsed(id, text))      }      case _ => None    }  }}

这段代码丑陋并有 bug,但应该能够编译通过。

在控制台中的测试

SBT 既可以用作命令行脚本,也可以作为构建控制台。我们将主要利用它作为构建控制台,不过大多数命令可以作为参数传递给 SBT 独立运行,如

sbt test

需要注意如果一个命令需要参数,你需要使用引号包括住整个参数路径,例如

sbt 'test-only com.twitter.sample.SampleSpec'

这种方式很奇怪。

不管怎样,要开始我们的代码工作了,启动SBT吧

[local ~/projects/sbt-sample]$ sbt[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1[info]    using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7> 

SBT 允许你启动一个 Scala REPL 并加载所有项目依赖。它会在启动控制台前编译项目的源代码,从而为我们提供一个快速测试解析器的工作台。

> console[info] [info] == compile ==[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.[info] Compiling main sources...[info] Nothing to compile.[info]   Post-analysis: 3 classes.[info] == compile ==[info] [info] == copy-test-resources ==[info] == copy-test-resources ==[info] [info] == test-compile ==[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.[info] Compiling test sources...[info] Nothing to compile.[info]   Post-analysis: 0 classes.[info] == test-compile ==[info] [info] == copy-resources ==[info] == copy-resources ==[info] [info] == console ==[info] Starting scala interpreter...[info] Welcome to Scala version 2.8.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_22).Type in expressions to have them evaluated.Type :help for more information.scala> 

我们代码编译通过了,并提供了典型的 Scala 提示符。我们将创建一个新的解析器,一个 tweet 以确保其“能工作”

scala> import com.twitter.sample._            import com.twitter.sample._scala> val tweet = """{"id":1,"text":"foo"}"""tweet: java.lang.String = {"id":1,"text":"foo"}scala> val parser = new SimpleParser          parser: com.twitter.sample.SimpleParser = com.twitter.sample.SimpleParser@71060c3escala> parser.parse(tweet)                    res0: Option[com.twitter.sample.SimpleParsed] = Some(SimpleParsed(1,"foo"}))scala> 

添加依赖

我们简单的解析器对这个非常小的输入集工作正常,但我们需要添加更多的测试并让它出错。第一步是在我们的项目中添加 specs 测试库和一个真正的 JSON 解析器。要做到这一点,我们必须超越默认的 SBT 项目布局来创建一个项目。

SBT 认为 project/build 目录中的 Scala 文件是项目定义。添加以下内容到这个文件中project/build/SampleProject.scala

import sbt._class SampleProject(info: ProjectInfo) extends DefaultProject(info) {  val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1"  val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test"}

一个项目定义是一个 SBT 类。在上面例子中,我们扩展了 SBT 的 DefaultProject。

这里是通过 val 声明依赖。SBT 使用反射来扫描项目中的所有 val 依赖,并在构建时建立依赖关系树。这里使用的语法可能是新的,但本质和 Maven 依赖是相同的

<dependency>  <groupId>org.codehaus.jackson</groupId>  <artifactId>jackson-core-asl</artifactId>  <version>1.6.1</version></dependency><dependency>  <groupId>org.scala-tools.testing</groupId>  <artifactId>specs_2.8.0</artifactId>  <version>1.6.5</version>  <scope>test</scope></dependency>

现在可以下载我们的项目依赖了。在命令行中(而不是 sbt console 中)运行 sbt update

[local ~/projects/sbt-sample]$ sbt update[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1[info]    using SampleProject with sbt 0.7.4 and Scala 2.7.7[info] [info] == update ==[info] :: retrieving :: com.twitter#sample_2.8.1 [sync][info]  confs: [compile, runtime, test, provided, system, optional, sources, javadoc][info]  1 artifacts copied, 0 already retrieved (2785kB/71ms)[info] == update ==[success] Successful.[info] [info] Total time: 1 s, completed Nov 24, 2010 8:47:26 AM[info] [info] Total session time: 2 s, completed Nov 24, 2010 8:47:26 AM[success] Build completed successfully.

你会看到 sbt 检索到 specs 库。现在还增加了一个 lib_managed 目录,并且在 lib_managed/scala_2.8.1/test目录中包含 specs_2.8.0-1.6.5.jar

添加测试

现在有了测试库,可以把下面的测试代码写入src/test/scala/com/twitter/sample/SimpleParserSpec.scala文件

package com.twitter.sampleimport org.specs._object SimpleParserSpec extends Specification {  "SimpleParser" should {    val parser = new SimpleParser()    "work with basic tweet" in {      val tweet = """{"id":1,"text":"foo"}"""      parser.parse(tweet) match {        case Some(parsed) => {          parsed.text must be_==("foo")          parsed.id must be_==(1)        }        case _ => fail("didn't parse tweet")      }    }  }}

在 SBT 控制台中运行 test

> test[info] [info] == compile ==[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.[info] Compiling main sources...[info] Nothing to compile.[info]   Post-analysis: 3 classes.[info] == compile ==[info] [info] == test-compile ==[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.[info] Compiling test sources...[info] Nothing to compile.[info]   Post-analysis: 10 classes.[info] == test-compile ==[info] [info] == copy-test-resources ==[info] == copy-test-resources ==[info] [info] == copy-resources ==[info] == copy-resources ==[info] [info] == test-start ==[info] == test-start ==[info] [info] == com.twitter.sample.SimpleParserSpec ==[info] SimpleParserSpec[info] SimpleParser should[info]   + work with basic tweet[info] == com.twitter.sample.SimpleParserSpec ==[info] [info] == test-complete ==[info] == test-complete ==[info] [info] == test-finish ==[info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0[info]  [info] All tests PASSED.[info] == test-finish ==[info] [info] == test-cleanup ==[info] == test-cleanup ==[info] [info] == test ==[info] == test ==[success] Successful.[info] [info] Total time: 0 s, completed Nov 24, 2010 8:54:45 AM> 

我们的测试通过了!现在,我们可以增加更多。运行触发动作是 SBT 提供的优秀特性之一。在动作开始添加一个波浪线会启动一个循环,在源文件发生变化时重新运行动作。让我们运行 ~test 并看看会发生什么吧。

[info] == test ==[success] Successful.[info] [info] Total time: 0 s, completed Nov 24, 2010 8:55:50 AM1. Waiting for source changes... (press enter to interrupt)

现在,让我们添加下面的测试案例

 "reject a non-JSON tweet" in {      val tweet = """"id":1,"text":"foo""""      parser.parse(tweet) match {        case Some(parsed) => fail("didn't reject a non-JSON tweet")        case e => e must be_==(None)      }    }    "ignore nested content" in {      val tweet = """{"id":1,"text":"foo","nested":{"id":2}}"""      parser.parse(tweet) match {        case Some(parsed) => {          parsed.text must be_==("foo")          parsed.id must be_==(1)        }        case _ => fail("didn't parse tweet")      }    }    "fail on partial content" in {      val tweet = """{"id":1}"""      parser.parse(tweet) match {        case Some(parsed) => fail("didn't reject a partial tweet")        case e => e must be_==(None)      }    }

在我们保存文件后,SBT 会检测到变化,运行测试,并通知我们的解析器有问题

[info] == com.twitter.sample.SimpleParserSpec ==[info] SimpleParserSpec[info] SimpleParser should[info]   + work with basic tweet[info]   x reject a non-JSON tweet[info]     didn't reject a non-JSON tweet (Specification.scala:43)[info]   x ignore nested content[info]     'foo","nested":{"id' is not equal to 'foo' (SimpleParserSpec.scala:31)[info]   + fail on partial content

因此,让我们返工实现真正的 JSON 解析器

package com.twitter.sampleimport org.codehaus.jackson._import org.codehaus.jackson.JsonToken._case class SimpleParsed(id: Long, text: String)class SimpleParser {  val parserFactory = new JsonFactory()  def parse(str: String) = {    val parser = parserFactory.createJsonParser(str)    if (parser.nextToken() == START_OBJECT) {      var token = parser.nextToken()      var textOpt:Option[String] = None      var idOpt:Option[Long] = None      while(token != null) {        if (token == FIELD_NAME) {          parser.getCurrentName() match {            case "text" => {              parser.nextToken()              textOpt = Some(parser.getText())            }            case "id" => {              parser.nextToken()              idOpt = Some(parser.getLongValue())            }            case _ => // noop          }        }        token = parser.nextToken()      }      if (textOpt.isDefined && idOpt.isDefined) {        Some(SimpleParsed(idOpt.get, textOpt.get))      } else {        None      }    } else {      None    }  }}

这是一个简单的 Jackson 解析器。当我们保存,SBT 会重新编译代码和运行测试。代码变得越来越好了!

info] SimpleParser should[info]   + work with basic tweet[info]   + reject a non-JSON tweet[info]   x ignore nested content[info]     '2' is not equal to '1' (SimpleParserSpec.scala:32)[info]   + fail on partial content[info] == com.twitter.sample.SimpleParserSpec ==

哦。我们需要检查嵌套对象。让我们在 token 读取循环处添加一些丑陋的守卫。

  def parse(str: String) = {    val parser = parserFactory.createJsonParser(str)    var nested = 0    if (parser.nextToken() == START_OBJECT) {      var token = parser.nextToken()      var textOpt:Option[String] = None      var idOpt:Option[Long] = None      while(token != null) {        if (token == FIELD_NAME && nested == 0) {          parser.getCurrentName() match {            case "text" => {              parser.nextToken()              textOpt = Some(parser.getText())            }            case "id" => {              parser.nextToken()              idOpt = Some(parser.getLongValue())            }            case _ => // noop          }        } else if (token == START_OBJECT) {          nested += 1        } else if (token == END_OBJECT) {          nested -= 1        }        token = parser.nextToken()      }      if (textOpt.isDefined && idOpt.isDefined) {        Some(SimpleParsed(idOpt.get, textOpt.get))      } else {        None      }    } else {      None    }  }

…测试通过了!

打包和发布

现在我们已经可以运行 package 命令来生成一个 jar 文件。不过我们可能要与其他组分享我们的 jar 包。要做到这一点,我们将在 StandardProject 基础上构建,这给了我们一个良好的开端。

第一步是引入 StandardProject 为 SBT 插件。插件是一种为你的构建引进依赖的方式,注意不是为你的项目引入。这些依赖关系定义在 project/plugins/Plugins.scala 文件中。添加以下代码到 Plugins.scala 文件中。

import sbt._class Plugins(info: ProjectInfo) extends PluginDefinition(info) {  val twitterMaven = "twitter.com" at "http://maven.twttr.com/"  val defaultProject = "com.twitter" % "standard-project" % "0.7.14"}

注意我们指定了一个 Maven 仓库和一个依赖。这是因为这个标准项目库是由 twitter 托管的,不在 SBT 默认检查的仓库中。

我们也将更新项目定义来扩展 StandardProject,包括 SVN 发布特质,和我们希望发布的仓库定义。修改SampleProject.scala

import sbt._import com.twitter.sbt._class SampleProject(info: ProjectInfo) extends StandardProject(info) with SubversionPublisher {  val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1"  val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test"  override def subversionRepository = Some("http://svn.local.twitter.com/maven/")}

现在如果我们运行发布操作,将看到以下输出

[info] == deliver ==IvySvn Build-Version: nullIvySvn Build-DateTime: null[info] :: delivering :: com.twitter#sample;1.0-SNAPSHOT :: 1.0-SNAPSHOT :: release :: Wed Nov 24 10:26:45 PST 2010[info]  delivering ivy file to /Users/mmcbride/projects/sbt-sample/target/ivy-1.0-SNAPSHOT.xml[info] == deliver ==[info] [info] == make-pom ==[info] Wrote /Users/mmcbride/projects/sbt-sample/target/sample-1.0-SNAPSHOT.pom[info] == make-pom ==[info] [info] == publish ==[info] :: publishing :: com.twitter#sample[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar[info]  published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom[info]  published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml[info]  published ivy to com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml[info] Binary diff deleting com/twitter/sample/1.0-SNAPSHOT[info] Commit finished r977 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010[info] Copying from com/twitter/sample/.upload to com/twitter/sample/1.0-SNAPSHOT[info] Binary diff finished : r978 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010[info] == publish ==[success] Successful.[info] [info] Total time: 4 s, completed Nov 24, 2010 10:26:47 AM

这样(一段时间后),就可以在 binaries.local.twitter.com 上看到我们发布的 jar 包。

添加任务

任务就是 Scala 函数。添加一个任务最简单的方法是,在你的项目定义中引入一个 val 定义的任务方法,如

lazy val print = task {log.info("a test action"); None}

你也可以这样加上依赖和描述

lazy val print = task {log.info("a test action"); None}.dependsOn(compile) describedAs("prints a line after compile")

刷新项目,并执行 print 操作,我们将看到以下输出

> print[info] [info] == print ==[info] a test action[info] == print ==[success] Successful.[info] [info] Total time: 0 s, completed Nov 24, 2010 11:05:12 AM> 

所以它起作用了。如果你只是在一个项目定义一个任务的话,这工作得很好。然而如果你定义的是一个插件的话,它就很不灵活了。我可能要

lazy val print = printActiondef printAction = printTask.dependsOn(compile) describedAs("prints a line after compile")def printTask = task {log.info("a test action"); None}

这可以让消费者覆盖任务本身,依赖和/或任务的描述,或动作本身。大多数 SBT 内建的动作都遵循这种模式。作为一个例子,我们可以通过修改内置打包任务来打印当前时间戳

lazy val printTimestamp = task { log.info("current time is " + System.currentTimeMillis); None}override def packageAction = super.packageAction.dependsOn(printTimestamp)

有很多例子介绍了怎样调整 SBT 默认的 StandardProject,和如何添加自定义任务。

快速参考

常用命令

  • actions – 显示这个项目中可用的动作
  • update – 下载依赖
  • compile – 编译源文件
  • test – 运行测试
  • package – 创建一个可发布的 jar 文件
  • publish-local – 在本地 ivy 缓存中安装构建好的jar包
  • publish – 将你的 jar 推到一个远程库中(如果配置了的话)

更多命令

  • test-failed – 运行所有失败的规格测试
  • test-quick – 运行任何失败的和/或依赖更新的规格
  • clean-cache – 删除 SBT 缓存各种的东西。就像 sbt 的 clean 命令
  • clean-lib – 删除 lib_managed 下的一切

更多的集合

Scala 提供了一套很好的集合实现,提供了一些集合类型的抽象。这让你的代码可以与 Foo 的集合交互,而无需担心该集合是是一个 List,还是 Set,或是任何你有的类型。

这里提供了一个很好的页面来查看各种集合的默认实现,并链接到他们的 scala 在线文档。

基础知识

表 List

标准的链表。

scala> List(1, 2, 3)res0: List[Int] = List(1, 2, 3)

你可以用函数式语言的方式连接它们。

scala> 1 :: 2 :: 3 :: Nilres1: List[Int] = List(1, 2, 3)

参考 API文档

集 Set

集没有重复

scala> Set(1, 1, 2)res2: scala.collection.immutable.Set[Int] = Set(1, 2)

参考 API文档

序列 Seq

序列有一个给定的顺序。

scala> Seq(1, 1, 2)res3: Seq[Int] = List(1, 1, 2)

(请注意返回的是一个列表。因为 Seq 是一个特质;而列表是序列的很好实现。如你所见,Seq 也是一个工厂单例对象,可以用来创建列表。)

参考 API文档

映射 Map

映射是键值容器。

scala> Map('a' -> 1, 'b' -> 2)res4: scala.collection.immutable.Map[Char,Int] = Map((a,1), (b,2))

参考 API文档

层次结构

下面介绍的都是特质,它们在可变的(mutable)和不可变的(immutable)的包中都有特定实现。

Traversable

所有集合都可以被遍历。这个特质定义了标准函数组合子。 这些组合子根据 foreach 来写,所有集合必须实现。

参考 API文档

Iterable

iterator() 方法返回一个 Iterator 来迭代元素。

参考 API文档

Seq 序列

有顺序的对象序列。

参考 API文档

Set 集

没有重复的对象集合。

参考 API文档

Map

键值对。

参考 API文档

方法

Traversable

下面所有方法在子类中都是可用的。参数和返回值的类型可能会因为子类的覆盖而看起来不同。

def head : Adef tail : Traversable[A]

这里是函数组合子定义的地方。

def map [B] (f: (A) => B) : CC[B]

返回每个元素都被 f 转化的集合

def foreach[U](f: Elem => U): Unit

在集合中的每个元素上执行 f 。

def find (p: (A) => Boolean) : Option[A]

返回匹配谓词函数的第一个元素

def filter (p: (A) => Boolean) : Traversable[A]

返回所有匹配谓词函数的元素集合

划分:

def partition (p: (A) ⇒ Boolean) : (Traversable[A], Traversable[A])

按照谓词函数把一个集合分割成两部分

def groupBy [K] (f: (A) => K) : Map[K, Traversable[A]]

转换:

有趣的是,你可以转换集合类型。

def toArray : Array[A]def toArray [B >: A] (implicit arg0: ClassManifest[B]) : Array[B]def toBuffer [B >: A] : Buffer[B]def toIndexedSeq [B >: A] : IndexedSeq[B]def toIterable : Iterable[A]def toIterator : Iterator[A]def toList : List[A]def toMap [T, U] (implicit ev: <:<[A, (T, U)]) : Map[T, U]def toSeq : Seq[A]def toSet [B >: A] : Set[B]def toStream : Stream[A]def toString () : Stringdef toTraversable : Traversable[A]

把映射转换为一个数组,您会得到一个键值对的数组。

scala> Map(1 -> 2).toArrayres41: Array[(Int, Int)] = Array((1,2))

Iterable

添加一个迭代器的访问。

  def iterator: Iterator[A]

一个迭代器能给你提供什么?

def hasNext(): Booleandef next(): A

这是非常 Java 式的。你通常不会看到在 Scala 中使用迭代器,通常更容易出现的是函数组合器或 for 循环的使用。

Set

  def contains(key: A): Boolean  def +(elem: A): Set[A]  def -(elem: A): Set[A]

Map

通过键查找的键值对的序列。

可以像这样将一个键值对列表传入 apply()

scala> Map("a" -> 1, "b" -> 2)res0: scala.collection.immutable.Map[java.lang.String,Int] = Map((a,1), (b,2))

或者像这样:

scala> Map(("a", 2), ("b", 2))res0: scala.collection.immutable.Map[java.lang.String,Int] = Map((a,2), (b,2))

题外话

什么是->?这不是特殊的语法,这是一个返回元组的方法。

scala> "a" -> 2res0: (java.lang.String, Int) = (a,2)

请记住,这仅仅是下面代码的语法糖

scala> "a".->(2)res1: (java.lang.String, Int) = (a,2)

您也可以使用++操作符构建

scala> Map.empty ++ List(("a", 1), ("b", 2), ("c", 3))res0: scala.collection.immutable.Map[java.lang.String,Int] = Map((a,1), (b,2), (c,3))

常用的子类

HashSet 和 HashMap 的快速查找,这些集合的最常用的形式。 HashSet API, HashMap API

TreeMap 是 SortedMap 的一个子类,它可以让你进行有序访问。 [TreeMap API]()

Vector 快速随机选择和快速更新。 Vector API

scala> IndexedSeq(1, 2, 3)res0: IndexedSeq[Int] = Vector(1, 2, 3)

Range 等间隔的 Int 有序序列。你经常会在 for 循环看到。 Range API

scala> for (i <- 1 to 3) { println(i) }123

Ranges 支持标准的函数组合子。

scala> (1 to 3).map { i => i }res0: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3)

默认实现

使用特质的 apply 方法会给你默认实现的实例,例如,Iterable(1, 2)会返回一个列表作为其默认实现。

scala> Iterable(1, 2)res0: Iterable[Int] = List(1, 2)

序列 Seq 也是一样的,正如我们前面所看到的

scala> Seq(1, 2)res3: Seq[Int] = List(1, 2)scala> Iterable(1, 2)res1: Iterable[Int] = List(1, 2)scala> Sequence(1, 2)warning: there were deprecation warnings; re-run with -deprecation for detailsres2: Seq[Int] = List(1, 2)

Set

scala> Set(1, 2)res31: scala.collection.immutable.Set[Int] = Set(1, 2)

一些描述性的特质

IndexedSeq 快速随机访问元素和一个快速的长度操作。"API 文档":http://www.scala-lang.org/api/current/scala/collection/IndexedSeq.html

LinearSeq 通过 head 快速访问第一个元素,也有一个快速的 tail 操作。 API 文档

可变 vs 不可变

不可变

优点

  • 在多线程中不会改变

缺点

  • 一点也不能改变

Scala 允许我们是务实的,它鼓励不变性,但不惩罚我们需要的可变性。这和 var vs. val 非常相似。我们总是先从 val 开始并在必要时回退为 var。

我们赞成使用不可改变的版本的集合,但如果性能使然,也可以切换到可变的。使用不可变集合意味着你在多线程不会意外地改变事物。

可变集合

前面讨论的所有类都是不可变的。让我们来讨论常用的可变集合。

HashMap 定义了 getOrElseUpdate, += HashMap API

scala> val numbers = collection.mutable.Map(1 -> 2)numbers: scala.collection.mutable.Map[Int,Int] = Map((1,2))scala> numbers.get(1)res0: Option[Int] = Some(2)scala> numbers.getOrElseUpdate(2, 3)res54: Int = 3scala> numbersres55: scala.collection.mutable.Map[Int,Int] = Map((2,3), (1,2))scala> numbers += (4 -> 1)res56: numbers.type = Map((2,3), (4,1), (1,2))

与 Java 生活

您可以通过 JavaConverters package 轻松地在 Java 和 Scala 的集合类型之间转换。它用 asScala 装饰常用的 Java 集合以和用 asJava 方法装饰 Scala 集合。

   import scala.collection.JavaConverters._   val sl = new scala.collection.mutable.ListBuffer[Int]   val jl : java.util.List[Int] = sl.asJava   val sl2 : scala.collection.mutable.Buffer[Int] = jl.asScala   assert(sl eq sl2)

双向转换:

scala.collection.Iterable <=> java.lang.Iterablescala.collection.Iterable <=> java.util.Collectionscala.collection.Iterator <=> java.util.{ Iterator, Enumeration }scala.collection.mutable.Buffer <=> java.util.Listscala.collection.mutable.Set <=> java.util.Setscala.collection.mutable.Map <=> java.util.{ Map, Dictionary }scala.collection.mutable.ConcurrentMap <=> java.util.concurrent.ConcurrentMap

此外,也提供了以下单向转换

scala.collection.Seq => java.util.Listscala.collection.mutable.Seq => java.util.Listscala.collection.Set => java.util.Setscala.collection.Map => java.util.Map

使用 specs 测试

扩展规格

让我们直接开始。

import org.specs._object ArithmeticSpec extends Specification {  "Arithmetic" should {    "add two numbers" in {      1 + 1 mustEqual 2    }    "add three numbers" in {      1 + 1 + 1 mustEqual 3    }  }}

Arithmetic(算术) 是一个 规范约束下的系统

add(加) 是一个上下文。

add two numbers(两个数相加),和 add three numbers(三个数字相加) 是例子。

mustEqual 表示 预期

1 mustEqual 1 是编写实际测试前使用的一种常见的预期占位符。所有的测试用例都应该至少有一个预期。

复制

注意到两个测试都是怎样将 add 加在其名称中的吗?我们可以通过嵌套预期摆脱这种重复。

import org.specs._object ArithmeticSpec extends Specification {  "Arithmetic" should {    "add" in {      "two numbers" in {        1 + 1 mustEqual 2      }      "three numbers" in {        1 + 1 + 1 mustEqual 3      }    }  }}

执行模型

object ExecSpec extends Specification {  "Mutations are isolated" should {    var x = 0    "x equals 1 if we set it." in {      x = 1      x mustEqual 1    }    "x is the default value if we don't change it" in {      x mustEqual 0    }  }}

Setup, Teardown

doBefore & doAfter

"my system" should {  doBefore { resetTheSystem() /** user-defined reset function */ }  "mess up the system" in {...}  "and again" in {...}  doAfter { cleanThingsUp() }}

注意 doBefore/doAfter 只能运行在叶子用例上。

doFirst & doLast

doFirst/doLast 用来做一次性的设置。(需要例子,我不使用这个)

"Foo" should {  doFirst { openTheCurtains() }  "test stateless methods" in {...}  "test other stateless methods" in {...}  doLast { closeTheCurtains() }}

Matchers

你有数据,并且想要确保它是正确的。让我们看看最常用的匹配器是如何帮助你的。

mustEqual

我们已经看到几个 mustEqual 的例子了。

1 mustEqual 1"a" mustEqual "a"

引用相等,值相等。

序列中的元素

val numbers = List(1, 2, 3)numbers must contain(1)numbers must not contain(4)numbers must containAll(List(1, 2, 3))numbers must containInOrder(List(1, 2, 3))List(1, List(2, 3, List(4)), 5) must haveTheSameElementsAs(List(5, List(List(4), 2, 3), 1))

映射中的元素

map must haveKey(k)map must notHaveKey(k)map must haveValue(v)map must notHaveValue(v)

数字

a must beGreaterThan(b)a must beGreaterThanOrEqualTo(b)a must beLessThan(b)a must beLessThanOrEqualTo(b)a must beCloseTo(b, delta)

Options

a must beNonea must beSome[Type]a must beSomethinga must beSome(value)

throwA

a must throwA[WhateverException]

这是一个针对trycatch块中有异常抛出的用例的简写。

您也可以期望一个特定的消息

a must throwA(WhateverException("message"))

您也可以匹配异常:

a must throwA(new Exception) like {  case Exception(m) => m.startsWith("bad")}

编写你自己的匹配器

import org.specs.matcher.Matcher

作为一个不变量

"A matcher" should {  "be created as a val" in {    val beEven = new Matcher[Int] {      def apply(n: => Int) = {        (n % 2 == 0, "%d is even".format(n), "%d is odd".format(n))      }    }    2 must beEven  }}

契约是返回一个包含三个值的元组,分别是期望是否为真、为真时的消息和为假时的消息。

作为一个样本类

case class beEven(b: Int) extends Matcher[Int]() {  def apply(n: => Int) =  (n % 2 == 0, "%d is even".format(n), "%d is odd".format(n))}

使用样本类可以增加代码的重用性。

Mocks

import org.specs.Specificationimport org.specs.mock.Mockitoclass Foo[T] {  def get(i: Int): T}object MockExampleSpec extends Specification with Mockito {  val m = mock[Foo[String]]  m.get(0) returns "one"  m.get(0)  there was one(m).get(0)  there was no(m).get(1)}

参考 Using Mockito

Spies

Spies(间谍)可以对真正的对象做一些“局部 mocking”:

val list = new LinkedList[String]val spiedList = spy(list)// methods can be stubbed on a spyspiedList.size returns 100// other methods can also be usedspiedList.add("one")spiedList.add("two")// and verification can happen on a spythere was one(spiedList).add("one")

然而,使用间谍可能会出现非常诡异的情况:

// if the list is empty, this will throws an IndexOutOfBoundsExceptionspiedList.get(0) returns "one"

这里必须使用 doReturn :

doReturn("one").when(spiedList).get(0)

在 sbt 中运行单个 specs

> test-only com.twitter.yourservice.UserSpec

将只运行那个规范。

> ~ test-only com.twitter.yourservice.UserSpec

将在一个循环中运行该测试,文件的每一次修改都将触发测试运行。

Scala 并发编程

Runnable/Callable

Runnable 接口只有一个没有返回值的方法。

trait Runnable {  def run(): Unit}Callable与之类似,除了它有一个返回值trait Callable[V] {  def call(): V}

线程

Scala 并发是建立在 Java 并发模型基础上的。

在 Sun JVM 上,对 IO 密集的任务,我们可以在一台机器运行成千上万个线程。

一个线程需要一个 Runnable。你必须调用线程的 start 方法来运行 Runnable。

scala> val hello = new Thread(new Runnable {  def run() {    println("hello world")  }})hello: java.lang.Thread = Thread[Thread-3,5,main]scala> hello.starthello world

当你看到一个类实现了 Runnable 接口,你就知道它的目的是运行在一个线程中。

单线程代码

这里有一个可以工作但有问题的代码片断。

import java.net.{Socket, ServerSocket}import java.util.concurrent.{Executors, ExecutorService}import java.util.Dateclass NetworkService(port: Int, poolSize: Int) extends Runnable {  val serverSocket = new ServerSocket(port)  def run() {    while (true) {      // This will block until a connection comes in.      val socket = serverSocket.accept()      (new Handler(socket)).run()    }  }}class Handler(socket: Socket) extends Runnable {  def message = (Thread.currentThread.getName() + "
").getBytes  def run() {    socket.getOutputStream.write(message)    socket.getOutputStream.close()  }}(new NetworkService(2020, 2)).run

每个请求都会回应当前线程的名称,所以结果始终是 main 。

这段代码的主要缺点是在同一时间,只有一个请求可以被相应!

你可以把每个请求放入一个线程中处理。只要简单改变

(new Handler(socket)).run()

(new Thread(new Handler(socket))).start()

但如果你想重用线程或者对线程的行为有其他策略呢?

Executors

随着 Java 5 的发布,它决定提供一个针对线程的更抽象的接口。

你可以通过 Executors 对象的静态方法得到一个 ExecutorService 对象。这些方法为你提供了可以通过各种政策配置的 ExecutorService ,如线程池。

下面改写我们之前的阻塞式网络服务器来允许并发请求。

import java.net.{Socket, ServerSocket}import java.util.concurrent.{Executors, ExecutorService}import java.util.Dateclass NetworkService(port: Int, poolSize: Int) extends Runnable {  val serverSocket = new ServerSocket(port)  val pool: ExecutorService = Executors.newFixedThreadPool(poolSize)  def run() {    try {      while (true) {        // This will block until a connection comes in.        val socket = serverSocket.accept()        pool.execute(new Handler(socket))      }    } finally {      pool.shutdown()    }  }}class Handler(socket: Socket) extends Runnable {  def message = (Thread.currentThread.getName() + "
").getBytes  def run() {    socket.getOutputStream.write(message)    socket.getOutputStream.close()  }}(new NetworkService(2020, 2)).run

这里有一个连接脚本展示了内部线程是如何重用的。

$ nc localhost 2020pool-1-thread-1$ nc localhost 2020pool-1-thread-2$ nc localhost 2020pool-1-thread-1$ nc localhost 2020pool-1-thread-2

Futures

Future 代表异步计算。你可以把你的计算包装在 Future 中,当你需要计算结果的时候,你只需调用一个阻塞的 get() 方法就可以了。一个 Executor 返回一个 Future 。如果使用 Finagle RPC 系统,你可以使用 Future 实例持有可能尚未到达的结果。

一个 FutureTask 是一个 Runnable 实现,就是被设计为由 Executor 运行的

val future = new FutureTask[String](new Callable[String]() {  def call(): String = {    searcher.search(target);}})executor.execute(future)

现在我需要结果,所以阻塞直到其完成。

val blockingResult = future.get()

参考 Scala School 的 Finagle 介绍中大量使用了 Future,包括一些把它们结合起来的不错的方法。以及 Effective Scala 对 [Futures](http://twitter.github.com/effectivescala/#Twitter's standard libraries-Futures) 的意见。

线程安全问题

class Person(var name: String) {  def set(changedName: String) {    name = changedName  }}

这个程序在多线程环境中是不安全的。如果有两个线程有引用到同一个 Person 实例,并调用 set ,你不能预测两个调用结束后 name 的结果。

在 Java 内存模型中,允许每个处理器把值缓存在 L1 或 L2 缓存中,所以在不同处理器上运行的两个线程都可以有自己的数据视图。

让我们来讨论一些工具,来使线程保持一致的数据视图。

三种工具

同步

互斥锁(Mutex)提供所有权语义。当你进入一个互斥体,你拥有它。同步是 JVM 中使用互斥锁最常见的方式。在这个例子中,我们会同步 Person。

在 JVM 中,你可以同步任何不为 null 的实例。

class Person(var name: String) {  def set(changedName: String) {    this.synchronized {      name = changedName    }  }}

volatile

随着 Java 5 内存模型的变化,volatile 和 synchronized 基本上是相同的,除了 volatile 允许空值。

synchronized 允许更细粒度的锁。 而 volatile 则对每次访问同步。

class Person(@volatile var name: String) {  def set(changedName: String) {    name = changedName  }}

AtomicReference

此外,在 Java 5 中还添加了一系列低级别的并发原语。 AtomicReference 类是其中之一

import java.util.concurrent.atomic.AtomicReferenceclass Person(val name: AtomicReference[String]) {  def set(changedName: String) {    name.set(changedName)  }}

这个成本是什么?

AtomicReference 是这两种选择中最昂贵的,因为你必须去通过方法调度(method dispatch)来访问值。

volatile 和 synchronized 是建立在 Java 的内置监视器基础上的。如果没有资源争用,监视器的成本很小。由于 synchronized 允许你进行更细粒度的控制权,从而会有更少的争夺,所以 synchronized 往往是最好的选择。

当你进入同步点,访问 volatile 引用,或去掉 AtomicReferences 引用时, Java 会强制处理器刷新其缓存线从而提供了一致的数据视图。

如果我错了,请大家指正。这是一个复杂的课题,我敢肯定要弄清楚这一点需要一个漫长的课堂讨论。

Java 5 的其他灵巧的工具

正如前面提到的 AtomicReference ,Java 5 带来了许多很棒的工具。

CountDownLatch

CountDownLatch 是一个简单的多线程互相通信的机制。

val doneSignal = new CountDownLatch(2)doAsyncWork(1)doAsyncWork(2)doneSignal.await()println("both workers finished!")

先不说别的,这是一个优秀的单元测试。比方说,你正在做一些异步工作,并要确保功能完成。你的函数只需要 倒数计数(countDown) 并在测试中 等待(await) 就可以了。

AtomicInteger/Long

由于对 Int 和 Long 递增是一个经常用到的任务,所以增加了 AtomicInteger 和 AtomicLong 。

AtomicBoolean

我可能不需要解释这是什么。

ReadWriteLocks

读写锁(ReadWriteLock) 使你拥有了读线程和写线程的锁控制。当写线程获取锁的时候读线程只能等待。

让我们构建一个不安全的搜索引擎

下面是一个简单的倒排索引,它不是线程安全的。我们的倒排索引按名字映射到一个给定的用户。

这里的代码天真地假设只有单个线程来访问。

注意使用了 mutable.HashMap 替代了默认的构造函数 this()

import scala.collection.mutablecase class User(name: String, id: Int)class InvertedIndex(val userMap: mutable.Map[String, User]) {  def this() = this(new mutable.HashMap[String, User])  def tokenizeName(name: String): Seq[String] = {    name.split(" ").map(_.toLowerCase)  }  def add(term: String, user: User) {    userMap += term -> user  }  def add(user: User) {    tokenizeName(user.name).foreach { term =>      add(term, user)    }  }}

这里没有写如何从索引中获取用户。稍后我们会补充。

让我们把它变为线程安全

在上面的倒排索引例子中,userMap 不能保证是线程安全的。多个客户端可以同时尝试添加项目,并有可能出现前面 Person 例子中的视图错误。

由于 userMap 不是线程安全的,那我们怎样保持在同一个时间只有一个线程能改变它呢?

你可能会考虑在做添加操作时锁定 userMap。

def add(user: User) {  userMap.synchronized {    tokenizeName(user.name).foreach { term =>      add(term, user)    }  }}

不幸的是,这个粒度太粗了。一定要试图在互斥锁以外做尽可能多的耗时的工作。还记得我说过如果不存在资源争夺,锁开销就会很小吗。如果在锁代码块里面做的工作越少,争夺就会越少。

def add(user: User) {  // tokenizeName was measured to be the most expensive operation.  val tokens = tokenizeName(user.name)  tokens.foreach { term =>    userMap.synchronized {      add(term, user)    }  }}

SynchronizedMap

我们可以通过 SynchronizedMap 特质将同步混入一个可变的 HashMap。

我们可以扩展现有的 InvertedIndex,提供给用户一个简单的方式来构建同步索引。

import scala.collection.mutable.SynchronizedMapclass SynchronizedInvertedIndex(userMap: mutable.Map[String, User]) extends InvertedIndex(userMap) {  def this() = this(new mutable.HashMap[String, User] with SynchronizedMap[String, User])}

如果你看一下其实现,你就会意识到,它只是在每个方法上加同步锁来保证其安全性,所以它很可能没有你希望的性能。

Java ConcurrentHashMap

Java 有一个很好的线程安全的 ConcurrentHashMap。值得庆幸的是,我们可以通过 JavaConverters 获得不错的 Scala 语义。

事实上,我们可以通过扩展老的不安全的代码,来无缝地接入新的线程安全 InvertedIndex。

import java.util.concurrent.ConcurrentHashMapimport scala.collection.JavaConverters._class ConcurrentInvertedIndex(userMap: collection.mutable.ConcurrentMap[String, User])    extends InvertedIndex(userMap) {  def this() = this(new ConcurrentHashMap[String, User] asScala)}

让我们加载 InvertedIndex

原始方式

trait UserMaker {  def makeUser(line: String) = line.split(",") match {    case Array(name, userid) => User(name, userid.trim().toInt)  }}class FileRecordProducer(path: String) extends UserMaker {  def run() {    Source.fromFile(path, "utf-8").getLines.foreach { line =>      index.add(makeUser(line))    }  }}

对于文件中的每一行,我们可以调用 makeUser 然后 add 到 InvertedIndex中。如果我们使用并发 InvertedIndex,我们可以并行调用 add 因为 makeUser 没有副作用,所以我们的代码已经是线程安全的了。

我们不能并行读取文件,但我们可以并行构造用户并且把它添加到索引中。

一个解决方案:生产者/消费者

异步计算的一个常见模式是把消费者和生产者分开,让他们只能通过队列(Queue) 沟通。让我们看看如何将这个模式应用在我们的搜索引擎索引中。

import java.util.concurrent.{BlockingQueue, LinkedBlockingQueue}// Concrete producerclass Producer[T](path: String, queue: BlockingQueue[T]) extends Runnable {  def run() {    Source.fromFile(path, "utf-8").getLines.foreach { line =>      queue.put(line)    }  }}// Abstract consumerabstract class Consumer[T](queue: BlockingQueue[T]) extends Runnable {  def run() {    while (true) {      val item = queue.take()      consume(item)    }  }  def consume(x: T)}val queue = new LinkedBlockingQueue[String]()// One thread for the producerval producer = new Producer[String]("users.txt", q)new Thread(producer).start()trait UserMaker {  def makeUser(line: String) = line.split(",") match {    case Array(name, userid) => User(name, userid.trim().toInt)  }}class IndexerConsumer(index: InvertedIndex, queue: BlockingQueue[String]) extends Consumer[String](queue) with UserMaker {  def consume(t: String) = index.add(makeUser(t))}// Let's pretend we have 8 cores on this machine.val cores = 8val pool = Executors.newFixedThreadPool(cores)// Submit one consumer per core.for (i <- i to cores) {  pool.submit(new IndexerConsumer[String](index, q))}

Java 与 Scala

Javap

javap 的是 JDK 附带的一个工具。不是 JRE,这里是有区别的。javap 反编译类定义,给你展示里面有什么。用法很简单

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTraitCompiled from "Scalaisms.scala"public interface com.twitter.interop.MyTrait extends scala.ScalaObject{    public abstract java.lang.String traitName();    public abstract java.lang.String upperTraitName();}

如果你是底层控可以看看字节码

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap -c MyTrait$classCompiled from "Scalaisms.scala"public abstract class com.twitter.interop.MyTrait$class extends java.lang.Object{public static java.lang.String upperTraitName(com.twitter.interop.MyTrait);  Code:   0:   aload_0   1:   invokeinterface #12,  1; //InterfaceMethod com/twitter/interop/MyTrait.traitName:()Ljava/lang/String;   6:   invokevirtual   #17; //Method java/lang/String.toUpperCase:()Ljava/lang/String;   9:   areturnpublic static void $init$(com.twitter.interop.MyTrait);  Code:   0:   return}

如果你搞不清为什么程序在 Java 上不起作用,就用 javap 看看吧!

在 Java 中使用 Scala 类 时要考虑的四个要点

  • 类参数
  • 类常量
  • 类变量
  • 异常

我们将构建一个简单的 Scala 类来展示这一系列实体

package com.twitter.interopimport java.io.IOExceptionimport scala.throwsimport scala.reflect.{BeanProperty, BooleanBeanProperty}class SimpleClass(name: String, val acc: String, @BeanProperty var mutable: String) {  val foo = "foo"  var bar = "bar"  @BeanProperty  val fooBean = "foobean"  @BeanProperty  var barBean = "barbean"  @BooleanBeanProperty  var awesome = true  def dangerFoo() = {    throw new IOException("SURPRISE!")  }  @throws(classOf[IOException])  def dangerBar() = {    throw new IOException("NO SURPRISE!")  }}

类参数

  • 默认情况下,类参数都是有效的 Java 构造函数的参数。这意味着你不能从类的外部访问。
  • 声明一个类参数为 val/var 和这段代码是相同的
class SimpleClass(acc_: String) {  val acc = acc_}

这使得它在 Java 代码中就像其他常量一样可以被访问

类常量

  • 常量(val)在 Java 中定义了一个获取方法。你可以通过方法“foo()”访问“val foo”的值

类变量

  • 变量(var)会生成一个 _$eq 方法。你可以这样调用它
foo$_eq("newfoo");

BeanProperty

你可以通过 @BeanProperty 注解 val 和 var 定义。这会按照 POJO 定义生成 getter/setter 方法。如果你想生成 isFoo 方法,使用 BooleanBeanProperty 注解。丑陋的 foo$_eq 将变为

setFoo("newfoo");getFoo();

异常

Scala 没有像 Java 那样有受检异常(checked exception)。需不需要受检异常是一个我们不会进入的哲学辩论,不过当你需要在Java中捕获它时就 很重要 了。dangerFoo 和 dangerBar 将演示这一点。在 Java 中不能这样做

        // exception erasure!        try {            s.dangerFoo();        } catch (IOException e) {            // UGLY        }

Java 会抱怨说 s.dangerFoo 从未抛出过 IOException 异常。我们可以通过捕获 Throwable 来跳过,但是这样不好。

相反,作为一个良好的 Scala 公民,可以很体面地像在 dangerBar 中那样使用 throws 注解。这使我们能够继续在 Java 中使用受检异常。

进一步阅读

支持 Java 互操作的 Scala 注解的完整列表在这里 http://www.scala-lang.org/node/106

特质

你如何获得一个接口+实现?让我们看一个简单的特质定义

trait MyTrait {  def traitName:String  def upperTraitName = traitName.toUpperCase}

这个特质有一个抽象方法(traitName)和一个实现的方法(upperTraitName)。Scala 为我们生成了什么呢?一个名为 MyTrait 的的接口,和一个名为 MyTrait$class 的实现类。

MyTrait 和你期望的一样

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTraitCompiled from "Scalaisms.scala"public interface com.twitter.interop.MyTrait extends scala.ScalaObject{    public abstract java.lang.String traitName();    public abstract java.lang.String upperTraitName();}

MyTrait$class 更有趣

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTrait$classCompiled from "Scalaisms.scala"public abstract class com.twitter.interop.MyTrait$class extends java.lang.Object{    public static java.lang.String upperTraitName(com.twitter.interop.MyTrait);    public static void $init$(com.twitter.interop.MyTrait);}

MyTrait$class 只有以 MyTrait 实例为参数的静态方法。这给了我们一个如何在 Java 中来扩展一个特质的提示。

首先尝试下面的操作

package com.twitter.interop;public class JTraitImpl implements MyTrait {    private String name = null;    public JTraitImpl(String name) {        this.name = name;    }    public String traitName() {        return name;    }}

我们会得到以下错误

[info] Compiling main sources...[error] /Users/mmcbride/projects/interop/src/main/java/com/twitter/interop/JTraitImpl.java:3: com.twitter.interop.JTraitImpl is not abstract and does not override abstract method upperTraitName() in com.twitter.interop.MyTrait[error] public class JTraitImpl implements MyTrait {[error]        ^

我们 可以 自己实现。但有一个鬼鬼祟祟的方式。

package com.twitter.interop;    public String upperTraitName() {        return MyTrait$class.upperTraitName(this);    }

我们只要把调用代理到生成的 Scala 实现上就可以了。如果愿意我们也可以覆盖它。

单例对象

单例对象是 Scala 实现静态方法/单例模式的方式。在 Java 中使用它会有点奇怪。没有一个使用它们的完美风格,但在 Scala2.8 中用起来并不很糟糕

一个 Scala 单例对象会被编译成由“$”结尾的类。让我们创建一个类和一个伴生对象

class TraitImpl(name: String) extends MyTrait {  def traitName = name}object TraitImpl {  def apply = new TraitImpl("foo")  def apply(name: String) = new TraitImpl(name)}

我们可以像这样天真地在 Java 中访问

MyTrait foo = TraitImpl$.MODULE$.apply("foo");

现在你可能会问自己,这是神马玩意?这是一个正常的反应。让我们来看看 TraitImpl$ 里面实际上是什么

local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap TraitImpl$Compiled from "Scalaisms.scala"public final class com.twitter.interop.TraitImpl$ extends java.lang.Object implements scala.ScalaObject{    public static final com.twitter.interop.TraitImpl$ MODULE$;    public static {};    public com.twitter.interop.TraitImpl apply();    public com.twitter.interop.TraitImpl apply(java.lang.String);}

其实它里面没有任何静态方法。取而代之的是一个名为MODULE$的静态成员。方法实现被委托给该成员。这使得访问代码很难看,但却是可行的。

转发方法(Forwarding Methods)

在 Scala2.8 中处理单例对象变得相对容易一点。如果你有一个类与一个伴生对象,2.8 编译器会生成转发方法在伴生类中。所以,如果你用 2.8,你可以像这样调用 TraitImpl 单例对象的方法

MyTrait foo = TraitImpl.apply("foo");

闭包函数

Scala 的最重要的特点之一,就是把函数作为头等公民。让我们来定义一个类,它定义了一些以函数作为参数的方法。

class ClosureClass {  def printResult[T](f: => T) = {    println(f)  }  def printResult[T](f: String => T) = {    println(f("HI THERE"))  }}

在 Scala 中可以像这样调用

val cc = new ClosureClasscc.printResult { "HI MOM" }

在 Java 中却不那么容易,不过也并不可怕。让我们来看看 ClosureClass 实际上被编译成什么:

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap ClosureClassCompiled from "Scalaisms.scala"public class com.twitter.interop.ClosureClass extends java.lang.Object implements scala.ScalaObject{    public void printResult(scala.Function0);    public void printResult(scala.Function1);    public com.twitter.interop.ClosureClass();}

这也不是那么恐怖。“f: => T”被转义成“Function0”,“f: String => T”被转义成“Function1”。Scala 实际上从 Function0 定义到 Function22,最多支持 22 个参数。这真的应该足够了。

现在我们只需要弄清楚如何在 Java 中使用这些东东。我们可以传入 Scala 提供的 AbstractFunction0 和 AbstractFunction1,像这样

    @Test public void closureTest() {        ClosureClass c = new ClosureClass();        c.printResult(new AbstractFunction0() {                public String apply() {                    return "foo";                }            });        c.printResult(new AbstractFunction1<String, String>() {                public String apply(String arg) {                    return arg + "foo";                }            });    }

注意我们可以使用泛型参数。

Scala 第一个Scala程序

我们可以执行Scala代码,首先使用scalac命令行工具编译它。

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

注意

语句末尾的分号通常是可选的。

语句末尾的分号通常是可选的。

Scala程序处理从主方法开始,这是每个Scala程序的一个强制性部分。

主要方法未标记为静态。

主要方法是对自动实例化的单例对象的实例方法。

没有返回类型。实际上有Unit,这是类似于void,但它是由编译器推断。

我们可以通过在参数后面加一个冒号和类型来显式地指定返回类型:

def main(args: Array[String]) : Unit = { } 

Scala使用def关键字告诉编译器这是一个方法。

在Scala中没有访问级别修改器。

Scala未指定公用修饰符,因为默认访问级别为public。

打印一些数字

让我们编写一个程序,在Print1.scalafile中打印从1到10的数字:

object Main {  def main(args: Array[String]) {        for {i <- 1 to10}           println(i)   }}

我们可以在控制台中输入scala Main.scala来运行代码

程序将数字1至10分配给变量,然后执行println(i),打印数字1至10。

在Print2.scala文件中,放入

object Main {  def main(args: Array[String]) {        for {           i <- 1 to 10              j <- 1 to 10        }         println(i* j)   }}

Scala do...while 循环

Scala 循环 Scala 循环

不像 while 循环在循环头部测试循环条件, Scala 语言中,do...while 循环是在循环的尾部检查它的条件。

do...while 循环与 while 循环类似,但是 do...while 循环会确保至少执行一次循环。


语法

Scala 语言中 while 循环的语法:

do {   statement(s);} while( condition );

流程图

Scala 中的 do...while 循环

请注意,条件表达式出现在循环的尾部,所以循环中的 statement(s) 会在条件被测试之前至少执行一次。

如果条件为 true,控制流会跳转回上面的 do,然后重新执行循环中的 statement(s)。

这个过程会不断重复,直到给定条件变为 false 为止。

实例

object Test {   def main(args: Array[String]) {      // 局部变量      var a = 10;      // do 循环      do{         println( "Value of a: " + a );         a = a + 1;      }while( a < 20 )   }}

执行以上代码输出结果为:

$ scalac Test.scala$ scala Testvalue of a: 10value of a: 11value of a: 12value of a: 13value of a: 14value of a: 15value of a: 16value of a: 17value of a: 18value of a: 19

Scala 循环 Scala 循环


Scala while 循环

Scala 循环 Scala 循环

只要给定的条件为 true,Scala 语言中的 while 循环语句会重复执行循环体内的代码块。


语法

Scala 语言中 while 循环的语法:

while(condition){   statement(s);}

在这里,statement(s) 可以是一个单独的语句,也可以是几个语句组成的代码块。condition 可以是任意的表达式,当为任意非零值时都为 true。当条件为 true 时执行循环。

当条件为 false 时,程序流将继续执行紧接着循环的下一条语句。


流程图


在这里,while 循环的关键点是循环可能一次都不会执行。当条件为 false 时,会跳过循环主体,直接执行紧接着 while 循环的下一条语句。

实例

object Test {   def main(args: Array[String]) {      // 局部变量      var a = 10;      // while 循环执行      while( a < 20 ){         println( "Value of a: " + a );         a = a + 1;      }   }}

执行以上代码输出结果为:

$ scalac Test.scala$ scala Testvalue of a: 10value of a: 11value of a: 12value of a: 13value of a: 14value of a: 15value of a: 16value of a: 17value of a: 18value of a: 19

Scala 循环 Scala 循环