1 - Scala 精要

1.1 - Case Class

下面会用到的 case 类实例:

case class Person(lastname: String, firstname: String, birthYear: Int)

基本特性

case 类与普通类的最大区别在于,编译器会为 case 类添加更多的额外特性。

  • 创建一个类和它的伴生对象

  • 创建一个名为apply的工厂方法。因此可以在创建实例时省略掉new关键字:

    val p = new Person("Lacava", "Alessandro", 1976)
    val p = Person("Lacava", "Alessandro", 1976)
    
  • 为参数列表中的所有参数添加val前缀,表示这些参数作为类的不可变成员,因此你会得到所有这个成员的访问器方法,而没有修改器方法:

    val lastname = p.lastname
    p.lastname = "Brown"		// 编译失败
    
  • hashCodeequalstoString方法添加原生实现。因为==在 Scala 中代表equals,因此 case 类实例之间总是以结构的方式进行比较,即比较数据而不是比较引用

    val p_1 = Person("Brown", "John", 1969)
    val p_2 = Person("Lacava", "Alessandro", 1976)
    
    p == p_1 // false
    p == p_2 // true
    
  • 生成一个copy方法,使用现有的实例并接收一些新的字段值来创建一个新的实例:

    val p_3 = p.copy(firstname = "Michele", birthYear = 1972)
    
  • 最重要的特性,实现一个 unapply 方法。因此,case 类可以支持模式匹配。这在定义 ADT 时尤为重要。unapply方法就是一个析构器

  • 当不需要参数列表时,可以定义为一个case object

常用其他特性

  • 创建一个函数,根据提供的参数创建一个 case 类的实例:

    val personCreator: (String, String, Int) => Person = Person.apply _
    personCreator("Brown", "John", 1969)	// Person(Brown,John,1969)
    
  • 如果需要将上面的函数柯里化分多步提供参数来创建一个实例:

    val curriedPerson: String => String => Int => Person = Person.curried
    
    val lacavaBuilder: String => Int => Person = curriedPerson("Lacava")
    
    val me = lacavaBuilder("Alessandro")(1976)
    val myBrother = lacavaBuilder("Michele")(1972)
    
  • 通过一个元组来创建实例:

    val tupledPerson: ((String, String, Int)) => Person = Person.tupled
    
    val meAsTuple: (String, String, Int) = ("Lacava", "Alessandro", 1976)
    
    val meAsPersonAgain: Person = tupledPerson(meAsTuple)
    
  • 将一个实例转换成一个由其参数构造的元组的Option

    val toOptionOfTuple: Person => Option[(String, String, Int)] = Person.unapply _
    
    val x: Option[(String, String, Int)] = toOptionOfTuple(p) // Some((Lacava,Alessandro,1976))
    

curriedtupled方法通过伴生对象继承自AbstractFunctionNN是参数的数量,如果N = 1则并不会得到这两个方法。

柯里化 的方式定义 case 类

Scala Case Classes In Depth

其他内建方法

因为所有的 case 类都会扩展Product特质,因此他们会得到如下方法:

  • def productArity:Int:获得参数的数量
  • def productElement(n:Int):Any:从 0 开始,获得第 n 个参数的值
  • def productIterator: Iterator[Any]:获得由所有参数构造的迭代器
  • def productPrefix: String:获得派生类中用于toString方法的字符串,这里会返回类名

1.2 - 类型系统

什么是类型系统(type-system)?

A type system is a tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values the compute. — Benjamin Pierce

一个类型系统是一种易于理解的语法方法,根据所计算的值的类别对短语进行分类,以证明某些程序行为的缺失。

什么是类型(type)?

  • 一个类型定义了一个变量可以拥有的一组值,以及一组能够应用于这些值的函数;
  • 一组值可以被定义为:
    • 笛卡尔产品类型(Cartesian product Types),比如 case 类或元组;
    • 总和类型(Sum Types),比如 Either
  • 类型可以是抽象的和(或)多态的。

为什么需要类型?

  • Make illegal states unrepresentable. — Yaron Minsky
    • 使非法状态无法表示
  • Where static typing fits, do it every time because it has just fantastic maintenance benefits. — Simon Peyton
    • 在合适的场景总是使用静态类型,因为它具有非常好的维护优势
  • Compiler can use Type informations to optimize compiled code.
    • 编译器可以使用类型信息来优化被编译的代码

Scala 的类型系统?

  • Scala 是一种同时支持面向对象和函数式的多范式语言
  • Scala 拥有强大静态类型系统
    • 会在编译期检查类型
    • 编译器能够通过推断类型
    • 函数也可以作为类型:A => B

类型的用途?

类型可以用于定义:

  • (抽象)类
  • 对象
  • 特质

Scala 中类型相关的特性?

设计目的

本部分整理至 Martin Odersky 的访谈,原文地址:The Purpose of Scala’s Type System,中文地址:Scala类型系统的目的

可伸缩性(scalability)

Scala 可以让你不必混用多种专用语言,无论是小型程序还是大型程序,无论是通用用途还是特定应用领域,Scala 都可以胜任。这样,可以避免你在多种语言环境中传递数据。

比如你想跨越数据边界传递数据,像 JDBC,从 Java 像数据库发起一次 SQL 查询,那么发出的查询最终将会是个字符串。这就意味着如果程序中只要有小小的拼写错误,在最终运行时,就会表现为一次非法查询,从而导致一系列相关错误。整个过程中编译器和类型系统不会告诉你那里写错了。

同时,如果仅使用一种语言,则只需要面对一套环境和工具。

可扩展性(extensibility)

“可伸缩性“的维度是”从小到大“。而可扩展性表示从”从通用用途到特定需求“。你可以强化 Scala 语言,使之涵盖你特别关注的领域。

比如数字类型,不同领域有很多不同的数字类型,密码学中的大数、商务人士用的十进制大数、科学家用的复数等等。但如果在语言中加入所有这些可能的类型,则会变得非常笨重。

因此我们可以留给外部库实现,但是又想像使用内置类型的代码一样干净、优雅。为此,你需要语言提供某些扩展机制,是你能够编写用起来不像库的库。

小规模编程中的类型

小规模编程中类型会显得不那么重要。类型的价值分布在一条长长的光谱上,一端表示超级有用,一端表示极度麻烦。通常说它麻烦是因为类型定义可能会太过冗余,要求你手动指定大量类型。有用则是因为,类型能避免运行时错误,能够提供 API 签名文档,能够为代码重构提供安全底线。

Scala 的类型推断,试图尽可能减少类型的麻烦之处。这意味着你编写脚本时并不需要涉及类型,系统会自动进行推断,同时编译器内部仍然会考虑类型。因此仍然能够享受类型检查带来的便利。

单元测试与随心所欲的表达式

仍然需要单元测试来测试你的程序逻辑。但相比动态类型语言,不需要额外对类型进行琐碎的单元测试,因此 Scala 中的单元测试也会精练很多。

另一条针对类型系统的反对意见是:静态类型对表达式限制太严。比如”我想更自由的表达自己,不想要类型系统的条条框框“。我(Martin)认为这种意见不靠谱,第一,Scala 的类型系统实际上很灵活,所以通常它可以让你用非常灵活的模式排列组合,而像 Java 这种类型系统偏弱的语言则难以实现;第二,通过模式匹配,你可以通过非常灵活的方式提取类型信息,甚至根本感觉不到类型信息的损失。

鸭子类型,Java 中的缺失

只要它具备我需要的功能,那么就可以把它当真。比如我需要某种支持关闭操作的资源,我可以以这种方式声明”它必须有 close 方法“,因此我不用关心它是 File、Channel 等等。

要想在 Java 中实现的话,需要定义一个包含该方法的通用接口,然后大家都需要实现这个接口。首先为了实现这一切就会带来大量接口和样板代码。其次,如果有了即成事实之后,你才想到要提个接口,那么几乎不可能做到。如果事先就写了一个类,鉴于这些类早已存在,那么你不改代码就无法加入该接口,除非修改所有客户端代码。所以,这一切都是类型系统强加给你的限制。

而 Scala 中则能表达鸭子类型。你完全可以用 Scala 把某一类型定义为:”任何拥有 close 方法并且该方法返回 Unit 的类型“。同时还可以把类型定义与其他约束结合起来。则可以这样定义类型:任何继承自某个类型,且拥有某某方法签名的类型。还可以这样定义,任何继承自某某类,且拥有某某类型的内部类的类型。从本质上讲,你可以通过定义类型汇总有哪些东西将为你所用,来描绘类型的结构。

既存类型(existential types)

既存类型已有 20 多年历史,它表示,给定某种类型,比如 List,但其内部元素是未知类型。你只知道它是由某种特定类型元素构成的 List,然而并不知道这个特定类型具体是哪种类型。这个概念在 Scala 中可以用既存类型来表达。比如:List[T] forSome {type T}。稍微有点笨重,这也算有意为之。因为事实证明,既存类型往往不大好处理。Scala 有更好的选择,因此不那么需要既存类型,因为 Scala 的类型可以包含其他类型作为内部成员。

归根结底,Scala 只有三种情况需要既存类型。

  • Scala 需要能表示 Java 通配符的语义。
  • Scala 需要能表示 Java 的 raw 类型,因为有许多库使用类非泛型类型,会出现 raw 类型。如果有一个 Java raw 类型 java.util.List,那么它其实是位置元素类型的 List。
  • 既存类型可以把虚拟机中的实现细节反映到上层。类似 Java ,Scala 使用的泛型模型时”擦除类型“。所以在程序运行起来以后,我们就再也找不到类型参数了。之所以要进行擦除,是为了与 Java 的对象模型进行互操作。可是,如果我们需要反射,或者需要表达虚拟机的实现细节时该怎么办呢?我们需要有能力用 Scala 中的某些类型表示虚拟机的行为。

有了既存类型,即使某一类型中的某些方面尚不可知,我们仍然可以操作该类型。

比如,Scala 中的 List,我希望能够描述 head 方法的返回类型。在虚拟机级别,List 类型是 List[T] forSome {type T}。我们不知道 T 是什么。只知道 head 返回 T。既存类型理论告诉我们,该类型是”某些类型 T 中的 T“。也就相当于根类型 Object。那么我们从 head 方法中取回的就是 Object。因此 Scala 中我们要是知道更多信息,就可以直接指定具体类型而不用既存类型的限定规则。但是如果没有更多信息,我们就留着既存类型,让既存类型理论帮我们推断出返回类型。

如果 Java 采用的是”具现化“的泛型类型系统,不支持 raw 类型或通配符类型,那么 Scala 中的既存类型在意义不大,恐怕不会实现既存类型。

Java 与 Scala 中的型变

Java 的型变是定义在使用通配符的代码之处,而 Scala 则是在类型定义之处,当然 Scala 的既存类型一样支持通配符,支持使用 Java 的写法,但并不推荐这么做。

首先,什么是”类型定义之处的型变“?当你定义某个类的类型参数时,例如 List[T],会有一个问题,如果你给一个苹果列表,那这个列表算不算水果列表呢?显然算,只要苹果是水果的子类型,List[Appke]就是List[Fruit]的子类型,这称为协变。

但某些情况下种种关系不成立,比如我有一个变量,只能用来保存 Apple,那么这个变量就是对类型 Apple 的引用。这个变量并不能当做 Fruit 类型的引用,因为我不能把任意 Fruit 赋值给这个变量,它只能是 Apple。因此可以发现,上述子类型关系在有些情况下适用,有些则不适用。

Scala 中的解决方案是给类型参数添加一个标志。如果 List 中的 T 支持协变,则写作List[+T]。这将意味着任意 List 之间的关系都可以随着其 T 的关系而协变。要想支持协变,必须遵守协变的附加条件。举例来说,只有 List 内容不可变式,List 才能支持协变,否则将会遇到刚才引用变量中类似的问题,而导致无法协变。

Scala 中的机制是这样的:程序员可以声明”我觉得 List 应用支持协变“,即,要求 List 必须遵守子类型关系,然后给 List 的类型参数加上加号标志,仅需一次,List 的所有用户都可以使用。然后编译器会去找出 List 内的所有定义实际上是否兼容协变,确保 List 中不存在导致冲突的成员签名。

相比之下,以 Java 的方式使用通配符,这就意味着库的作者对协变爱莫能助,只能草草定义成 List<T>了事。但接下来如果用户需要协变 List,却不能写作List<Fruit>,而是List<? extends Fruit>。通配符就是这样用的。问题在于这是用户代码啊,用户总不能人人都像设计库的人那么专业吧。此外,这些标注之间,只要有一处不匹配,就会导致类型错误。

Scala 中可以在库中处理型变,让用户感觉不到型变存在,不必手动处理型变。

抽象类型成员

抽象类型与泛型参数看似类似,但拥有更多好处。对于抽象,业界有两套不同的机制:参数化和抽象类型成员。Java 也一样支持两套抽象,只不过 Java 的两套抽象取决于对什么进行抽象。Java 支持抽象方法,但不支持把方法作为参数;Java 不支持抽象字段,但支持把值作为参数;Java 不支持抽象类型成员,但支持把类型作为参数。所以在 Java 中三者均可抽象,但是之间的原理有所区别。

在 Scala 中试图把这些抽象支持的更完备、更正交,即对上述三类成员都采用相同的构造原理。因此,你可以使用抽象字段,也可以使用值参数;可以把方法(函数)作为参数,也可以声明抽象方法;即可以指定类型参数也可以声明抽象类型。至少在原则上,我们可以用同一种面向对象抽象成员的形式,表达全部三类参数。

抽象类型能够更好的处理先前谈到的协变问题。比如,一个 Ainmal 类,其中一个 eat 方法。问题是,如果从 Animal 派生一个类,比如 Cow,那么就能吃某一种食物,比如 Grass。Cow 不可以吃 Fish 之类的其他食物。你希望有办法可以声明 Cow 拥有一个 eat 方法,且该方法只能吃 Grass。实际上,这个需求在 Java 中实现不了,因为你一定会构造出有矛盾的情形,类似之前把 Fruit 复制给 Apple 一样。

Scala 中则可以增加一个类型成员,比如声明一个 SuitableFood 类型,它不定义具体是什么,这就是抽象类型,直接让 Animal 的 eat 方法吃下 SuitableFood 即可。然后在 Cow 中指定其为 Grass 即可。

所以,抽象类型提供了一种机制:先在父类中声明未知类型,稍后再在子类中填上某种已知类型。

你可能会说用参数也可以实现同样功能。确实可以。你可以给 Animal 增加参数,表示能吃的食物。但实践中,当你需要支持许多不同的功能是就会导致参数爆炸。而且通常更要命的问题是参数的边界。

相关概念

类型推断

Scala 拥有类型推断,这表示我们可以省略掉类型注解。

trait Thing
def getThing = new Thing { }

// without Type Ascription, the type is infered to be `Thing`
val infered = getThing

// with Type Ascription
val thing: Thing = getThing

在这些情况下都是可以编译通过的。所以都有哪些地方不能省略类型注解呢:

  • 参数
  • 公共方法返回值
  • 递归或重载方法
  • 需要包含类型签名来加快编译速度
  • 需要类型签名来使代码更易读

统一的类型系统 — Any、AnyRef、AnyVal

我们之所以称 Scala 的类型系统是统一的,是因为它拥有一个顶层类型 Any。这与 Java 不同,它有一些原始类型(int/long/float/double/byte/char/short/boolean)并非扩展自 Java 中看起来像是顶层类型的 Object。

Any 作为顶层类型,下面有 AnyVal 和 AnyRef 两种子类型。

AnyRef 相当于 Java 中 对象的世界,对应于 Object,作为所有对象的超类。而 AnyVal 表示了 Java 中的 值世界,像 int 或其他 JVM 原始类型。

因此我们可以定义一个接收 Any 类型的方法,同时能够处理 Int 或 String。这对类型系统来说是透明的虽然在虚拟机层 Int 实例会被打包成一个对象。通过查看字节码能够发现:

Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
  • Conventional types
  • Value type classes
  • Nonnullable type
  • Monad types
  • Trait types
  • Singlton object types
  • Compound types
  • Functional Types
  • Case classes
  • Path-dependent types
  • Anonymous types
  • Self types
  • Type aliases
  • Package object
  • Generic types
    • 类型擦除问题
  • Pattern Match
  • Bounded generic types
  • Type Variance
  • Higher kinded types
  • Abstract types
  • Existential types
  • Implicit types
  • View bounded types
  • Structural types
  • Dotty?

TODO: http://ktoso.github.io/scala-types-of-types/#type-of-an-code-object-code

社区评论

Scala 的语言设计有哪些缺陷?

Scala 中提供了相当多的便利,但这与类型系统的强大无关,仅仅是因为与类型有关。而类型系统的强大之处在于其强大的表现力,能够表示在其它语言中无法表示的事情。

事实上,类型推断是类型系统效率的直接影响因素—它并非那么强大,有些语言拥有完整的类型推断,比如 Haskell。

Scala 中采用局部的基于流的推导,而不是全局式的 HM 推导,导致有些情况不如 Haskell 的推导那么强大。

维基百科:类型推断

为什么有的语言不支持类型推断?

图灵完备:http://www.tuicool.com/articles/rqUjAb

1.3 - 函数式-基础

方法

创建函数最简单的方式就是作为对象的成员,即方法

本地函数

函数式编程风格的一个主要原则就是:程序应该被解构为若干小的函数块,每块实现一个完备的任务,每块都很小。

但是大量的小块函数会污染程序的命名空间,这时可以私有函数,或者另一种方式,本地函数,即嵌套函数:

import scala.io.Source
object LongLines {
  def processFile(filename:String, width:Int){
    def processLines(line:String){    // 打印超出宽度的行, 引用外层的 width
      if (line.length > width) println(filename + ": " + line)
  }
  val source = Source.fromFile(filename)
  for (line <- source.getLines)
    processLine(line)
  }
}

头等函数

Scala 的函数是头等函数(first-class-function),不仅可以定义或调用函数,还可以把函数写成匿名字面量,当做值来传递。函数字面量被编译进类,并在运行期实例化为函数值。因此函数字面量与值的区别在于:函数字面量存在于源代码,而函数值作为对象存在于运行期。类似于类(源代码)和对象(运行期)之间的关系。

(x: Int) => x + 1

符号=>指:把左边的东西转换为右边的东西。

函数值是对象,因此可以赋给变量。同时也是函数,可以按照通常的函数调用方式,使用一个括号来调用:

var increase = (x: Int) => x + 1
increase(2)   // 3

函数可以由多条语句构成,由大括号包围,组成一个代码块。当函数执行时,所有的语句都会执行,最后一行作为返回值。

函数字面量的短格式

可以省略函数字面量中的参数类型来进行简化:

someNumber.filter((x) => x > 0)

因为使用这个函数来过滤整数列表someNumber,因此 Scala 可以推断出 x 肯定为整数。这种方式称为目标类型化(target typing)

某些参数的类型是推断的,可以省略参数的括号:

someNumber.filter(x => x > 0)

占位符语法

只要每个参数在函数字面量中仅出现一次,可以使用占位符_来代替该参数名:

someNumber.filter(_ > 0)

但是有时候把下划线当做参数的占位符,编译器可能无法推断缺失的参数类型:

val f = _ + _

因为上面的filter调用是一个整数列表,编译器可以推断,但这里,编译器无从推断,会编译失败。这时,我们可以为参数提供类型:

val f = (_:Int) + (_:Int)

这里需要注意的是,这里的两个占位符表示的是需要两个参数,而不是一个参数使用两次。

_ + _   (x, y) => x + y
_ * 2     x => x * 2
_.head    xs => xs.head
_ drop _  (xs, n) => xs.drop(n)

在参数较少时,使用这种方式可以使程序更清晰,但是当参数增多,比如foo(_, g(List(_ + 1), _)),则会让程序变得难以理解。

偏应用函数

或称为部分应用函数(Partially applied functions)。

可以使用占位符代替一个参数,也可以代替整个参数列表。比如println(_),或者println _

这中下划线方式的使用,实际上定义的是一个部分应用函数。它是一种表达式,不需要提供函数需要的所有参数,可以只提供部分,或者不提供。

比如一个普通的sum函数:

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

当调用函数时,传入任何需要的参数,实际是把函数应用到参数上。

如果通过sum来创建一个部分应用表达式,不需要提供所需要的参数,只需要在sum之后添加一个下划线,使用空格隔开,然后把得到的函数存入变量:

val a = sum _
a : (Int, Int, Int) => Int = <function3>  // function3 类型的实例
a(1,3,4)  // 6

可以看到,a为一个函数。变量a指向一个函数值对象。这个函数值是由 Scala 编译器依照部分应用函数表达式sum _自动产生的类的一个实例。编译器产生的类有一个apply方法,该方法接收 3 个参数。在通过a进行a(1,2,3)调用时,会被翻译成对函数值的apply方法的调用,即apply(1,2,3)

可以看到function3的源码定义:

/** A function of 3 parameters.
 *
 */
trait Function3[-T1, -T2, -T3, +R] extends AnyRef { self =>
  /** Apply the body of this function to the arguments.
   *  @return   the result of function application.
   */
  def apply(v1: T1, v2: T2, v3: T3): R
  ...
}

偏应用函数实际上是指该函数未被应用到所有的参数,sum的例子是没有应用到任何参数,也可以只应用到部分参数。

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

这时会发现b为一个function1类型的实例,它接收一个参数。

同时,如果定义的是一个省略所有参数的偏应用函数,比如这里的sum,二者在代码的某个位置需要一个相同签名的函数,这时可以省略掉占位符进行简化:

someNumbers.foreach(println _)
someNumbers.foreach(println)

需要注意的是,只有在需要一个函数的地方才可以省略占位符。比如这里,编译器知道foreach需要一个函数。否则会编译错误。比如:val s = sum,必须是:val s = sum _

闭包

重复参数

可以指定一个函数的最后一个参数是重复的,然后就可以传入可变长度的参数列表:

def echo(args:String*) = 
  for(arg <- args) println(arg)

echo("a")
echo("a", "b")
val seq = Seq("a","b","c")
echo(seq:_*)

尾递归

高阶函数

函数值作为参数的函数称为高阶函数

减少代码重复

比如我们要实现一个 根据文件名查找文件的程序,首先是文件名以指定字符串结尾的文件名

object FileMatcher { 
  private def filesHere = (new java.io.File(".")).listFiles
  def filesEnding(query: String) = 
    for (file <- filesHere; if file.getName.endsWith(query)) yield file
}

filesHere这里作为一个工具函数来获取所有文件名列表。

现在如果需要的不只是以指定字符串结尾的方式查找,只要是文件名中包含指定字符串,或者以指定的方式能够匹配指定的字符串,因此我们需要查找的方式是这样的:

def filesMatching(query: String, method ) = 
  for (file <- filesHere; if file.getName.method (query)) yield file

method表示匹配方式,但是 Scala 中不支持传入函数名的方式,因此我们可以传递一个函数值:

def filesMatching(query: String, matcher: (String, String) => Boolean) = {
  for (file <- filesHere; if matcher(file.getName, query)) yield file
}

mathcer接收两个字符串,一个是文件名,一个是需要匹配的字符串,返回一个布尔值表示该文件名与指定的字符串是否匹配。因此,我们可以实现我们的不同匹配方式,而对filesMatching函数进行复用:

def filesEnding(query: String) = filesMatching(query, _.endsWith(_))
def filesContaining(query: String) = filesMatching(query, _.contains(_))
def filesRegex(query: String) = filesMatching(query, _.matches(_))

类似_.endsWith(_)的部分使用了占位符语法,前面已经提到,函数的参数只被使用一次,且参数顺序与使用顺序一致,则可以使用占位符语法简化。其完整的写法实际是:

(fileName: String, query: String) => fileName.endsWith(query)

简化后会发现,函数filesMatching的参数中,query已经不再需要了,因为该参数只用于matcher函数,并且已经通过匹配方法传入,因此再次进行简化:

object FileMatcher{
  private def filesHere = (new java.io.File(".")).listFiles
  private def filesMatching(matcher: String => Boolean) = 
    for (file <- filesHere; if matcher(file.getName)) yield file
    
  def filesEnding(query:String) = filesMatching(_.endsWith(query))
  def filesContains(query:String) = filesMatching(_.contains(query))
  def filesRegex(query:String) = filesMatching(_.matches(query))
}

简化客户端代码

集合 API 提供一些列常用的方法,其中应用了大量的高阶函数,通过将高阶函数作为参数来定义 API,从而使客户端代码更加易于使用。

比如常用的existsfind, 在scala.collection.TraversableLike包中:

def exists(p: A => Boolean): Boolean = {
  var result = false
    breakable {
      for (x <- this)
        if (p(x)) { result = true; break }
    }
    result
}

def find(p: A => Boolean): Option[A] = {
    var result: Option[A] = None
    breakable {
      for (x <- this)
        if (p(x)) { result = Some(x); break }
    }
    result
}

参数p是一个A => Boolean类型的函数,它接收一个参数并放回一个布尔值,用于判断集合中的元素是否满足条件,比如在应用时:

List(1,2,3,-1).exists(_ < 0)

这里同样应用了占位符语法,使整个操作更加简便。

柯里化

柯里化是将函数应用到多个参数列表上

def plainOldSum(x: Int, y: Int) = x + y   // 普通函数
plainOldSum(1, 2)
def curriedSum(x: Int)(y: Int) = x + y    // 柯里化函数
curriedSum(1)(2)

在调用柯里化函数时,如果没有一次给出所有的参数列表,比如上面的curriedSum,第一次只提供一个参数列表进行调用curriedSum(1),这时会返回一个函数值(y: Int) => x + y,调用该函数值并提供另一个参数列表,即(y: Int),得出最后的求和值。

其过程类似于以下面的方式定义函数:

def first(x: Int) = (y: Int) => x + y
val second = first(1) // Int => Int = <function1>
second(2)       // 3

柯里化函数也可以以下面的方式,使用一个占位符语法,来获取中间的函数值,即上面的second函数值:

val onePlus = curriedSum(1)_    // Int => Int = <function1>

调用时提供的占位符_代表第二个参数列表。

同样,可以定义更多个参数列表的柯里化函数,比如:

def multiSum(x: Int)(y: Int)(z: Int) = x + y + z
val second = multiSum(1)_   // Int => (Int => Int) = <function1>
val third = second(2)     // Int => Int = <function1>
third(3)            // 6

自定义控制结构

因为函数可以作为参数值来传递,因此可以使用该特性来定义自己的控制结构,只需要定义接收函数值的方法即可。

比如:

def twice(op: Double => Double, x: Double) = op(op(x))
twice(_ + 1, 5)   // 7

一旦发现代码中有重复的控制模式,就可以通过定义一个函数的方式来代替。

比如我们需要一个控制结构,操作一个文件并最终将其关闭,这就是一个控制模式,而操作部分是主要的处理:

def withPrintWriter(file: File, op: PrintWriter => Unit) = { 
  val writer = new PrintWriter(file) 
  try {
    op(writer) 
  } finally {
    writer.close() 
  }
}

withPrintWriter( 
  new File("date.txt"), 
  writer => writer.println(new java.util.Date) 
)

我们可以利用这种模式来实现不同的控制,比如将withPrintWriter实现为一个日志打印过程或缓存更新过程,而操作部分是一次数据库查询。

这种模式成为借贷模式。这个例子中,控制抽象函数withPrintWriter打开一个资源,即writer,借贷给op函数,当op函数不再需要时又将其关闭。

但是这种模式的使用方式扛起来并不像是一个控制结构,它就是一个函数调用,然而可以通过使用大括号的方式使其更像是真正的控制结构。

但是在使用大括号进行函数调用时只能接收一个参数,比如:

println { "Hello, world!" }
"Hello, world!".substring { 7, 9 }    // error,多个参数必须使用小括号包围参数列表

因此,我们可以将上面的withPrintWriter函数改写为柯里化的方式,每次只接收一个函数,来满足只有一个参数才能使用大括号的要求:

def withPrintWriter(file: File)(op: PrintWriter => Unit) = { 
  val writer = new PrintWriter(file) 
  try {
    op(writer) 
  } finally {
    writer.close() 
  }
}

然后,下面的调用使withPrintWriter 看起来更像一个控制结构:

withPrintWriter(file) { writer =>
  writer.println(new java.util.Date) 
}

传名参数

但是上面的例子与内建的控制接口,比如ifwhile并不相似,因为在大括号中需要传入一个参数,这可以通过传名参数实现。

比如定义一个断言函数:

var assertionsEnabled = true

def myAssert(predicate: () => Boolean) = 
  if (assertionsEnabled && !predicate()) throw new AssertionError

然后以下面的方式调用:

myAssert(() => 5 > 3)

或许你更希望使用myAssert(5 > 3)的方式来调用,在创建传名参数时可以使用=>来代替完整的() =>,括号部分实际是函数的参数列表,只不过该函数不接受任何参数,因此省略。

def byNameAssert(predicate: => Boolean) = 
  if (assertionsEnabled && !predicate) throw new AssertionError

myAssert(5 > 3)

一个传名类型,即其参数列表为空(),并进行省略,这样的用法仅仅在作为参数时可行。

但是根据上面的定义方式,其实与def byNameAssert(predicate:Boolean)没有什么差别了。

真正的差别在于,如果是传入的值,这个值必须在调用byNameAssert之前完成计算,如果是传入的函数,则会在调用之后进行计算。

1.4 - 函数式-用例

匿名函数

匿名函数,或函数字面值,可以将它传递给一个方法,或者赋值给一个变量。

比如过滤集合中的偶数,可以将一个匿名函数传递给集合的 filter 方法:

val x = List.range(1, 10)
val evens = x.filter((i: Int) => i % 2 == 0)

这里的匿名函数就是(i: Int) => i % 2 == 0。符号=>可以认为是一个转换器,将集合中的每个整数 i ,通过符号右边的表达式i % 2 == 0转换为一个布尔值,filter 通过这个布尔值判断这个 i 的去留。

因为 Scala 可以推断类型,因此可以省略掉类型:

val evens = x.filter(i => i % 2 == 0)

又因为当一个参数在函数中只出现一次时使用通配符 _ 表示,因此可以省略为:

val evens = x.filter(_ % 2 == 0)

在更为常见的场景中:

x.foreach((i:Int) => println(i))

简略为:

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

进一步简略:

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

再次简略:

x.foreach(println(_))

最终,如果由一条语句组成的函数字面量仅接受一个参数,并不需要明确的名字和指定参数,因此最终可以简略为:

x.foreach(println)

像变量一样使用函数

可以向传递一个变量一样传递函数,就像是一个 String、Int 一样。

比如需要将一个数字转换为它的2倍,向上一节一样,将=>当做一个转换器,这时是将一个数字转换为它的2倍,即:

(i: Int) => { i * 2 }

然后将这个函数字面量赋值给一个变量:

val double = (i: Int) => { i * 2 }

变量 double 是一个实例,就像 String、Int 的实例一样,但是它是一个函数的实例,作为一个函数值,然后可以像方法调用一样使用它:

double(2) 	// 4

或者将它传递给一个函数或方法:

list.map(double)

如何声明一个函数

已经见过的方式:

val f = (i: Int) => { i % 2 == 0 }

Scala 编译器能够推断出该方法返回一个布尔值,因此这里省略了函数的返回值。一个包含完整类型的声明会是这样:

val f: (Int) => Boolean = i => { i % 2 == 0 }
val f: Int => Boolean = i => { i % 2 == 0 }
val f: Int => Boolean = i => i % 2 == 0
val f: Int => Boolean = _ % 2 == 0

将函数作为方法的参数

这种需求的处理过程:

  1. 定义方法,定义想要接收并作为参数的函数签名
  2. 定义一个或多个对应该签名的函数
  3. 将需要的函数传入方法
def executeFunction(callback:() => Unit) {
  callback() 
}

签名callback:() => Unit表示该函数不需要参数并返回一个空值。比如:

val sayHello = () => { println("Hello") }
executeFunction(sayHello)

这里,方法中定义的函数名并没有实际意义,仅作为方法的一个参数名,就像一个 Int 常被命名为字母 i 一样,这里可以通用定义为任何形式:

def executeFunction(f:() => Unit) {
  f() 
}

同时,传入的函数必须与签名完全一致,签名通用的语法为:

methodParameterName: (functionParameterType_s) => functionReturnType

更为复杂的函数

函数参数的签名为:接收一个整形数字作为参数并返回一个空值:

def exec(callback: Int => Unit) {
  callback(1) 	// 调用传入的函数
}
val plusOne = (i: Int) => { println(i+1) }
exec(plusOne)

或者接收更多参数的函数:

executeFunction(f:(Int, Int) => Boolean)

或者返回一个集合类型:

exec(f:(String, Int, Double) => Seq[String])

或者接收函数参数并同时接受其他类型的参数:

def executeAndPrint(f:(Int, Int) => Int, x: Int, y: Int) { 
  val result = f(x, y) 
  println(result) 
}

使用闭包

如果需要将函数作为一个参数传递,同时又需要使函数在其声明的作用域引用以存在的变量。比如下面这个例:

class Foo {
  def exec(f:String => Unit, name:String){ f(name) }
}

object ClosureExample extends App{
  var hello = "hello"
  def sayHello(name:String) = { println(s"$hello, $name") }	// 引入闭包变量 hello
  
  val foo = new otherscope.Foo
  foo.exec(sayHello, "Al")
  
  hello = "Hola"			// 修改本地变量 hello
  foo.exec(sayHello, "Lorenzo")
}

Hello, Al
Hola, Lorenzo

在这个例子中,函数 sayHello 在定义时除了声明了一个正式参数 name,同时引用了当前作用域的变量 hello。将 函数 sayHello 作为一个参数传入 exec 方法后,再修改本地变量 hello 的值,sayHello中仍然能够引用到改变后的 hello 的值。这里,Scala 创建了一个闭包。

这里为了简单只只是将 sayHello 传递给了 exec 方法,同样可以将其传递到很远的位置,即多层传递。但是变量并不在 Foo 的作用域或方法 exec 的作用域中,比如在 Foo 中或 exec 中再单独打印 hello 都不会编译通过。

闭包的三个要素:

  • 一段代码块可以像值一样传递
  • 任何拥有这段代码块的人都可以在任何时间根据需要执行它
  • 这段代码块可以在创建它的上下文中引用变量

创建闭包

var votingAge = 18
val isOfVotingAge = (age: Int) => age >= votingAge

现在可以把函数 isOfVotingAge 传递给任意作用域中的函数、方法、对象,同时 votingAge 是可变的,改变它的值同时会引起 isOfVotingAge 函数中对他引用的值的改变。

使用其他数据结构闭包

val fruits = mutable.ArrayBuffer("apple")
val addToBasket = (s: String) => { 
  fruits += s 
  println(fruits.mkString(", ")) 
}

这时,将 addToBasket 函数传递到其他作用域并执行时,都能够修改 fruits 的值。

使用偏应用函数

可以定义一个需要多个参数的函数,提供部分参数并返回一个偏函数,它会携带已获得的参数,最终传递给他剩余需要的参数以完成执行:

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

它本身需要三个参数,当只提供两个参数时,它会返回一个偏函数:

val sum = (a: Int, b: Int, c: Int) => a + b + c
// sum: (Int, Int, Int) => Int = <function3>

val f = sum(1, 2, _: Int)
// f: Int => Int = <function1>

最后给他提供一个参数,完成整个计算:

f(3)
// 6

创建返回函数的函数

可以定义一个返回函数的函数,将它传递给另一个函数,最终提供需要的参数并调用。

这是一个匿名函数:

(s: String) => { prefix + " " + s }

定义一个函数来生成这个函数:

def saySomething(prefix: String) = (s: String) => {
  prefix + " " + s 
}

可以将它赋值给一个变量并通过这个拥有函数值的变量来调用函数:

val sayHello = saySomething("Hello")
sayHello("Alex")

或者更复杂的,根据输入值的不同从而返回不同的函数:

def greeting(language: String) = (name: String) => { 
  language match { 
    case "english" => "Hello, " + name 
    case "spanish" => "Buenos dias, " + name 
  } 
}

创建偏函数

可以创建一个函数,只对所有可能的输入值的一个子集有效(称为偏函数的原因),或者一组这样的函数,最后通过组合来完成需要的功能。

定义一个偏函数:

val divide = new PartialFunction[Int, Int] { 
  def apply(x: Int) = 42 / x 
  def isDefinedAt(x: Int) = x != 0 	// 仅对部分不等于 0 的整数有效
}

divide.isDefinedAt(1)  					// true
if (divide.isDefinedAt(1)) divide(1)	// 42
divide.isDefinedAt(0)					// false

或者更为常用的模式:

val divide2: PartialFunction[Int, Int] = {
  case d: Int if d != 0 => 42 / d 
}

意思就是,它只能接受 Int 类型参数的一部分(d 不等于 0)进行处理并返回一个 Int 值。

使用 case 的方式仍然能够更第一种方式一样判断它是否能够接受一个值:

divide2.isDefinedAt(0)
divide2.isDefinedAt(1)

同时可以使用 orElse 将多个偏函数组合,andThen 则是将多个偏函数进行链接

1.5 - Future-基础

介绍:Java 与 Scala 中的并发

Java 通过内存共享和锁来提供并发支持。Scala 中通过不可变状态的转换来实现:Future。虽然 Java 中也提供了 Future,但与 Scala 中的不同。

二者都是通过异步计算来表示结果,但是 Java 中需要使用阻塞的get方法来访问结果,同时可以在调用get之前使用isDone来检查结果是否完成来避免阻塞,但是仍然需要等待结果完成以支持后续使用该结果的计算。

在 Scala 中,无论Future是否完成,都可以对他指定转换过程。每一个转换过程的结果都是一个新的Future,这个新的Future表示通过函数对原始Future转换后得到的结果。计算执行的线程通过一个隐式的*execution context(执行上下文)*来决定。以不可变状态串行转换的方式来描述异步计算,避免共享内存和锁带来的额外开销。

锁机制的弊端

Java 平台中,每个对象都与一个逻辑监视器关联,以控制多线程对数据的访问。使用这种模式时需要指定哪些数据会被多线程共享并将被访问的、控制访问的和共享数据的代码段都标记为synchronized。Java 运行时使用的机制来确保同一时间只有一个线程能够进入被锁保护的代码段。以此协调你能够通过多线程来访问数据。

为了兼容性,Scala 提供了 Java 的并发原语。可以在 Scala 中调用方法wait/notify/notifyAll,并且意义与 Java 一致。但是并不提供关键字synchronized,但是预定义了一个方法:

var counter = 0
synchronized {
  // 这里同时只能有一个线程
  counter = counter + 1
}

但是这种模式难于编写可靠的多线程应用。死锁、竟态…

使用 Try 处理异步中的异常

当你调用一个 Scala 方法时,它会在你等待返回结果时执行一个计算,如果结果是一个Future,它表示另一个异步化执行的计算,通常会被一个完全不同的线程执行。在Future上执行的操作都需要一个excution context来提供异步执行的策略,通常可以使用由 Scala 自身提供的全局执行上下文,在 JVM 上,它使用一个线程池

引入全局执行上下文:

import scala.concurrent.ExecutionContext.Implicits.global
val future = Future { Thread.sleep(10000); 21 + 21 }

当一个Future未完成时,可以调用两个方法:

future.isComplated		// false
future.value			// Option[scala.util.Try[Int]] = None

完成后:

future.isComplated		// true
future.value			// Option[scala.util.Try[Int]] = Some(Success(42))

value方法返回的Option包含一个Try,成功时包含一个类型为 T 的值,失败时包含一个异常,java.lang.Throwable的实例。

Try支持在尝试异步计算前进行同步计算,同时支持一个可能包含异常的计算。

同步计算时可以使用try/catch来确保新城调用方法并捕捉、处理方法抛出的异常。但是异步计算中,发起计算的线程常会移动到其他任务上,然后当计算中抛出异常时,原始的线程不再能通过catch子句来处理异常。因此使用Future进行异步操作时使用Try来处理可能的失败并生成一个值,而不是直接抛出异常。

scala> val fut = Future { Thread.sleep(10000); 21 / 0 } 
fut: scala.concurrent.Future[Int] = ...

scala> fut.value 
res4: Option[scala.util.Try[Int]] = None

// 10s later
scala> fut.value 
res5: Option[scala.util.Try[Int]] = Some(Failure(java.lang.ArithmeticException: / by zero))

Try的定义:

object Try {
  /** 通过传名参数构造一个 Try。
   * 捕获所有 non-fatal 错误并返回一个 `Failure` 对象。
   */
  def apply[T](r: => T): Try[T] =
    try Success(r) catch {				// 常规的 try、catch 调用
      case NonFatal(e) => Failure(e)
    }
}
sealed abstract class Try[+T]
final case class Failure[+T](exception: Throwable) extends Try[T]
final case class Success[+T](value: T) extends Try[T]

Future 操作

map

将传递给map方法的函数作用到原始Future的结果并生成一个新的Future

val result = fut.map(x => x + 1)

原始Futuremap转换可能在两个不同的线程上执行。

for

因为Future声明了一个flatMap方法,因此可以使用for表达式来转换。

val fut1 = Future { Thread.sleep(10000); 21 + 21 }	// Future[Int]
val fut2 = Future { Thread.sleep(10000); 23 + 23 }	// Future[Int]
for { x <- fut1; y <- fut2 } yield x + y			// Future[Int]

因为for表达式是对转换的串行化,如果没有在for之前创建Future并不能达到并行的目的。

for { 
	x <- Future { Thread.sleep(10000); 21 + 21 } 
	y <- Future { Thread.sleep(10000); 23 + 23 } 
} yield x + y		// 需要最少 20s 的时间完成计算

for { x <- fut1; y <- fut2 } yield x + y实际会被转化为fut1.flatMap(x => fut2.map(y => x + y))

flatMap的定义:将一个函数作用到Future成功时的结果并生成一个新的Future,如果原Future失败,新的Future将会包含同样的异常。

创建 Future

上面的例子是通过Futureapply方法来创建:

def apply[T](body: =>T)(implicit @deprecatedName('execctx) executor: ExecutionContext): Future[T] = impl.Future(body)

body是需要执行的异步计算。

创建一个成功Future

Future.successful { 21 + 21 }
// def successful[T](result: T): Future[T] = Promise.successful(result).future
// result 为 Future 的结果

创建一个失败Future

Future.failed(new Exception("bummer!"))
// def failed[T](exception: Throwable): Future[T] = Promise.failed(exception).future
// exception 为指定的异常

通过Try创建一个已完成的Future

import scala.util.{Success,Failure}
Future.fromTry(Success { 21 + 21 })
Future.fromTry(Failure(new Exception("bummer!")))
// def fromTry[T](result: Try[T]): Future[T] = Promise.fromTry(result).future

常用的方式是通过Promise来创建,得到一个被这个Promise控制的Future,当这个Promise完成时对应的Future才会完成:

val pro = Promise[Int]			// Promise[Int]
val fut = pro.future			// Future[Int]
fut.value						// None
pro.success(42)					// 或者 pro.failure(exception)/pro.complete(result: Try[T])
fut.value						// Try[Int]] = Some(Success(42))

或者调用completeWith方法并传入一个新的Future,新的Future一旦完成则用值赋予给这个Priomise

filter & collect

filter用户验证Future的值,如果满足则保留这个值,如果不满足则会抛出一个NoSuchElementException异常:

val fut = Future { 42 }
val valid = fut.filter(res => res > 0)
valid.value		// Some(Success(42))
val invalid = fut.filter(res => res < 0)
invalid.value	// Some(Failure(java.util.NoSuchElementException: Future.filter predicate is not satisfied))

同时提供了一个withFilter方法,因此可以在for表达式中执行相同的操作:

val valid = for (res <- fut if res > 0) yield res
val invalid = for (res <- fut if res < 0) yield res

collect方法对Future的值进行验证并通过一个操作将其转换。如果传递给collect偏函数符合Future的值,该Future会返回经过偏函数转换后的值,否则会抛出NoSuchElementException异常:

val valid = fut collect { case res if res > 0 => res + 46 }		// Some(Success(88))
val invalid = fut collect { case res if res < 0 => res + 46 }	// NoSuchElementException

错误处理:failed、fallBackTo、recover、recoverWith

failed

failed方法将一个任何类型的、错误的Future转换为一个成功的Future[Throwable],这个Throwable即引起错误的异常。

val failure = Future { 42 / 0 }
failure.value			// Some(Failure(java.lang.ArithmeticException: / by zero))
val expectedFailure = failure.failed
expectedFailure.value	// Some(Success(java.lang.ArithmeticException: / by zero))

如果调用failed方法的Future最终是成功的,而调用failed方法返回的Future会以一个NoSuchElementException异常失败。因此,只有当你需要Future失败时,调用failed方法才是适当的:

val success = Future { 42 / 1 }
success.value			// Some(Success(42)), 原本是一个成功的 Future
val unexpectedSuccess = success.failed
unexpectedSuccess.value	// NoSuchElementException, 称为一个失败的 Future

fallBackTo

fallBackTo方法用于提供一个可替换的Future,以便调用该方法的Future失败时作为备用。

val fallback = failure.fallbackTo(success)
fallback.value

如果调用fallBackTo方法的原始Future执行失败,传递给fallBackTo的错误本质上会被忽略。但是如果调用fallBackTo提供的Future也失败了,则会返回最初的错误,即原始Future中的错误:

val failedFallback = failure.fallbackTo( 
	Future { val res = 42; require(res < 0); res } // 这里实际是一个 require 异常
)
failedFallback.value	// Some(Failure(java.lang.ArithmeticException: / by zero)),仍然返回了原始 Future 中的除零异常

recover

recover允许将一个失败的Future转换为一个成功的Future,或者原始Future成功时则不作处理。

val recovered = failedFallback recover { case ex: ArithmeticException => -1 }
recovered.value		// Some(Success(-1)), 捕捉异常并设置成功值,返回新的 Future

如果原始Future成功,recover部分会以相同的值完成:

val unrecovered = fallback recover { case ex: ArithmeticException => -1 }
unrecovered.value	// Some(Success(42))

同时,如果传递给recover的偏函数并不包含原始Future的错误类型,新的Future仍然会以原始Future中的失败完成:

val alsoUnrecovered = failedFallback recover { case ex: IllegalArgumentException => -2 }
alsoUnrecovered.value	// Some(Failure(java.lang.ArithmeticException: / by zero))

recoverWith

recoverWithrecover类似,但是使用的是一个Future值。

val alsoRecovered = failedFallback recoverWith { 
	case ex: ArithmeticException => Future { 42 + 46 } 	// 这是一个 Future
}

其他方面的处理则于recover一致。

transform:对可能性的映射

transfor接收两个转换Future的函数:一个处理原始Future成功的请求,一个处理失败的情况。

val first = success.transform( 
	res => res * -1, 						// 成功
	ex => new Exception("see cause", ex) 	// 失败
)

**注意:**现有的transform并不能将一个成功的Future转换为一个失败的Future,或者反向。只能对成功时的结果进行转换或失败时的异常类型进行转换。

Scala 2.12 版本中提供了一种替代的方式,接收Try => Try的函数:

val firstCase = success.transform { 		// 处理成功的 Future
	case Success(res) => Success(res * -1) 
	case Failure(ex) => Failure(new Exception("see cause", ex)) 
}

val secondCase = failure.transform { 		// 处理失败的 Future
	case Success(res) => Success(res * -1) 
	case Failure(ex) => Failure(new Exception("see cause", ex)) 
}

val nonNegative = failure.transform { 		// 将失败转换为成功
	case Success(res) => Success(res.abs + 1) 
	case Failure(_) => Success(0) 
}

组合 Future:zip、fold、reduce、sequence、traverse

zip

zip方法将两个成功的Future转换为一个新的Future,其值两个Future值的元组。

val zippedSuccess = success zip recovered		// scala.concurrent.Future[(Int, Int)]
zippedSuccess.value								// Some(Success((42,-1)))

如果其中一个失败,zip方法的值会以同样的异常失败:

val zippedFailure = success zip failure
zippedFailure.value		// Some(Failure(java.lang.ArithmeticException: / by zero))

如果两个都失败,结果值会包含最初的异常,即调用zip方法的那个Future的异常。

fold

trait TraversableOnce[+A] extends GenTraversableOnce[A]

可以被贯穿一次或多次的集合的模板特质。它的存在主要用于消除IteratorTraversable之间的重复代码。包含一系列抽象方法并在IteratorTraversable..中实现,这些方法贯穿集合中的部分或全部元素并返回根据操作生成的值。

fold方法通过穿过一个TraversableOnceFuture集合来累积值,生成一个Future结果。如果集合中的所有Future都成功了,结果Future会以累积值成功。如果集合中任何一个失败,结果Future就会失败。如果多个Future失败,结果中会包含第一个失败的错误。

val fortyTwo = Future { 21 + 21 }
val fortySix = Future { 23 + 23 }
val futureNums = List(fortyTwo, fortySix)
val folded = Future.fold(futureNums)(0) { 	// (0), 提供一个累积值的初始值
	(acc, num) => acc + num 
}
folded.value								// Some(Success(88))

reduce

reduce方法与fold类似,但是不需要提供初始的默认值,它使用最初的Future的结果作为开始值。

val reduced = Future.reduce(futureNums) { 
	(acc, num) => acc + num 
}
reduced.value	// Some(Success(88))

如果给reduce方法传入一个空的集合,则会以NoSuchElementException异常失败,因为没有初始值。

sequence

sequence方法将一个TraversableOnceFuture集合转换为一个包含TraversableOnce值的Future。比如List[Future[Int]] => Future[List[Int]]:

val futureList = Future.sequence(futureNums)
futureList.value	// Some(Success(List(42, 46)))

traverse

traverse方法将一个包含任意元素类型的TraversableOnce转换为一个TraversableOnceFuture,并且这个序列转换为一个TraversableOnce值的Future。比如,List[Int] => Future[List[Int]]

val traversed =Future.traverse(List(1, 2, 3)) { i => Future(i) }	// .Future[List[Int]]
traversed.value		// Some(Success(List(1, 2, 3)))

执行副作用:foreach、onComplete、andThen

有时需要在Future完成时执行一些副作用,而不是通过Future生成一个、一些值。

foreach

最基本的foreach方法会在Future成功完成时执行一些副作用。失败时将不会执行:

failure.foreach(ex => println(ex))		// 不会执行
success.foreach(res => println(res))	// 42

因为不带yieldfor表达式会被重写为一个foreach执行,因此也可以使用for表达式来实现:

for (res <- failure) println(res)
for (res <- success) println(res)

onComplete

这是Future的一种回调函数,无论Future最终成功或失败,onComplete方法都会执行。它需要被传入一个TrySuccess用于处理成功的情况,Failure用于处理失败的情况:

success onComplete { 
	case Success(res) => println(res) 
	case Failure(ex) => println(ex) 
}

andThen

Future并不会保证通过onComplete注册的回调函数的执行顺序。如果需要保证回调函数的执行顺序,可以使用andThen方法代替,它是Future的两一个回调函数。

andThen方法返回一个对原始Future映射(即与原始 Future 同样的方式成功或失败)的新Future,但是当回调完全执行后才会完成。它的功能是,既不影响原始 Future 的结果,又能在原始 Future 完成时执行一些回调。

val newFuture = success andThen { 
	case Success(res) => println(res) 
	case Failure(ex) => println(ex) 
}
42					// 在回调中打印 结果
newFuture.value		// Some(Success(42)), 同时仍然保持了原始 Future 的值

但是需要注意的是,如果传递给andThen的函数如果在执行时引发异常,该异常会传递给后续的回调或者通过结果Future呈现。

2.12 中的新方法

flatten

flatten方法将一个嵌套的Future转换为一个单层的Future,即Future[Future[Int]] =>Future[Int]

val nestedFuture = Future { Future { 42 } }		// Future[Future[Int]]
val flattened = nestedFuture.flatten			// Future[Int]

zipWith

zipWith方法实质上是对两个Future执行zip方法,并将结果元组执行一个map调用:

val futNum = Future { 21 + 21 }
val futStr = Future { "ans" + "wer" }
val zipped = futNum zip futStr
val mapped = zipped map { case (num, str) => s"$num is the $str" }

使用zipWith只需要一步:

val fut = futNum.zipWith(futStr) { // Scala 2.12 
	case (num, str) => s"$num is the $str" 
}

transformWith

transformWith支持通过一个Try => Future的函数来转换Future

val flipped: Future[Int] = success.transformWith { // Scala 2.12 
	case Success(res) => Future { throw new Exception(res.toString) } 
	case Failure(ex) => Future { 21 + 21 } 
}

该方法实质上是对transform方法的重写,它支持生成一个Future而不是生成一个Try

测试 Future

Future 的作用在于避免阻塞。在很多 JVM 实现上,创建上千个线程之后,线程间的上下文切换对性能的影响达到不能接受的程度。通过避免阻塞,可以繁忙时维持有限的线程数。不过,Scala 支持在需要的时候阻塞Future的结果,通过Await

val fut = Future { Thread.sleep(10000); 21 + 21 }
val x:Int = Await.result(fut, 15.seconds) 		// <= blocks

然后就可以对其结果进行测试:

import org.scalatest.Matchers._
x should be (42)

或者直接通过特质ScalaFutures提供的阻塞结构来测试。比如futureValue方法,它会阻塞直到Future完成,如果Future失败,则会抛出TestFailedException异常。

import org.scalatest.concurrent.ScalaFutures._
val fut = Future { Thread.sleep(10000); 21 + 21 }
fut.futureValue should be (42)			// <= futureValue 阻塞

或者使用 ScalaTest 3.0 提供的异步测试风格:

import org.scalatest.AsyncFunSpec 
import scala.concurrent.Future

class AddSpec extends AsyncFunSpec {

	def addSoon(addends: Int * ): Future[Int] = Future { addends.sum }

	describe("addSoon") { 
		it("will eventually compute a sum of passed Ints") { 
			val futureSum: Future[Int] = addSoon(1, 2) 
			// You can map assertions onto a Future, then return 
			// the resulting Future[Assertion] to ScalaTest: 
			futureSum map { sum => assert(sum == 3) } 
		}
	}
}

1.6 - Future-用例

在 Akka 中实现事务

因为 Akka 中的ask模式有超时问题,这种方式不易于多重逻辑处理与 Debug,因此可以使send模式与Promise组合的方式来实现与其他 actor 的通信:在当前 actor 中创建Promise,通过send发送给其他 actor 引用,在其他 actor 中完成对Promise的填充,然后在当前 actor 中来处理这个被填充后的Promise - 即处理一个Future

如果需要同时与多个 actor 通信,拿到所有 actor 的结果 - 即多个由Promise填充后生成的Future,才能完成后续的逻辑处理 - 即事务部分。只需要通过for表达式的方式来实现“所有需要的Future“都已完成。但是多个Future中任何一部分都会引发异常,包括事务部分,因此在最后的结果失败处理(事务回调)中要对所有的错误情况进行处理,比如通知其他的各个 actor 将刚才的操作分别进行回滚(比如买了一个东西,事务失败,重新将这个东西放回库存中,或者同时,将用户账户的余额扣款取消)。

import scala.concurrent.{ Future, Promise }
import scala.concurrent.ExecutionContext.Implicits.global

val fundsPromise:Promise[Funds] = Promise[Funds]
val sharesPromise:Promise[Shares] = Promise[Shares]

buyerActor ! GetFunds(amount, fundsPromise)
sellerActor ! GetShares(numShares, stock, sharesPromise)

val futureFunds = fundsPromise.future
val futureShares = sharesPromise.future

def transact(funds:Funds, shares:Shares):ResultType = {
  // 一些事务操作,比如更新数据库、缓存等
  // 这里也可能引发一些异常
}

val purchase = for{
  funds <- futureFunds
  shares <- futureShares
  // if ... 一些条件等等
} yield transact(funds, shares)

// 通过回调来处理事务结果
purchase onComplete{
  case Success(transcationResult) =>
  	buyerActor ! PutShares(numShares)
  	sellerActor ! PutFunds(amount)
  	// 通知其他系统事务执行成功
  case Failure(err) =>
  	// 分别检查各个操作是否成功,成功则通知其进行对应的回滚操作
  	futureFunds.onSuccess{ case _ =>  buyerActor ! PutFunds(amount) }
  	futureShares onSuccess { case _ => sellerActor ! PutShares(numShares) }
  	// 通知其他系统事务执行失败
}

在处理事务结果部分,如果需要得到值而不是以通知(副作用)的方式,并对事务结果进行检查以执行其他操作(回滚等),可以使用andThen方法,而不是通过onComplete对调:

val purchaesResult:Future[Result] = purchase andThen{
  case Success(res) => ???
  case Failure(ex) => ???
}
// 然后再响应给客户端或其他后续的处理,而不需要在 onComplete 中编写大量嵌套很深的逻辑
doSomething(purchaesResult)

另一种解决方案是创建一个临时的 actor 来保存状态。

1.7 - Future-集合

**主要解决的问题:**处理Future的集合,使用分组方式避免一次将过多的Future压入执行器队列。有效处理,每个单独Future可能引发的异常,同时不会丢弃Future的结果,对所有的Future结果进行累积并返回。最后抽象为一种清晰易复用的模式提供使用。

Scala、Akka 中的Future并不是 lazy 的,一旦构造会立即执行。而scalaz中为 lazy。Lazy 性质的Future实际是创建一个执行计划,并被最终的调用者执行。这种技术有很多优势,而本文基于标准的Future

示例需要的所有引入:

import scala.language.higherKinds
import scala.collection.generic._
import scala.concurrent._
import scala.concurrent.duration._
import ExecutionContext.Implicits.global

多次调用返回 Future 的方法

首先是一个返回Future的方法:

trait MyService {
  def doSomething(i: Int) : Future[Unit]
}
class MyServiceImpl extends MyService {
 def doSomething(i: Int) : Future[Unit] = Future { Thread.sleep(500);println(i) }
}

有些场景下需要调用多次:

val svc : MyService = new MyServiceImpl
val someInts : List[Int] = (1 to 20).toList
val result : Unit = someInts.foreach(svc.doSomething _)

然而这会创建一个List[Future[Unit]],其中包含 20 个会立即执行Future,20 个还好,如果是成百上千个则难以承受。

程序内部,执行器会将Future存入队列,并使用所有可用的 worker 来执行尽可能多的Future。一次将过多的Future压入队列将会使其他需要执行器的代码进入饥饿状态并引起内存溢出错误。

另一方面,所有svc.doSomething的返回值被丢弃并赋予一个Unit。这里不但没有正确的等待Future完成,而且还把Future分配为Unit,这会扔掉所有可能的异常。

不能将 Future 指派为 Unit

为了避免将Future指派为Unit,可以使用map来替换foreach。同时,需要一种方式来等待所有的Future完成。

val futResult:Future[List[Unit]] = 
  Future.sequence{											// 2
    someInts.map(svc.doSomething _)							// 1
  }
val result: Unit = Await.result(futResult, Duration.Inf)	// 3
  1. 使用map将不会丢弃Future的结果
  2. 使用Future.sequenceList[Future[Unit]]转换为Future[List[Unit]]
  3. 恰当的等待所有Future完成,这时可以安全的丢弃List[Unit],因为没有抛出任何异常

svc.doSomething调用过程中抛出异常时会通过Await.result体现。

Future.sequence会等待所有内部的Future完成,一旦完成,外部的Future则会完成。

Future.sequence 源码

控制 Future 的执行流程

val result:Unit = 
  someInts
  	.grouped(3)										// 1
  	.toList
  	.map{ group =>
  	  val innerFutResult: Future[List[Unit]] = 		// 2
  	    Future.sequence {
  		  group.map(svc.doSomething _)
		}
		Await.result(innerFutResult, Duration.Inf)	// 3
    }.flatten										// 4
  1. someInts每 3 个分成一组
  2. 每一组创建一个Future[List[Unit]]
  3. 使用Await.result等待每一个内部分组完成
  4. 因为将someInts分割成了多个小组,这时需需要使用flatten将整个嵌套的分组展开

这样可以很好的解决一次将大量的Future压入执行器队列,同时使用map也不会丢弃Future的结果。但是有个问题就是这种方式不会返回一个Future结果,因为Await.result的使用使整个执行变成了部分同步阻塞。在实际的异步编程中,这样是不可取的。

返回 Future

为了使结果为Future,下面是新的实现:

val futResult: Future[List[Unit]] = 
  someInts
    .grouped(3)
    .toList
    .foldLeft(Future.successful(List[Unit]())){ (futAccumulator, group) =>	// 1
  	  futAccumulator.flatMap{ accumulator =>								// 2
  		val futInnerResult:Future[List[Unit]] = 
  		  Future.sequence {
  			group.map(svc.doSomething _)
		  }
		futInnerResult.map(innerResult => accumulator ::: innerResult)		// 3
	  }  
	}
val result: Unit = Await.result(futResult, Duration.Inf)
  1. 使用foldLeft替换map,这样可以确保一次只处理一个组,然后当每个组完成时,从左到右对每个组进行累积。累计器被初始化为一个已完成的Future.successful(List[Unit]())
  2. 使用Future.flatMap替换Future.map,这里用于展开返回结果的类型为Future[List[Unit]]。如果使用map,返回结果将会是Future[Future[List[Unit]]]
  3. 一旦一个分组完成,将结果进行累积。

现在返回结果已经是一个Future了,但是这种使用模式很常见,但上面的写法难以复用,因此需要简化其复杂性。

简化

这里将会使用 for 表达式,并使用值类(value class)来实现pimp-my-library模式,pimp-my-library模式将会创建一个隐式包装器类,将方法添加到已有的类上,本质上以面向对象的方式调用新的方法。

implicit class Future_PimpMyFuture[T](val self: Future[T]) extends AnyVal {
    def get : T = Await.result(self, Duration.Inf)
  }
implicit class Future_PimpMyTraversableOnceOfFutures[A, M[AA] <: TraversableOnce[AA]](val self: M[Future[A]]) extends AnyVal {
    /** @return a Future of M[A] completes once all futures have completed */
    def sequence(implicit cbf: CanBuildFrom[M[Future[A]], A, M[A]], ec: ExecutionContext) : Future[M[A]] =
      Future.sequence(self)
  }

然后以下面的方式使用:

val futResult : Future[List[Unit]] =
    someInts
      .grouped(3)
      .toList
      .foldLeft(Future.successful(List[Unit]())) { (futAccumulator,group) =>
        for { 														// 1
          accumulator <- futAccumulator
          innerResult <- group.map(svc.doSomething _).sequence 		// 2
        } yield accumulator ::: innerResult
      }
 
val result : Unit = futResult.get 									// 3
  1. Future.flatMap和嵌套的Future.map替换为更清晰易懂的 for 表达式
  2. 使用上面预定义的“语法糖方法”替换Future.sequence
  3. 使用上面预定义的“语法糖方法”替换Await.result

再次简化

在一个新的值类Future_PimpMyTraversableOnce创建另一个 pimp-my-library方法。

implicit class Future_PimpMyTraversableOnce[A, M[AA] <: TraversableOnce[AA]](val self: M[A]) extends AnyVal {
    /** @return a Future of M[B] that completes once all futures have completed */
    def mapAsync[B](groupSize: Int)(f: A => Future[B])(implicit
      cbf: CanBuildFrom[M[Future[A]], A, M[A]],
      cbf2: CanBuildFrom[Nothing, B, M[B]],
      ec: ExecutionContext) : Future[M[B]] = {
      self
       .toList 		// 1
       .grouped(groupSize)
       .foldLeft(Future.successful(List[B]())) { (futAccumulator,group) =>
         for {
           accumulator <- futAccumulator
           innerResult <- group.map(f).sequence
         } yield accumulator ::: innerResult
       }
       .map(_.to[M]) // 2
    }
  }
  1. 转换为List以有效的进行结果累积
  2. 转换为预期的集合

然后使用:

val futResult : Future[List[Unit]] = someInts.mapAsync(3)(svc.doSomething _)
val result : Unit = futResult.get

1.8 - Future-Promise

介绍

Promise是一个可以被一个值或一个异常完成的对象。并且只能完成一次,完成后就不再可变。称为单一赋值变量,如果再次赋值则会引发异常。

Promise值的类型为Promise[T],可以通过其伴生对象中的Promise.apply()创建一个实例:

def apply[T](): Promise[T]

调用该方法后会立即返回一个Promise实例,它是非阻塞的。当新的Promise对象被创建后,不会包含值或异常,通过successfailure方法分别赋值为异常

通过complete可以提供一个Try[T]实例来填充Promise,这时Promise的值是一个值还是一个异常,取决于Try[T]最终是一个包含值的Sucess还是一个包含异常的Failure对象。

每个Promise对象都明确对应一个Future对象,通过future方法获取关联的Future对象,并且无论调用该方法多少次,都会放回相同的Future对象。

success/failure/complete对应的方法是trySuccess/tryFailure/tryCompletee,这些方法会尝试对Promise进行赋值并返回赋值操作是否成功的布尔值。而基本的success/failure/complete操作返回的是则是对原Promise的引用。

object Promise

Promise的伴生对象,一共定义了四种构造器。

apply

def apply[T](): Promise[T] = new impl.Promise.DefaultPromise[T]()

它不接受然和参数,创建一个能够被类型为T的值完成的Promise对象。即创建它时只需要设置预期被完成的值的类型:

val promise: Promise[User] = Promise[User]()

fromTry

def fromTry[T](result: Try[T]): Promise[T] = new impl.Promise.KeptPromise[T](result)

提供一个Try[T]类型的值,并返回一个被完成后的Promise对象。这个结果Promise中是一个值还是异常,取决于传入的Try[T]最终是一个Success还是一个Failure

failed

def failed[T](exception: Throwable): Promise[T] = fromTry(Failure(exception))

通过传入一个异常创建一个被该异常完成的Promise对象。

successful

def successful[T](result: T): Promise[T] = fromTry(Success(result))

通过传入一个值创建一个被该值完成的Promise对象。

trait Promise

isCompleted

判断该Promise是否已被完成,返回一个布尔值。

complete & tryComplete

def complete(result: Try[T]): this.type =
    if (tryComplete(result)) this 
    else throw new IllegalStateException("Promise already completed.")

def tryComplete(result: Try[T]): Boolean

tryComplete方法尝试通过传入的Try[T]来完成该Promise,返回一个该操作成功失败的布尔值。需要注意的是,虽然返回的是一个布尔值,但这个布尔值表示,该 Promise 之前没有被完成并且已经被当前的操作完成,或在调用该方法之间就已经被完成。

因此,在complete方法中,通过传入一个Try[T]来完成一个Promise,实际上会在内部调用tryComplete,如果``tryComplete返回true,表示该Promise还没有被完成并通过这次操作成功完成,同时返回该Promise`的引用,否则则报错已经被完成过。

completeWith & tryCompleteWith

final def completeWith(other: Future[T]): this.type = tryCompleteWith(other)
final def tryCompleteWith(other: Future[T]): this.type = {
    other onComplete { this tryComplete _ }
    this
  }

completetryComplete类似,不过他接收的是一个Future[T]而不是一个Try[T]

success & trySuccess

def success(@deprecatedName('v) value: T): this.type = complete(Success(value))
def trySuccess(value: T): Boolean = tryComplete(Success(value))

success方法通过一个值来完成Promise,而trySuccess则首先将传入的值包装为一个Success[T ]然后调用前面的tryComplete方法,尝试完成Promise并返回一个布尔值。

failure & tryFailure

def failure(@deprecatedName('t) cause: Throwable): this.type = complete(Failure(cause))
def tryFailure(@deprecatedName('t) cause: Throwable): Boolean = tryComplete(Failure(cause))

successtrySuccess处理过程相同,只是通过一个异常而不是一个值来完成Promise

future

def future: Future[T]

获取该Promise对应的Future

总结

  • 一个Promise有两种构造方式,构造为未完成的、构造为已完成的
  • 一个Promise只能调用完成方法一次,无论是哪个完成方法,再次调用将抛出已完成异常
  • 一个Promise只与一个Future对应,无论调用多少次future方法都会返回相同的Future

1.9 - Implicits-基础

《Programming in Scala 3rd》- Implicit Conversions And Parameters

主要解决的问题是扩展已有的对象。

隐式转换

每个库对实质上相同的概念有其各自不同的编码方式,隐式转换用于减少两种类型之间的显式转换操作。

隐式-规则

这些隐式定义是编译器允许的、被用于插入到程序中以用来解决类型错误问题。

  • 标记规则:只有那些被关键字implicit标记的定义
  • 作用域规则:被插入的隐式转换在作用域中必须是一个单独的标示符,或者是在被转换对象的伴生对象中。必须直接引入该隐式定义,而不是拥有该定义的对象。比如需要convert,它属于一个对象object,只有直接通过import object.convert才有效。需要的隐式定义在伴生对象中时则不需要显式引入。
  • 一次一个规则:只有一个隐式被插入。编译器永远不会将x + y重写为convert1(convert2(x)) + y
  • 显式优先规则:无论何时以代码编写的方式类型检查,都不会尝试隐式。编译器不会再去改变已经运行的代码。因此你可以随时使用显式定义替换隐式定义。

隐式转换到一个指定的类型

如果编译器看到一个 X,但是需要的是一个 Y,这时他会寻找隐式转换函数来将 X 转换为 Y。

隐式转换方法调用者

如果编译器看到如下代码obj.toInt,但是对象obj并不拥有toInt方法,这是会尝试查找拥有该方法的类型并尝试进行隐式转换。

与新类型互操作

1.10 - Implicits-进阶

隐式转换的用途:

  • DSL
  • Type evidence
  • 减少代码冗余
  • Type class
  • 编译期 DI
  • 扩展现有库

需要注意的地方:

  • 解决规则会变得困难
  • 自动转换
  • 不要过度使用

隐式转换

一个普通的方法:

def makeString(i:Int):String = s"makeString: ${i.toString}"

以正常方式调用:

makeString(100)

但是如果我们有一个其他非 Int 类型的对象需要作为该方法的参数,则需要提供一个隐式转换,将其他类型转换为需要的 Int 类型:

case class Balance(amount:Int)
val balance = Balance(100)

implicit def balance2Int(balance:Balance):Int = balance.amount
makeString(balance)

JavaConversion & JavaConverters

在与 Java 互操作时,比如我们的方法需要一个 Scala 类型的集合,而原有代码只能提供 Java 类型的集合,这时可以引入 Scala 中预定义的两种类型的隐式转换:

val javaList:java.util.List[Int] = java.util.Array.asList(1,2,3)
def someDefUsingSeq(seq:Seq[Int]) = println(seq)
import scala.collection.JavaConversions._
someDefUsingSeq(javaList)

或者以更好的方式,在转换时显示指定:

import scala.collection.JavaConverters
someDefUsingSeq(javaList.asScala)

隐式视图

如果我们需要包装或扩展一个已有的类型对象:

class StringWrapper(s:String){
  def quoted = s"$s"
}

可以以下面的方式调用:

new StringWrapper("string").quoted

但是并不能以下面的方式调用:

"string".quoted

这时,可以定义一个从StringStringWrapper视图作为隐式转换:

implicit def warpString(s:String):StringWrapper = new StringWrapper(s)
"string".quoted		// "string" 会通过隐式转换自动转换为一个 StringWrapper 对象

视图绑定:废弃。

隐式参数

声明一个带有隐式参数的方法:

def giveMeAnInt(implicit i: Int) = i

可以以正常的方式调用该方法:

giveMeAnInt(1)

以隐式的方式提供参数:

implicit val someInt = 100
giveMeAnInt	// 100

但是如果同时提供多个隐式 Int 类型值,则会报错。

隐式类

假如其他的库已经定义了一个类:

case class Balance(amount:Int)
val balacne = 100

如果这时我们想对它做些操作,比如:

-balance

这时,我们可以创建一个包装类来扩展它:

implicit class RichBalance(val balance:Balance){
  def unary-: Balance = balance.copy(amount = -balance.amount)
}

这种用法主要用于扩展其他已有的库或类型。

隐式声明

作用域

查找优先级:

  • 通过名字,不使用任何前缀,当前作用域
  • 在隐式作用域中:
    • 伴生对象
    • 包对象
      • 源类型的包对象
      • 参数和超类、超特质的包对象

Type class

1.11 - Trait-基础

原理

定义特质

trait Philosophical{
  def philosophical() {
    println("I consume memory, therefore I am!")
  }
}

除了使用trait关键字,与类的定义一样,也没有声明超类,因此只有一个默认的AnyRef作为超类。同时定义了一个具体的方法。

在类中混入特质

然后将特质混入到一个类中:

class Forg extends Philosophical {
  override def toString = "green"
}

可以使用extendswith混入特质,混入特质的类同时将会隐式的继承特质的超类。

从特质继承的方法可以向使用超类中的方法一样:

val forg = new Forg()
forg.philosophical()		// I consume mem...

特质也是类型

val phil: Philosophical = forg
phil.philosophical()		// I consume mem...

phil变量被声明为Philosophical类型,因此它可以被初始化为任何继承了Philosophical特质的类的实例。

混入多个特质

如果需要将特质混入到一个继承自超类的类里,可以使用with来混入特质,或者使用多个with混入多个特质:

class Animal
trait HasLegs
class Forg extends Animal with Philosophical with HasLegs{
  override def toString = "green"
}

重写特质方法

class Forg extends Philosophical {
  override def toString = "green"
  override def philosophical() {
    println("It ain't easy being $toString!")
  }
}

这时,类Forg的实例仍然可以赋值到Philosophical类型的变量,但是其方法已经被重写。

特质与类的不同

特质类似于带有具体方法的 Java Interface。特质能够实现类的所有功能,但是有两点差别:

  • 特质没有构造器
  • 无论类的所处位置,super调用都是静态绑定的。但是特质中是动态绑定的。因为可以同时混入多个特质,其绑定会基于混入特质的顺序。

胖瘦接口

特质一种主要的应用方式是更具类已有的方法自动为类添加方法。但是在 Java 的接口中,如果需要多个接口的相互继承,必须在子接口中声明所有父接口的抽象方法。而在 Scala 中,可以通过定义一个包含部分抽象方法的特质,和一个包含大量已实现方法的特质,子类在继承多个特质后只需要实现抽象方法部分,并且同时能够获得需要的所有方法。

实例:Ordered 特质

比如一个需要比较的对象,希望使用><等操作符来进行比较:

class Rational(n:Int, d:Int){
  // ...
  def < (that.Rational) = this.numer * that.denom > that.numer * this.denom
  def > (that.Rational) = that < this
  def <= (that:Rational) = (this < that) || (this == that)
  def >= (that.Rational) = (this > that) || (this == that)
}

一共定义了 4 中操作符,并且后续的 3 中都是基于第一个方法,这是一个胖接口的典型,如果是瘦接口的话,只会顶一个一个<操作符,然后其他的功能由客户端自己实现。

这些用法很常见,因此 Scala 专门提供了一个Ordered特质,只需要定义一个compare方法,然后Ordered会自动创建所有的比较操作。比如:

class Rational(n:Int, d:Int) extends Ordered[Rational]{
  def compare(that:Rational) = 
    (this.numer * that.denom) - (that.numer - this.denom)
}

这个compare方法通过比较两个值的差来判断大小。可以参考Ordered原码:

trait Ordered[A] extends Any with java.lang.Comparable[A] {
  def compare(that: A): Int
  def <  (that: A): Boolean = (this compare that) <  0
  def >  (that: A): Boolean = (this compare that) >  0
  def <= (that: A): Boolean = (this compare that) <= 0
  def >= (that: A): Boolean = (this compare that) >= 0
  def compareTo(that: A): Int = compare(that)
}

特质叠加

当类同时混入多个特质时,混入的特质会从右到左依次执行。

如果混入特质中使用了super,将会调用其左侧特质中的方法。

多重继承

应用场景

  1. 如果行为不会被复用,使用具体类
  2. 如果需要在多个不相关的类中使用,使用特质
  3. 如果需要构造参数,或者在 Java 中使用这部分代码,使用抽象类。因为 Scala 中只有那些仅包含抽象成员的特质会被翻译为 Java 的接口,否则并没有具体的模拟。
  4. 如果效率非常重要,使用类。大多数 Java 运行时都能让类成员的续方法调用快于接口方法调用。

1.12 - Trait-用例

介绍

特质(trait)类似于 Java 中的接口(interface)。类似 Java 类能够实现多个接口,Scala 类能够扩展多个特质。

像 Interface 一样使用 trait

在特质中,仅声明在需要扩展的类中需要的方法

trait BaseSoundPlayer { 
  def play 
  def close 
  def pause 
  def stop 
  def speak(whatToSay: String)
}

然后在需要扩展的类中使用特质,仅有一个特质时使用 extends,多于一个时第一个使用 extends,后续的使用 with:

class Mp3SoundPlayer extends BaseSoundPlayer { ...
class Foo extends BaseClass with Trait1 with Trait2 { ...
class Foo extends Trait1 with Trait2 with Trait3 with Trait4 { ...

除非一个类被声明为抽象类(abstract),否则需要实现特质中的所有抽象方法:

class Mp3SoundPlayer extends BaseSoundPlayer { 
  def play { // code here ... } 
  def close { // code here ... } 
  def pause { // code here ... } 
  def stop { // code here ... } 
  def resume { // code here ... } 
}
    
abstract class SimpleSoundPlayer extends BaseSoundPlayer { 
  def play { ... } 
  def close { ... } 
}

同时,一个特质能够扩展另一个特质:

trait Mp3BaseSoundFilePlayer extends BaseSoundFilePlayer { 
  def getBasicPlayer: BasicPlayer 
  def getBasicController: BasicController 
  def setGain(volume: Double) 
}

特质中除了能够声明抽象方法,还能提供方法的实现,类似 Java 中的抽象类:

abstract class Animal {
  def speak 
}

trait WaggingTail { 
  def startTail { println("tail started") } 
  def stopTail { println("tail stopped") } 
}

当一个类拥有多个特质时,这些特质被称为**混入(mix)**这个类。混入同时用于使用特质来扩展一个单独的对象:

val f = new Foo with Trait1

在特质中使用抽象或具体的字段

可以在特质中定义抽象或具体字段,以便所有扩展他们的类型都能够使用它们。没有提供初始值的字段将会是一个抽象字段,拥有初始值的字段将会是具体字段:

trait PizzaTrait { 
  var numToppings: Int 
  var size = 14 
  val maxNumToppings = 10 
}

类似于抽象方法,被扩展的类需要实现所有抽象字段,否则需要声明为抽象类。

在特质中定义字段时可以使用 var 或 val,如果是 val,在子类或子特质中需要使用 override 来覆写该字段,而 var 则不需要。

像抽象类一样使用特质

在特质中定义的方法跟普通的 Scala 方法类似,可以直接使用或重写它们。

trait Pet { 
  def speak { println("Yo") }  	// 具体方法
  def comeToMaster 				// 抽象方法
}

class Dog extends Pet { 
  // 如果不需要则不用重写特质中的具体方法
  def comeToMaster { ("I'm coming!") } 	// 实现抽象方法
}

class Cat extends Pet { 
  override def speak { ("meow") }		// 重写具体方法 
  def comeToMaster { ("That's not gonna happen.") } 
}

虽然 Scala 中有抽象类,但是一个类只能继承一个抽象类,但是可以同时扩展多个方法,除非如 Classes 章节中介绍的对抽象类有特殊需要,否则,使用特质户更加灵活。

简单的混入

需要将多个特质混入类以提供健康的设计。为了实现简单的还如,只需要在特质中定义需要的方法,然后在需要扩展的类中使用 extends 或 with 来进行混入。

下面的例子中,同时继承自一个抽象类并混入一个特质,同时实现了抽象类中的抽象方法:

trait Tail { 
  def wagTail { println("tail is wagging") } 
  def stopTail { println("tail is stopped") } 
}

abstract class Pet (var name: String) {
  def speak // abstract 
  def ownerIsHome { println("excited") } 
  def jumpForJoy { println("jumping for joy") } 
}

class Dog (name: String) extends Pet (name) with Tail { 
  def speak { println("woof") } 
  override def ownerIsHome { wagTail speak } 
}

限定哪些类可以通过继承来使用特质

如果需要对一些特质进行限制,比如,只能添加到继承了一个抽象类或扩展了一个特质的类上。

trait [TraitName] extends [SuperThing]

这种声明语法称为 TraitName,TraitName 只能被混入到哪些扩展了名为 SuperThing 的类型的类中,这里的 SuperThing 可以是 特质、类、抽象类。

简单的说就是,只有继承或扩展了 SuperThing,才能混入 TraitName 这个特质

class StarfleetComponent 
trait StarfleetWarpCore extends StarfleetComponent 
class Starship extends StarfleetComponent with StarfleetWarpCore

这个例子中,StarfleetWarpCore 继承了 StarfleetComponent,而 Starship 也继承了 StarfleetComponent,因此,类 Starship 可以混入特质 StarfleetWarpCore。

限定一个特质只混入到一种类型的子类

可以为一个特质添加一个类型,只有该类型的子类才能混入该特质。

trait StarfleetWarpCore { 
  this: Starship => 
  // more code here ... 
}

这个特质只能混入到 Starship 的子类中,比如:

class Starship 
class Enterprise extends Starship with StarfleetWarpCore

否则将会报错:

class RomulanShip 
class Warbird extends RomulanShip with StarfleetWarpCore	// 错误

详细的错误信息:

error: illegal inheritance; 
self-type Warbird does not conform to StarfleetWarpCore's selftype StarfleetWarpCore with Starship 
class Warbird extends RomulanShip with StarfleetWarpCore

错误中提到了 self type,关于 self type 的描述:

“Any concrete class that mixes in the trait must ensure that its type conforms to the trait’s self type.”

任何混入特质的实现类必须确保它的类型与特质的 self type 一致。

特质同时能够限定混入它的类型需要同时扩展其他多个类型,想要混入特质 WarpCore 的类型需要同时混入 this 关键字后面的所有类型:

trait WarpCore {
  this: Starship with WarpCoreEjector with FireExtinguisher => 
}

限定特质只能混入到拥有特定方法的类型

可以使用 self type 语法的变型来限制一个特质只能混入到拥有特定方法的类型(类、抽象类、特质):

trait WarpCore {
  this: { def ejectWarpCore(password: String): Boolean } => 
}

因此,如果想要混入特质 WarpCore,需要拥有更上述方法签名一致的方法才可以。

或者需要多个方法:

trait WarpCore { 
  this: { 
    def ejectWarpCore(password: String): Boolean 
    def startWarpCore: Unit 
  } => 
}

这个方式称为结构化类型

将特质添加到对象实例

不同于将特质混入到实际的类,同样能够在创建对象时,将特质扩展到一个单独的对象。

class DavidBanner

trait Angry {
  println("You won't like me ...") 
}

object Test extends App {
  val hulk = new DavidBanner with Angry 
}

或者更为实际的用法:

trait Debugger { 
  def log(message: String) {
    // do something with message 
  } 
}

// no debugger 
val child = new Child

// debugger added as the object is created 
val problemChild = new ProblemChild with Debugger

像特质一样扩展一个 Java Interface

如果想要在 Scala 中实现一个 Java 接口,可以和使用特质一样,使用 extends 和 with 来扩展。

首先是 Java 接口的定义:

public interface Animal {
  public void speak(); 
}

public interface Wagging {
  public void wag(); 
}

public interface Running {
  public void run(); 
}

然后在 Scala 中像使用特质一样使用他们:

class Dog extends Animal with Wagging with Running { 
  def speak { println("Woof") } 
  def wag { println("Tail is wagging!") } 
  def run { println("I'm running!") }
}

区别在于 Java 中的接口都没有实现行为,因此要实现接口或声明为抽象类。

1.13 - Try

介绍

类型Try表示一个计算,它的结果可能是一个异常,或者成功完成计算的值。它类似于类型Either但是语义上完全不同。

Try[T]的实例必须是一个scala.util.Success[T]scala.util.Failure[T]

一个实例:处理用户的输入并计算,并且在可能会抛出异常的位置并不需要显式的异常处理:

import scala.io.StdIn
import scala.util.{ Try, Sucess, Failure }

def divide: Try[Int] = {
  val dividend: Try[Int] = Try(StdIn.readLine("Enter an Int that you'd like to divide:\n").toInt)		// 有可能会抛出异常,比如用户输入一个不能转换为 Int 的值
  val divisor: Try[Int] = Try(StdIn.readLine("Enter an Int that you'd like to divide by:\n").toInt)			// 有可能会抛出异常
  val problem: Try[Int] = dividend.flatMap(x => divisor.map(y => x/y))	// TIP
  problem match {
    case Success(v) =>
      println("Result of " + dividend.get + "/"+ divisor.get +" is: " + v)
      Success(v)
    case Failure(e) =>
      println("You must've divided by zero or entered something that's not an Int. Try again!")
      println("Info from the exception: " + e.getMessage)
      divide		// 递归调用自身,重新获取用户输入
  }
}

TIP部分展示了Try的重要属性,它能够进行管道、链式的方式组合操作,同时进行异常的捕获。

**它只能捕获 non-fatal 异常,如果是系统致命异常,将会抛出。**查看scala.util.control.NonFatal

sealed abstract class Try[+T]

Try本身定义为一个封闭抽象类,继承它的有两个子类:

final case class Failure[+T](exception: Throwable) extends Try[T]
final case class Success[+T](value: T) extends Try[T]

因此,Try的实例有么是一个Failure,要么是一个Success,分别表示失败成功

两个子类都只有一个类成员,Failure为一个异常,Success为一个值。

toOption

def toOption: Option[T] = if (isSuccess) Some(get) else None

如果是一个Success就返回它的值,是Failure则返回None。

用例:

def decode(text:String):Option[String] = Try { base64.decode(text) }.toOption

transform

def transform[U](s: T => Try[U], f: Throwable => Try[U]): Try[U] =
    try this match {
      case Success(v) => s(v)
      case Failure(e) => f(e)
    } catch {
      case NonFatal(e) => Failure(e)
    }

接收两个偏函数,一个用于处理成功的情况,一个用于处理失败的情况。根据对应偏函数的处理结果,生成一个新的Try实例。

object Try

这是Try的伴生对象,其中定义了Try的构造器:

def apply[T](r: => T): Try[T] =
    try Success(r) catch {
      case NonFatal(e) => Failure(e)
    }

可见,构造器中只是进行了普通的try/catch处理,并且对NonFatal异常进行捕获。成功则返回一个Success实例,失败则返回一个Failure实例。

Failure

isFailure & isSuccess

def isFailure: Boolean = true
def isSuccess: Boolean = false

覆写父类抽象方法,分别写死为truefalse。用于判断是否成功。

recoverWith

def recoverWith[U >: T](f: PartialFunction[Throwable, Try[U]]): Try[U] =
   try {
     if (f isDefinedAt exception) f(exception) else this
   } catch {
     case NonFatal(e) => Failure(e)
   }

接受一个Throwable => Try[U]类型的偏函数,如果该偏函数定义了原始Try抛出的异常,将异常转换为一个新的Try实例,否则,返回原始Try的异常。

最后,对偏函数的执行或原始异常进行NonFatal捕获,生成一个可能的新的Failure实例。

get

def get: T = throw exception

直接抛出异常,即实例成员。

flatMap

def flatMap[U](f: T => Try[U]): Try[U] = this.asInstanceOf[Try[U]]

接收一个T => Try[U]类型的偏函数,将本身转换为当前偏函数返回值类型。

flatten

def flatten[U](implicit ev: T <:< Try[U]): Try[U] = this.asInstanceOf[Try[U]]

foreach

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

接收一个T => U类型的偏函数,因为实例本身的成员是一个异常,因此对Failure调用只会返回一个Unit而不会真正对成员执行传入的偏函数。

map

 def map[U](f: T => U): Try[U] = this.asInstanceOf[Try[U]]

接收一个T => U的偏函数。

filter

def filter(p: T => Boolean): Try[T] = this

接收一个T => Boolean类型的偏函数,对实例成员进行过滤,直接返回实例本身,结果仍然是一个包含异常的Failure

recover

def recover[U >: T](rescueException: PartialFunction[Throwable, U]): Try[U] =
    try {
      if (rescueException isDefinedAt exception) {
        Try(rescueException(exception))
      } else this
    } catch {
      case NonFatal(e) => Failure(e)
    }

接受一个Throwable => U类型的偏函数,如果偏函数定义了原始异常,则通过偏函数来处理原始异常并生成一个新的Try实例,否则返回自身。

failed

def failed: Try[Throwable] = Success(exception)

将自身转换为一个包含自身成员异常的Success实例。

Success

isFailure & isSuccess

def isFailure: Boolean = false
def isSuccess: Boolean = true

重写父类方法并分别写死为falsetrue

recoverWith

def recoverWith[U >: T](f: PartialFunction[Throwable, Try[U]]): Try[U] = this

接收一个Throwable => Try[U]类型的偏函数,因为自身成员并不是异常类型,因此直接返回自身实例,不做异常处理。

get

def get = value,直接返回自身成员的值。

flatMap

def flatMap[U](f: T => Try[U]): Try[U] =
    try f(value)
    catch {
      case NonFatal(e) => Failure(e)
    }

接收一个T => Try[U]类型的偏函数,将自身成员应用到该偏函数,成功则生成一个新的Try,否则进行异常捕获。

flatten

def flatten[U](implicit ev: T <:< Try[U]): Try[U] = value

返回成员值。

foreach

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

接收一个函数并将成员值作用到该函数。

map

def map[U](f: T => U): Try[U] = Try[U](f(value))

接收一个函数,将成员值作用到该函数并生成一个新的Try

filter

def filter(p: T => Boolean): Try[T] = {
    try {
      if (p(value)) this
      else Failure(new NoSuchElementException("Predicate does not hold for " + value))
    } catch {
      case NonFatal(e) => Failure(e)
    }
  }

如果成员值满足传入的函数,则返回自身,否则,返回一个包含NoSuchElementException异常的Failure实例。

recover

def recover[U >: T](rescueException: PartialFunction[Throwable, U]): Try[U] = this

返回自身,不做处理。

failed

def failed: Try[Throwable] = Failure(new UnsupportedOperationException("Success.failed"))

调用该方法时,生成一个新的包含UnsupportedOperationException异常的Failure

1.14 - Type-用例

介绍

Scala 有一个强大的类型系统。然而,除非你是一个库的创建者,你可以不用深入了解类型系统。但是一旦你要为其他用户创建集合类型的 API,你就需要学习这些。

Scala 类型系统使用一组标示符来表示不同的泛型类型概念,包括型变、界限、限定

界限

界限(bound)用于限制类型参数。界限标示符汇总:

标示符名称描述
A <: B上界A 必须是 B 的子类
A >: B下界A 必须是 B 的父类
A <: Upper >: Lower同时使用上下界A 同时用于上界和下界
A <% B视图界限
T : M上下文界限

类型限定

Scala 允许你指定额外的类型限制:

A =:= B 	// A 和 B 必须相同
A <:< B 	// A 必须是 B 的子类型
A <%< B		// A 必须是 B 的视图类型

常用类型参数标示符

标示符说明
A用于一个简单的类型时,List[A]
B,C,D用于同时需要多个类型时
K在 Java 中常用于 Map 的 key,Scala 中多使用 A
N用于一个数字类型
V跟 V 类似,Scala 中多用 B

类型参数化

类型参数化用于编写泛型类和特质。

实例:queues 函数式队列

函数式队列是拥有以下三种操作的数据结构:

head	返回队列的第一个元素
tail	返回除第一个元素之外的队列
append	返回尾部添加了指定元素的队列

不同于可变队列,函数式队列在添加元素时不会改变其内容,而是返回包含这个元素的新队列。

支持的工作方式:

val q1 = Queue(1,2,3)
val q2 = q1.append(4)
print(q1)	// Queue(1,2,3)

如果将 Queue 实现为可变类型,则 append 操作会改变 q1 的值,这是 q1 和 q2 都拥有新的元素 4。但是对于函数式队列来说,新添加的元素只能出现在 q1 中而不能出现在 q2 中。

纯函数式队列与 List 具有相似性,都被称为是完全持久的数据结构,即使在扩展或改变之后,旧的版本依然可用。但是 List 通常使用::操作在前端扩展,队列使用append在后端扩展。

Queue 需要保持三种操作都能在常量时间内完成,低效的实现和最后的实现:

class SlowAppendQueue[T](elems:List[T]){
  def head = elems.head
  def tail = new SlowAppendQueue(elems.tail)
  // append 操作的时间花费与元素的数量成正比
  def append(x:T) = new SlowAppendQueue(elems :: List(x))
}

class SlowHeadQueue[T](smele:List[T]){
  // 将 elems 元素顺序翻转
  def head = smele.last								// 与元素个数成正比
  def tail = new SlowHeadQueue(smele.init)			// 与元素个数成正比
  def append(x:T) = new SlowHeadQueue(x :: smele)	// 常量
}

class Queue[T](
  private val leading: Lit[T]
  private val trailing: List[T]
) {
  private def mirror = {
    if (leading.isEmpty) new Queue(trailing.reverse :: Nil)
    else this
  }
  
  def head  = mirror.leading.head
    
  def tail = {
  	val q = mirror
    new Queue(q.leading.tail, q.trailing)
  }
  
  def append(x: T) = new Queue(leading, x :: trailing)
}

最终的方案中使用两个 List,leading 和 trailing 来表达队列。leading 包含前段元素,trailing 包含了反向排序的后段元素,整个队列表示为:leading :: trailing.reverse

  • append 时,使用::将元素添加到 trailing,时间为常量。如果一直通过添加操作构建队列,则 leading 部分会一直为空。
  • tail 时,如果 tailing 不为空会直接返回。如果为空,需要将 tailing 翻转并复制给 leading 然后取第一个元素返回,这个操作称为 mirror,时间与元素量成正比。
  • head 与 tail 类似。

这种方案基于三种操作的频率接近。

信息隐藏

上面 Queue 的实现中暴露了细节实现。

私有构造器及工厂方法

使用私有构造器和私有成员来隐藏类的初始化代码和表达代码。

Java 中可以把主构造器声明为私有使其不可见,Scala 中无须明确定义。虽然它的定义可以隐含在类参数以及类方法体中,还是可以通过在类参数列表前添加 private 修饰符把主构造器隐藏起来:

class Queue[T] private (
  private val leading:List[T],
  private val trailing:List[T]
)

这个参数列表之前的 private 修饰符表示 Queue的构造器是私有的:它只能被类本身或伴生对象访问。类名 Queue 仍然是公开的,因此可以继续使用这个类,但不能调用它的构造器。

可以通过添加辅助构造器来创建实例:

def this() = this(Nil, Nil)	 					// 可以构建空队列
def this(elems:T*) = this(elems.toList, Nil)	// 可以提供队列初始元素

另一种方式添加一个工厂方法,最简单的方式是定义与类同名的对象及 apply 方法:

object Queue{
  def apply[T](xs:T*) = new Queue[T](xs.toList, Nil)
}

// Usage
Queue(123)

同时将该对象放到 Queue 类同一个源文件,成为 Queue 类的伴生对象。

供选方案:私有类

直接将类隐藏掉,仅提供能够暴露类公共接口的特质。

trait Queue[T]{
  def head:T
  def tail:Queue
  def appand(x:T):Queue[T]
}

object Queue{
  def apply[T](xs:T*):Queue[T] = New QueueImpl[T](xs.toList, Nil)
  
  private class QueueImpl[T](
    private val leading:List[T],
    private val trailing:List[t]
  ) extends Queue[T]{
    def mirror = {
  	  if (leading.isEmpty) new QueueImpl(trailing.reverse, Nil) else this
	}
	
	def head:T = mirror.leading.head
	
	def tail:QueueImpl[T] = {
      val q = mirror
      new QueueImpl(q.leading.tail, q.trailing)
	}
	
	def append(x:T) = new QueueImpl(leading, x :: trailing)
  }
}

型变注解

上面定义的 Queue 是一个特质而不是类型,因为他带有类型参数。即 Queue 是特质,也可称为类型构造器(给它提供参数来构建新的类型),Queue[Int] 是类型。

带有参数的类和特质是泛型的,但是他们产生的类型已被“参数化”,不再是泛型的。

泛型:指通过一个能够广泛适用的类或特质来定义许多特定的类型。

**型变:泛型话类型(Queue[T])产生的类型家族成员(Queue[String],Queue[Int],…)之间具有的特定的子类型关系。**定义了参数化类型传入方法的规则。

协变:如果 S 类型是 T 类型的子类型,同时 Queue[S] 也是 Queue[T] 的子类型,即认为,Queue 是与他的类型参数 T 保持协变的。

协变意味着,如果S 为 T 的子类型,向能够接受参数类型为 Queue[T] 的函数传入类型为 Queue[S] 的参数。比如方法签名def func(arg:Queue[AnyRef]),可以调用func(Queue[String]),因为 String 是 AnyRef 的子类型。

非型变:Scala 中默认的泛型类型是非型变的。即不泛型类型产生的类型家族成员之间没有子类型关系。

非型变意味着,即便是类型参数之间有子类型关系,比如 String 是 AnyRef 的子类型,但是泛型类型为非型变,则 Queue[String] 不能当做 Queue[AnyRef] 来使用,必须使用定义的 Queue[AnyRef] 类型。

逆变:如果 T 是 S 的子类型,表示 Queue[T]Queue[S] 的父类型。

逆变意味着,如果 S 为 T 的子类型,向能够接受参数类型为 Queue[S] 的函数传入类型为 Queue[T] 的参数。

这里说的参数传入的例子,即:方法参数预期为父类,传入必须为子类(里氏替换原则:任何使用父类的地方都可以用子类替换掉,因为子类拥有父类所有的属性和方法,即只需要一个父类就完成的工作,传入了一个功能更多的子类当然也能完成需要的工作)。

型变参数:

标示符名称描述
Array[T]非型变类型当容器中的元素是可变的即 collections.mutable。比如:预期参数为 Array[String] 的方法只能传入 Array[String]
Seq[+A]协变类型当容器中的元素是不可变的,这使容器更灵活。比如,预期参数为 Seq[Any] 的方法可以传入 Seq[String]
Foo[-A]逆变类型与协变相反
Function1[-A, +B]组合型变参考 Function1 特质的定义

一个型变的实例:

// 一组具体类型
class Grandparent 
class Parent extends Grandparent 
class Child extends Parent

// 一组容器类型
class InvariantClass[A] 		// 不变容器类型,容器中只能传入类型 A
class CovariantClass[+A] 		// 协变容器类型,容器中只能传入类型 A 和 A 的子类型 
class ContravariantClass[-A]	// 逆变容器类型,容器中只能传入类型 A 和 A 的父类型

class VarianceExamples {

  def invarMethod(x: InvariantClass[Parent]) {}
  def covarMethod(x: CovariantClass[Parent]) {}
  def contraMethod(x: ContravariantClass[Parent]) {}

  invarMethod(new InvariantClass[Child]) 				// 正确
  invarMethod(new InvariantClass[Parent]) 				// 错误
  invarMethod(new InvariantClass[Grandparent])			// 错误

  covarMethod(new CovariantClass[Child]) 				// 正确
  covarMethod(new CovariantClass[Parent]) 				// 正确
  covarMethod(new CovariantClass[Grandparent])			// 错误

  contraMethod(new ContravariantClass[Child]) 			// 错误
  contraMethod(new ContravariantClass[Parent]) 			// 正确
  contraMethod(new ContravariantClass[Grandparent])		// 正确
}

一个逆变的例子:

trait OutputChannel[-T] {
  def write(x: T)
}

这里定义 OutputChannel 是逆变的,比如:一个 Channel[AnyRef] 会是 Channel[String] 的子类型。如果用做一个方法参数:def func(arg: Channel[String]),可以调用为:func(Channel[AnyRef])

型变与数组

Scala 认为 数组是非型变的。

检查型变注解

只要泛型的参数类型被当做方法参数的类型,那么包含它的类或特质就有可能不与类型参数一起协变。

比如:

class Queue[+T] {
  def append(x: T) ...
}

类型 T 即作为泛型 Queue 的参数类型,又作为方法 append 的参数类型,这是不允许的,编译器会报错。

下界

上面例子中 Queue[T] 不能实现对 T 的协变,因为 T 作为参数类型出现在了 append 方法中。想要解决这个问题,可以把 append 变为多态使其泛型化,并使用它的类型参数的下界:

class Queue[+T](...){
  def append[U >: T](x: U) = new Queue[U](leading, x :: trailing) ...
}

这个定义指定了 append 的类型参数 U,并通过语法U >: T定义了 T 为 U 的下界,即 U 必须是 T 的超类。

比如类 Fruit 及两个子类 Apple、Orange。现在可以吧 Orange 对象加入到 Queue[Apple],返回个 Queue[Fruit] 类型。

这个定义支持,队列类型元素类型为 T,即Queue[T],允许将任意 T 的超类 U 的对象加入到队列中,结果为 Queue[U]

对象私有数据

为了避免 leading 一直为空导致的 mirror 不断重复的执行,下面是改进后的 Queue 定义:

class Queue[+T] private (
  private[this] var leading: List[T],
  private[this] var trialing: List[T]
) {
  private def mirror() = {
    if(leading.isEmpty) {
	  while(!trailing.isEmpty) {
  		leading = trailing.head :: leading
        trailing = trailing.tail
	  }  
    }
  }
  
  def head: T = {
    mirror()
    leading.head
  }
  
  def tail: Queue[T] = {
    mirror()
    new Queue(leading.tail, trailing)
  }
  
  def append[U >: T](x: U) = new Queue[U](leading, x :: trailing)
}

这个版本中的 leading 和 trailing 都是可变变量。而 mirror 从 trailing 反向复制到 leading 的操作通过副作用对两段队列进行修改而不是返回队列。由于二者都是私有变量,因此这些操作对客户端是不可见的。

同时,leading 和 trailing 都被 private[this]修饰符声明对对象私有了,因此能通过类型检查。Scala 的型变检查包含了关于对象私有定义的特例。当检查到带有+/-符号的类型参数只出现在具有相同型变分类的位置上时,这种定义将被忽略。

上界

下面是一个为自定义类实现排序的例子。通过把 Ordered 混入类中,并实现 Ordered 唯一的抽象方法 compare,就可以使用 <,>.<=,>=来做类实例的比较:

class Person(val firstName:String, val lastName:String) extends Ordered[Person] {
  def compare(that:Person) = {
    val lastNameComparison = lastNmae.compareToIngoreCase(that.lastName)
    if (lastName.comparison != 0) lastNameComparison
    else firstName.conpareToIngoreCase(that.firstName)
  }
  
  override def toString = firstName + " " + lastName
}

为了让传递给你的新排序函数的列表类型混入到 Ordered 中,需要使用到上界。通过指定T <: Ordered[T],表示类型参数 T 具有上界 Ordered[T],即传递给排序方法 orderedMergeSort 的列表元素类型必须是 Ordered 的子类型。因此,可以传递 List[Person] 给该方法,因为 Person 混入了 Ordered。

def orderedMergeSort[T <: Ordered[T]](xs: List[T]): List[T] = {
  def merge(xs: List[T], ys: List[T]): List[T] = {
    (xs, ys) match{
      case (Nil,_) => ys
      case (_, Nil) => xs
      case (x :: xsl, y: ysl) =>
        if (x < y) x :: merge(xsl, ysl)
        else y :: merge(xs, ysl)
    }
  }
  
  val n = xs.length / 2
  if (n == 0) xs
  else {
    val (ys, zs) = xs splitAt n
    merge(orderedMergeSort(ys), orderedMergeSort(zs))
  }
}

实例

如何使用泛型类型创建类

创建一个能够接受泛型类型的类或方法,比如创建一个链表类:

class LinkedList[A] {
  private class Node[A](elem:A){
    var next: Node[A] = _
    overrice def toString = elem.toString
  }
  
  private var head:Node[A] = _
  
  def add(elem:A){
    val n = new Node(elem)
    n.next = head
    head = n
  }
  
  private def printNodes(n:Node[A]) = {
    if (n != null){
      println(n)
      printNoeds(n.next)
	}
  }
  
  def printAll(){ printNodes(head) }
}

[A]是该类的参数化类型,要创建一个 Int 类型的链表实例:val ints = new LinkedList[Int](),

此时这个链表的整体类型为LinkedList[Int],可以向其添加 Int 类型的节点:ints.add(1)

或者创建其他类型的链表:val strings = new LinkedList[String]val foos = new LinkedList[Foo]

当创建一个基本类型的泛型实例时,比如:val anys = new LinkedList[Any],这是可以传入基本类型 Any 的子类型比如 Int,anys.add(1)。但是如果有一个方法:

def printTypes(elems:LinkedList[Any]) = elems.printAll()

这时并不能传入一个ListedList[Int]到该方法,这需要这个链表直接协变

如果同时需要多个类型参数,比如:

trait Pair[A, B]{
  def getKey:A
  def getValue:B
}

如何使用泛型类型创建方法

创建一个带有类型参数的方法能够使其用于更多的适用范围:

def randomElement[A](seq:Seq[A]):A = {
  val randomNum = util.Random
  seq(randomNum)
}

如何使用鸭子类型(结构化类型)

def callSpeak[A <: { def speak(): Unit }](obj:A){
  obj.speak()
}

在这个定义中,方法callSpeak可以接受任意一种类型 A,只要该类型拥有一个类型参数中定义签名的 speak 方法。

类型参数语法[A <: { def speak(): Unit }]表示,类型 A 必须是一个拥有方法def speak(): Unit的类型的子类型,即上界语法。同时需要注意的是,这个父类中的方法的签名必须与类型参数中定义的签名一致。

使可变集合非型变

在定义一个元素可变的集合时,其元素类型必须是非型变的,即[A]

使用非型变类型会有一些副作用,比如,容器可以同时接收基本类型或其子类型。同时,如果一个方法被声明为接收一个父类型的容器,比如ArrayBuffer[Any],传入ArrayBuffer[Int]则不会通过编译。因为:

  • ArrayBuffer 中的元素是可以改变的
  • 定义的方法接收的是ArrayBuffer[Any],但传入的却是ArrayBuffer[Int]
  • 如果编译器通过了,集合会使用 Any 代替普通的 Int 类型,这是不允许的

如果想要一个方法技能接收父类型的集合,又能接收其子类型的集合,需要使用一个不可变的集合类型,比如 List、Set 等。

在 Scala 中,可变集合是非型变的,而不可变集合为协变,参考协变与飞行变的区别。

使不可变集合协变

正如协变中的说明一样,不可变集合被定义为协变,则,使用这类集合作为参数的方法,同样能够接受其子类型的集合作为参数。

创建一个不可变容器,并声明其为协变:

class Container[+A] (val emel:A)

def makeDogsSpeak(dogHouse:Container[Dog]){
  dogHouse.elem.speak()
}

makeDogsSpeak(new Container(Dog("dog of Dog")))
// SubDog is sub type of Dog
makeDogsSpeak(new Container(SubDog("dog of SubDog"))) 

限制类型参数的范围

在一个拥有类型参数的类或方法中,如果需要限制该类型参数的范围,可以使用上界下界来限制类型参数的可选范围。

比如有一些多重继承的类:

class Professor()
class Teacher() extends Professor
class Student()
class Child() extends Student

假设一些场景:

def teach[A](A >: Teacher)
def learn[A](A <: Student)

这里,只有老师或教授能够讲课,即下界,最少为老师。只有学生或小孩能够学习,即上界。

选择性的为封闭模型添加新行为

比如想要给所有的数字类型增加一个求和方法,比如Int、Double、Float等。因为Numeric类型类已经存在,这支持你创建一个能够接受一个任意数字类型的求和方法:

def add[A](x:A, y:A)(implicit  numeric: Numeric[A]):A = numeric.plus(x,y)

然后,这个方法就可以用于不同的数字类型求和:

add(1, 3)
add(1.0, 1.5)
add(1, 1.5f)

如何创建一个类型类(type class)

创建类型类的过程有点复杂,单仍然可以总结为一个公式:

  • 通常你有一个需求,为一个封闭的模型增加新的行为
  • 为了增加这个行为,你会创建一个类型类。通常的方式是,创建一个基本的特质,然后使用隐式对象对该特质创建具体的实现
  • 然后回到应用中,创建一个使用该类型类的方法将新的行为添加到封闭模型,比如上面创建的 add 方法

比如你有一些封闭模型,包含一个 Dog 和 Cat,你想要 Dog 能够说话而 Cat 不能。

首先是封闭模型:

// 一个已存在的封闭模型
trait Animal
final case class Dog(name:String) extends Animal
final case class Cat(name:String) extends Animal

为了能够给 Dog 添加说话方法,创建一个类型类并为 Dog 实现 speak 方法:

object Humanish{
  // 类型类,创建一个 speak 抽象方法
  trait HumanLike[A]{
    def speak(speaker:A): Unit
  }
  
  // 伴生对象
  object HumanLike{
    // 为需要的类型实现要增加的行为,这里只要为 Dog 实现
    implicit object DogIsHumanLike extends HumanLike[Dog]{
      def speak(dog:Dog){ println("I'm a dog, my name is ${dog.name}") }
    }
  }
}

定义完新的行为后,在应用中使用该功能:

object TypeClassDemo extends App{
  // 创建一个方法能够使动物说话
  def makeHumanLikeThingSpeak[A](animal:A)(implicit humanLike: HumanLike[A]){
    humanLike.speak(animal)
  }
  
  // 因为 HumanLike 中实现了 Dog 类型的方法,因此可以用于 Dog 类型
  makeHumanLikeThingSpeak(Dog("Rover"))
  
  // 但是 HumanLike 中并没有 Cat 类型的实现,因此不能用于 Cat 类型
  // makeHumanLikeThingSpeak(Cat("Mimi"))
}

这里需要注意的是:

  • 方法 makeHumanLikeThingSpeak 类似于本节开头的 add 方法
  • 因为 Numeric 类型类已经由 Scala 定义,因此可以自己用来创建自己的方法。否则,需要创建自己的类型类,这里就是 HumanLike 特质
  • 因为 speak 方法定义于 DogsIsHumanLike 中,该隐式对象继承于 HumanLike[Dog],因此只能将一个 Dog 对象传入 makeHumanLikeThingSpeak 方法,而不能是一个 Cat 对象

这里的 类(class) 概念并不是来自面向对象的世界,而是函数式编程的世界。正如上面例子中演示的,一个 类型类(type class) 的益处在于能为一个已存在的(不能再进行修改的)封闭模型添加新的行为。另一个益处在于,能够为泛型类型创建方法,并且能够控制这些泛型类型,比如只有 Dog 可以说话。

与类型构建功能

创建一个计时器

在 Unix 系统中,可以使用一下命令来查看一个执行过程花费的时间:

time find . -name "*.scala"

在 Scala 中我们可以创建一个类似的方法来查看对应执行过程消耗的时间:

val (result, time) = timer(someLongRunningAlgorithm)
println(s"result: $result, time: $time")

这个方法中,timer 方法会执行传入的someLongRunningAlgorithm方法,返回执行结果和其消耗的时间。

下面是 timer 的实现:

def timer[A](blockOfCode: => A) = {
  val startTime = System.nanoTime
  val result = blockOfCode
  val stopTime = System.nanoTime
  val delta = stopTime - startTime
  (result, delta/10000000d)
}

timer 方法使用**按名调用(call-by-name)**的语法来接收一个代码块作为一个参数。同时声明一个泛型类型最为该代码块的返回值,而不是指定声明为一个具体的类型比如 Int。这支持你传入任意类型的方法,比如:timer(println("nothing"))

创建自己的 Try 类

在 Scala 2.10 之前并没有 Try、Succeeded、Failed 这些类,如何自己实现以拥有以下的功能呢:

val x = Attempt("10".toInt)		// Succeeded(10)
val y = Attempt("10A".toInt)	// Failed(Exception)

首先需要实现一个 Attempt 类,同时为了不使用 new 关键字来创建实例,需要实现一个 apply 方法。还需要定义 Succeeded 和 Failed 类,并继承 Attempt。下面是第一个版本的实现:

sealed class Attempt[A]

object Attempt {
  def apply[A](f: => A) = {
  	try{
      val result = f
      return Succeeded(result)
    } catch {
      case e:Excaption => Failed(e)
    }
  }
}

final case class Failed[A](val exception:Throwable) extends Attempt[A]
final case class Succeeded[A](value:A) extends Attempt[A]

与上面的 timer 实现类似,apply 方法接收一个按名调用的参数,同时返回值是一个泛型类型。但是为了使这个类真正有用,还需要实现一个 getOrElse 方法来获取结果的信息,无论是 Failed 还是 Succeeded。

val x = Attempt(1/0)
val result = x.getOrElse(0)

val y = Attempt("foo".toInt).getOrElse(0)

下面我们实现这个 getOrElse 方法:

sealed abstract class Attempt[A]{
  def getOrElse[B >: A](default: => B): B = if (isSuccess) get else default
  var isSuccess = false
  def get:A
}

object Attempt{
  def apply[A](f: => A):Attempt[A] = 
    try{
  	  val result = f
  	  Succeeded(result)
	} catch {
      case e:Exception => Failed(e)
	}
}

final case class Failed[A](val exception:Thorwable) extends Attempt[A] {
  isSuccess = false
  def get:A = thorw exception
}

fianl case class Succeeded[A](result:A) extends Attempt[A]{
  isSuccess = true
  def get = result
}

这里需要注意的是方法 getOrElse 的签名:

def getOrElse[B >: A](default: => B): B = if (isSuccess) get else default

它的类型签名[B >: A]使用了下界,同时返回值的类型为 B,即该方法的返回值类型必须是 A 或 A 的父类。因为,在预期一个返回值是父类的地方可以返回一个子类,因为对父类的需求其子类都能满足,但是如果预期返回值是一个子类,但是返回一个父类,对子类要比父类的多,父类并不能满足使用需要,比如子类有个新的方法而父类中并没有,这时候返回了一个父类,再去调用该新方法时将会报错。即任何使用父类的地方都可以使用其子类替换,反之则行不通。

1.15 - Type-进阶

类型别名

type Foo = String
type IntList = List[Int]

type MyList[T] = List[T]
val MyList = List

为已有类型创建一个类型别名,或者同时为其半生对象创建一个别名。

场景:提高 API 可用性

如果你的 API 需要引入一些外部类型,比如:

import spray.http.ContentType
import org.joda.time.DateTime

final case class RiakValue(
	contentType:ContentType,
	lastModified: DateTime
)

当客户端在创建RiakVlue实例时则必须同时引入这些外部类型

import spray.http.ContentType		// <= 需要引入外部类型
import org.joda.time.DateTime

val rv = RiakValue(
	ContentType.`application/json`,
	new DateTime()
)

为了避免这些多次重复且必要的引入,我们可以为需要的类型创建别名并组织在一起:

package com.scalapenos
package object riak{
  type ContentType = spray.http.ContentType
  val ContentType = spray.http.ContentType
  
  val MediaTypes = spray.http.MediaTypes
  
  type DateTime = org.joda.time.DateTime
}

然后客户端就可以这样使用:

import com.scalapenos.rika._	// 引入所有使用 RiakValue 需要的外部类型

val rv = RiakVaule(
	ContentType.`application/json`,
	new DateTime()
)

场景:简化类型签名

有时候类型签名比较难于理解,特别是一些函数作为参数时的类型签名:

def authenticate[T](auth:RequestContext => Future[Either[Rejection, T]]) = ...

我们可以为这种复杂类型创建别名以隐藏复杂性:

package object authentication {
  type Authectication[T] = Either[Rejection, T]
  type ContextAuthenticator[T] = RequestContext => Future[Authection[T]]
}

最终得到经过简化的类型签名:

def authenticate[T](auth: ContextAutuenticator[T]) = ...

场景:任何地方都可以使用类型别名

在 Scala 标准库中的 Predef 中,为大量的常用类型定义了类型别名,以简化使用:

object Predef extends LowPriorityImplicits{
  ...
  type String        = java.lang.String
  type Class[T]      = java.lang.Class[T]
  ...
  type Function[-A, +B] = Function1[A, B]
  ...
  type Map[A, +B] = immutable.Map[A, B]
  type Set[A]     = immutable.Set[A]
  val Map         = immutable.Map
  val Set         = immutable.Set
  ...
}

class Tag & type Tag

type class

1.16 - Primitive

基本类型

Byte、Short、Int、Lont、Char称为整形类型,与 Double、Float 一起构成整个数字类型。这些都定义在scala包中。

而 String 是一个由 Char 构成的序列。

scalajava.lang会被自动引入 Scala 源文件。

字面值

所有这些基本类型都可以用字面值表示,通过字面值可以在代码中显式的定义常量。

整形字面值

整形类型,即 Byte、Short、Int、Lont、Char,有两种表示形式:十进制和十六进制,十六进制以0x0X开头。

无论以何种形式初始化整形字面值,Scala 都会以十进制打印该字面值。

# 以 16 进制初始化整形字面值
scala> val hex = 0x5 			# hex: Int = 5
scala> val hex2 = 0x00FF 		# hex2: Int = 255
scala> val magic = 0xcafebabe 	# magic: Int = -889275714

# 以 10 进制初始化整形字面值
scala> val dec1 = 31 			# dec1: Int = 31
scala> val dec2 = 255 			# dec2: Int = 255
scala> val dec3 = 20 			# dec3: Int = 20

如果一个整形字面值以字母Ll开头,则为类型Long

scala> val prog = 0XCAFEBABEL 	# prog: Long = 3405691582
scala> val tower = 35L 			# tower: Long = 35
scala> val of = 31l 			# of: Long = 31

如果一个整形字面值被赋值给一个类型为ShortByte的变量,则这个字面值为被当做ShortByte类型,并且该字面值必须在这些类型的有效取值范围内。

浮点数字面值

浮点数字面值由 10 进制数字创建,带有可选的小数点,和一个Ee及对应的指数。

scala> val big = 1.2345 		# big: Double = 1.2345
scala> val bigger = 1.2345e1 	# bigger: Double = 12.345
scala> val biggerStill = 123E45 # biggerStill: Double = 1.23E47

同时,可以用结尾字符Dd表示双精度浮点,fF表示单精度浮点。

字符字面值

字符字面量由使用单引号包围的任意 Unicode 字符构成。

scala> val a = 'A' 				# a: Char = A

同时可以使用以\u开头的 Unicode 码表示:

scala> val d = '\u0041' 		# d: Char = A

同时可以在任意位置使用 Unicode 字符:

scala> val B\u0041\u0044 = 1 	# BAD: Int = 1

字符串字面值

字符串字面值是一个由双引号包围的字符字面值序列。

同时,可以定义多行字符串:

println("""Welcome to Ultamix 3000. 
			Type "HELP" for help.""")
println("""|Welcome to Ultamix 3000. 
		   |Type "HELP" for help.""".stripMargin)

符号字面值(Symbol)

符号字面值写作'indentindent部分可以任意字符数字标示符。这样的一个标示符被被映射为scala.Symbol的一个实例,编译器会将它编译为一个工厂方法调用:Symbol("indent")

字符字面量并不能做太多操作,只能获取它的name属性:

scala> val s = 'aSymbol 		# s: Symbol = 'aSymbol
scala> val nm = s.name 			# nm: String = aSymbol

同时需要注意的是字符字面量是interned,编写一个字符字面量两次,表达式会引用同一个Symbol对象。

字符串插值

可以使用s直接在字符串字面量中引用变量进行插值:

val name = "reader" 
println(s"Hello, $name!")
  
s"The answer is ${6 * 7}." 		// res0: String = The answer is 42.

使用raw创建的字符串不会对字面值进行转义:

println(raw"No\\\\escape!") 	// prints: No\\\\escape!

使用f创建格式化字符串:

scala> f"${math.Pi}%.5f" 		// res1: String = 3.14159

操作符都是方法

基本类型的操作符实际是普通的方法调用:

// Scala invokes 1.+(2)
scala> val sum = 1 + 2 			// sum: Int = 3

Int同时包含一些重载方法来接收不同类型的参数:

// Scala invokes 1.+(2L)
scala> val longSum = 1 + 2L 	// longSum: Long = 3

所有的方法都可以作为操作符。中缀操作符(infix)接收两个运算数,一个在左边一个在右边。前缀操作符(prefix)接收一个操作数,位于操作符的右边。而后缀操作符(postfix)则是操作数位于操作符的左边。

7 + 2		// infix
-7			// prefix
7 toLong	// postfix

在前缀操作符中,会将表达式转换成对应的方法调用:

-2.0
(2.0).unary_-

可以作为前缀操作符的标示符只有+、-、!、~。因此只有使用这四种标示符类定义方法,比如unary_!,才能以!param这样的语法调用。

对象相等

==可以用于所有的对象相等性比较。该方法定义在Any包中,实际的意义为:

final def == (that: Any): Boolean = if (null eq this) {null eq that} else {this equals that}

x == that
if (x eq null) that eq null else x.equals(that)

Java 中的==可以用于比较基本类型和引用类型。基本类型时会进行值比较,这与 Scala 一致。

但是在比较引用类型时,Java 进行引用相等性比较,即两个变量是否指向 JVM 堆中的同一个对象,Scala 会使用equals进行引用类型的比较,该方法由用户定义。

而 Scala 中的引用相等性比较则使用eq方法。而 Java 中的equal仅作为引用比较。

创建比较方法

在定义equals方法时,有四种影响判等行为的陷阱

  1. equals方法签名错误
  2. 改变equals放但是没有改变hashCode方法
  3. 依据可变字段定义equals方法
  4. 没有为equals定义正确的等价关系

1、方法签名错误

现在有一个简单的类:

class Point(val x: Int, val y: Int) { ... }

现在是第一种equals方法的实现:

def equals(other: Point): Boolean = 
  this.x == other.x && this.y == other.y

进行测试:

val p1, p2 = new Point(1, 2)
val q = new Point(2, 3)
p1 equals p2		// Boolean = true
p1 equals q			// Boolean = false

看起来一切正常,但是把他们放入集合时:

val coll = mutable.HashSet(p1)
coll contains p2	// Boolean = false

虽然p1p2相等,但是contains方法却判断失败。

同时,当我们把p2赋值给一个Any类型的对象时:

val p2a: Any = p2
p1 equals p2a		// Boolean = false

比较结果任然错误。

下面是Any中的equals定义:

def equals(other: Any): Boolean

在一开始我们定义的equals方法中,参数类型设置为Point而不是Any,同时没有对Any中的方法进行重写,即使用override关键字标识。因此,这只是一个方法重载。当前,Scala 与 Java 中的重载已经通过参数的静态类型解决,但并非运行时类型。因此,当参数的静态类型为Point时会调用接收Poiont类型参数的方法,一旦参数的静态类型为Any,则会调用Any类型的方法。

因此在调用Setcontaions方法时,它会调用object类型的泛型equals方法而不是Point类型的方法。同时也是p1 equals p2a失败的原因。

下面是正确的equals定义:

override def equals(other: Any) = other match { 
	case that: Point => this.x == that.x && this.y == that.y 
	case _ => false 
}

同时以相同的签名重写==方法,因为他被定义为final

2、未重新定义 hashCode 方法

现在重复测试coll contains p2是仍然会出现错误,但并不是 100%。因为Set会以元素的 hash 值来进行比较,但是Point并未定义新的hashCode方法,仍然是原始的定义:只是对已分配对象的地址的转换。

在调用equals结果为true后会分别调用两个对象的hashCode方法并对结果进行比较。

同时,hashCode只能依赖于字段的值。下面是一个正确的定义:

class Point(val x: Int, val y: Int) { 
	override def hashCode = (x, y).## 
	override def equals(other: Any) = other match { 
		case that: Point => this.x == that.x && this.y == that.y 
		case _ => false 
	}
}

##方法是用于计算主要类型、引用类型、null的快捷方式。

3、依据可变字段定义 equals 方法

比如下面的定义,字段被定义为var而不再是val

class Point(var x: Int, var y: Int) {		// var
	override def hashCode = (x, y).## 
	override def equals(other: Any) = other match { 
		case that: Point => this.x == that.x && this.y == that.y 
		case _ => false 
	}
}

这是在通过Setcontains方法进行判断:

val p = new Point(1, 2)
val coll = collection.mutable.HashSet(p)
coll contains p				// true
// 修改 p 的字段值
p.x += 1
coll contains p				// false
coll.iterator contains p	// true

如果改变了p的字段值,将会判断失败,但是通过iterator方法发现p仍然是Set的元素。

这是因为,修改字段值后的p,其 hash 值也跟着改变,contaions方法通过 hash 值比较的结果必然失败。

4、错误的等价关系

scala.Anyequals方法约定中,指定equals方法必须为non-null对象实现正确的等价关系。

  • 反射性:non-null值 x,表达式x.equals(x)必须返回true
  • 对称性:任何non-null值 x 和 y,当且仅当x.equals(y)返回true时,y.equals(x)才会返回true
  • 传递性:任何non-null值 x、y、z,如果x.equals(y)y.equals(z)都返回true,则x.equals(z)也会返回true
  • 一致性:任何non-null值 x 和 y,多次调用x.equals(y)都会一致的返回truefalse
  • 对任何non-null值 x,x.equals(null)应该返回false

上面的Point类已经能够很好的工作,但是如果它有一个新的子类,并且新增了一个字段:

object Color extends Enumeration {
	val Red, Orange, Yellow, Green, Blue, Indigo, Violet = Value 
}

class ColoredPoint(x: Int, y: Int, val color: Color.Value)
	extends Point(x, y) {
	override def equals(other: Any) = other match { 
		case that: ColoredPoint => this.color == that.color && super.equals(that) 
		case _ => false 
	}
}

通常会以上面的方式实现。这个子类继承父类,并重写了equals方法,该方法类似父类的形式,比较新字段并利用父类的equals方法比较原有的字段。

注意当前这个例子中,并不需要重写hashCode方法,因为子类中的equals实现比父类中的实现更为严谨(它与更小范围内的对象相等),因此hashCode的契约依然有效。???

这个子类中的实现看起来没有问题,但是当他与父类混合时:

scala> val p = new Point(1, 2) 		
# p: Point = Point@5428bd62
scala> val cp = new ColoredPoint(1, 2, Color.Red) 
# cp: ColoredPoint = ColoredPoint@5428bd62

scala> p equals cp 		# res9: Boolean = true
scala> cp equals p 		# res10: Boolean = false

p equals cp会调用pequals方法,这个方法只会对对象的坐标进行比较,并返回了true

cp equals p会调用cpequals方法,因为p并不是一个ColoredPoint对象,因此返回false

因此,equals中定义的相等性并不是对称的。

canEqual

在继承类型的比较中,需要引入一个canEqual方法。这个想法是,一旦一个类重新定义了equals(或同时也冲定义了hashCode),它也必须明确指出,这类对象永远不能等于那些实现了不同判等方法的父类对象。

def canEqual(other: Any): Boolean

这个方法中,如果other对象是一个(重)定义了canEqual方法的类的实例,返回true,否则返回false。在equals方法中调用这个方法来确保将要比较的两个对象能够进行双向比较。

class Point(val x: Int, val y: Int) { 
	override def hashCode = (x, y).## 
	override def equals(other: Any) = other match { 
		case that: Point => 
			(that canEqual this) && (this.x == that.x) && (this.y == that.y) 
		case _ => false
	} 
	def canEqual(other: Any) = other.isInstanceOf[Point]	// 运行时类型相同
}

然后是子类的定义:

class ColoredPoint(x: Int, y: Int, val color: Color.Value) extends Point(x, y) {
	override def hashCode = (super.hashCode, color).## 		// 重写 hashCode
	override def equals(other: Any) = other match { 
		case that: ColoredPoint => 							// 重写 equals
		(that canEqual this) && super.equals(that) && this.color == that.color 
		case _ => false
	} 
	override def canEqual(other: Any) = other.isInstanceOf[ColoredPoint]
}

对象相等性的实现依赖于场景。当前场景中,两个不同的Point对象拥有相同的坐标即视作相等。但是两个对象拥有相同坐标,但是一个没有颜色,一个为红色,则视作不相等。

拓展:Java 中的字符串相等性比较

1.17 - Numbers

介绍

Scala 中所有的数字类型都是对象,包含 Byte, Char, Double, Float, Int, Long, Short。这 7 种数字类型都继承自 AnyVal。同时,另外的 Unit 和 Boolean 作为非数字类型。

如果需要更复杂的数字类型,可以使用 Spire 库或 ScalaLab 库。

如果需要使用时间库,可以使用 Joda 或对 Joda 的封装 nscala-time。

将 String 转换为 Int

"100".toInt
"100".toDouble
"100".toFloat
"1".toLong
"1".toShort
"1".toByte

如果将一个实际上并不能转换为数字的字符串进行转换,会抛出 NumberFormatException 错误。

如果需要在转换时使用一个基数,即转换到对应的进制:

Inter.parseInt("1",2)

或者可以创建一个隐式类和对应的方法来是转换更易于使用:

implicit class StringToInt(s:String){
  def toInt(radix:Int) = Inter.parseInt(s, radix)
}

"1".toInt(2)

数字类型之间进行转换

19.45.toInt
19.toFloat
19.toLong

使用isValid方法可以检查一个数字能否转换到另一种类型:

1000L.isValidByte	// false
1000L.isValidShort	// true

覆写默认的数字类型

在向一个变量赋值时,Scala 会自动设置数字类型,可以通过几种不同的方式来设置需要的类型:

val a = 1	// Int
val a = 1d	// Double
val a = 1f	// Float
val 1 = 1L	// Long

val a = 0:Byte	// Byte

val a:Long = 1	// Long

var b:Short = _	// 设置为默认值,并不推荐的用法
val name = null.asInstanceOf[String]

使用 ++ 和 — 使数字自增或自减

这种++的用法在 Scala 中并不支持。因为 val 对象是不可变的,而 var 对象能够使用+=-=来实现。并且,这些操作符是以方法的形式实现的。

比较浮点型数字

可以创建一个方法来设置对比浮点数需要的精度:

def ~=(x:Doubele, y:Double, precision:Double) = {
  if ((x - y).abs < precision) true else false
}

~=(0.3, 0.33333, 0.0001)

这个功能的应用场景在于:

0.1 + 0.2 = 0.30000000004		// 并不是等于 0.3

处理大数字

可以使用 BigInt 和 BigDecimal 来处理大的整数和浮点数。

生成随机数字

val r = scala.util.Random
r.nextInt
r.nextInt(100)		// 0(包含0) 到 100(不包含100) 之间
r.nextFloat
r.nextDouble
r.nextPrintableChar	// 	H

创建集合与 Range

1 to 10			// Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
1 to 10 by 2	// Range(1, 3, 5, 7, 9)
for (i <- 1 to 5) println(i)
for (i <- 1 until 5) println(i)

1 to 10 toArray
(1 to 10).toList
1 to 10 toList
(1 to 10).toArray

格式化输出浮点数

val pi = scala.math.Pi		// pi: Double = 3.141592653589793
println(f"$pi%1.5f")		// 3.14159
f"$pi%1.5f"					// 3.14159
f"$pi%1.2f"					// 3.14
f"$pi%06.2f"				// 003.14

1.18 - String

介绍

Scala 中的 String 其实是 Java 中的 String,因此可以在 Scala 中使用所有 Java 中的相关 API。同时,StringOps 中定义了很多 String 相关的隐式转换,因此可以对 String 使用很多方便的操作,或者将 String 看做是一个有**字符(character)**组成的序列来操作,使其拥有序列的所有方法。

// Predef.scala
type String        = java.lang.String

// Usage
"hello".foreach(println)
for(c <- "hello") println(c)
s.getBytes.foreach(println) // 104,101,108...
"hello world".filter(_ != 'l')

StringOps

在 Scala 中,会自动引入 Predef 对象,Predef 中定义了 String 到 StringOps 的隐式转换,根据需要,String 会被 隐式转换为 StringOps 以拥有所有有序序列的方法。

final class StringOps extends AnyVal with StringLike[String] {
  override protected[this] def thisCollection: WrappedString = new WrappedString(repr)
  override protected[this] def toCollection(repr: String): WrappedString = new WrappedString(repr)
  ...
  def seq = new WrappedString(repr)
}
new StringOps(repr: String)

WrappedString

Predef 中还定义了 String 和 WrappedString:

implicit def wrapString(s: String): WrappedString = if (s ne null) new WrappedString(s) else null
implicit def unwrapString(ws: WrappedString): String = if (ws ne null) ws.self else null

WrappedString 的实现是对 String 的包装容器,将 String 作为一个参数并提供所有有序序列的操作。

class WrappedString extends AbstractSeq[Char] with IndexedSeq[Char] with StringLike[WrappedString]
new WrappedString(self: String)

StringLike

WrappedString 与 StringOps 的不同是,当执行一些类似 filter、map 的转换操作时,该类生成的是一个 WrappedString 对象而不是 String,在 StringOps 中,间接使用了 WrappedString 的封装。二者都混入了 StringLike,区别在于前者的混入方式是 StringLike[WrappedString],后者是StringLike[String]

trait StringLike[+Repr] extends IndexedSeqOptimized[Char, Repr] with Ordered[String]

在 StringLike 特质中,实现了 String 对象的相关集合操作。

检查相等性

两个 String 的相等,实际上是检查两个字符集合的相等。

"hello" == "world"
"hello" == "hello"
"hello" == null		// ok
null == "hello"		// ok

当对一个值为 null 的 String 进行相等性检查时并不会出现空指针异常,但是当使用一个 null 调用方法时则会出现空指针异常:

val test = null
test.toUpperCace == "HELLO"	// java.lang.NullPointerException

忽略大小写的相等性检查:

a.equalsIgnoreCase(b)

Scala 中检查对象相等使用的是==而不是 Java 中的equal

x == y 实际上是:if (x eq null) x eq null else x.equals(y)

因此,使用==进行相等判断时不需要检查是否为null

创建多行 String

val foo = """This is 
a multiline 
String"""
  
val speech = """Four score and 
|seven years ago""".stripMargin

切分 String

"hello world".split(" ")
// Array(hello, world)

将变量带入 String

val name = ???
val age = ???
val weight = ???
println(s"$name is $age years old, and weighs $weight pounds.")

或者在 String 中使用表达式:

println(s"Age next year: ${age + 1}")

逐个处理 String 中的每个字符

val upper = "hello, world".map(c => c.toUpper)
val upper = "hello, world".map(_.toUpper)

同时可以使用集合的方法与字符串方法相结合:

val upper = "hello, world".filter(_ != 'l').map(_.toUpper)

模式查找

如果需要使用正则表达式来对 String 中需要的部分进行匹配,首先使用.r方法创建一个Regex对象,然后使用findFirstInfindAllIn查找第一个或所有匹配的结果。

scala> val numPattern = "[0-9]+".r 
numPattern: scala.util.matching.Regex = [0-9]+

scala> val address = "123 Main Street Suite 101"

scala> val match1 = numPattern.findFirstIn(address) 
match1: Option[String] = Some(123)

scala> val matches = numPattern.findAllIn(address) 
matches: scala.util.matching.Regex.MatchIterator = non-empty iterator

scala> matches.foreach(println)
123
101

处理匹配的结果:

match1 match{
  case Some(result) => ???
  case None =>  ???
}

val match1 = numPattern.findFirstIn(address).getOrElse("no match")

或者另一种方式创建Regex对象:

import scala.util.matching.Regex
val numPattern = new Regex("[0-9]+")

对于正则表达式的使用,可以应用一个名为“JavaVerbalExpressions”的扩展库,以更 DSL 的方式构建Regex对象。

VerbalExpression.regex()

VerbalExpression.regex().startOfLine().then("http").maybe("s")
                        .then("://")
                        .maybe("www.").anythingBut(" ")
                        .endOfLine()
                        .build();

模式替换

由于 String 是不可变的,不可以在原有的 String 上进行修改,可以创建一个新的 String 包含替换后的结果。

val address = "123 Main Street".replaceAll("[0-9]", "x")

val regex = "[0-9]".r
val newAddress = regex.replaceAllIn("123 Main Street", "x")

val result = "123".replaceFirst("[0-9]", "x")

使用正则解析多个部分

如果需要将 String 的多个匹配部分解析到不同的变量,可以使用正则表达式组

val pattern = "([0-9]+) ([A-Za-z]+)".r
val pattern(count, fruit) = "100 Bananas"

count: String = 100
fruit: String = Bananas

或者同事创建多种模式以匹配不同的预期结果:

"movies near 80301" 
"movies 80301" 
"80301 movies" 
"movie: 80301" 
"movies: 80301" 
"movies near boulder, co" 
"movies near boulder, colorado"

// match "movies 80301" 
val MoviesZipRE = "movies (\\d{5})".r

// match "movies near boulder, co" 
val MoviesNearCityStateRE = "movies near ([a-z]+), ([a-z]{2})".r

textUserTyped match { 
  case MoviesZipRE(zip) => getSearchResults(zip) 
  case MoviesNearCityStateRE(city, state) => getSearchResults(city, state) 
  case _ => println("did not match a regex") 
}

访问字符串中的字符

可以以位置索引来访问 String 中的字符:

"hello".charAt(0)
"hello"(0)
"hello".apply(1)

为 String 类添加额外的方法

如果通过给现有的 String 添加额外的方法,使 String 拥有需要的方法,而不是将 String 作为一个参数传入一个需要的方法:

"HAL".increment
// 而不是
StringUtilities.increment("HAL")

可以创建一个隐式类,然后在隐式类中添加需要的方法:

implicit class StringImprovements(s: String) {
  def increment = s.map(c => (c + 1).toChar)
}

val result = "HAL".increment

但是在真实的应用中,隐式类必须在一个 class、object、package 中定义。

在 object 中定义 隐式类

object StringUtils{
  implicit class StringImprovements(val s:String){
    def increment = s.map(c => (c + 1).toChar)
  }
}

在 package 中定义 隐式类

package object utils{
  implicit class StringImporvemrnts(val s:String){
    def increment = s.map(c => (c +1).toChar)
  }
}

使用隐式转换的方式

首先定义一个类,带有一个需要的方法,然后创建一个隐式转换,将 String 转换为这个带有目的方法的对象,就可以在 String 上调用该方法了:

class StringImprovement(val s: String){
  def increment = s.map(c => (c +1).toChar)
}

implicit def stringToStringImpr(s:String) = new StringImprovement(s)

1.19 - Control

使用 for 与 foreach 循环

如果需要迭代集合中的元素,或者操作集合中的每个元素,或者是通过已有的集合创建新集合,都可以使用 for 和 foreach 来处理。

for (elem <- collection) operationOn(elem)

或者从循环中生成值:

val newArray = for (e <- a) yield a.toUpperCase

生成的集合类型与输入的集合类型一致,比如一个 Array 会生成一个 Array。

或者通过一个计数器访问集合中的元素;

for (i <- 0 until a.length) println(s"$i is ${a(i)}")

或者使用集合提供的zipWithIndex方法,然后访问索引与元素:

for ((e, count) <- a.zipWithIndex) (s"$count is $e")

或者使用守卫了限制处理条件:

for (i <- 1 to 10 if i < 4) println(i)

或者处理一个 Map:

val names = Map("fname" -> "Robert", "lname" -> "Goren")
for ((k,v) <- names) println(s"key: $k, value: $v")

在使用 for/yield 组合时,实际上是在创建一个新的集合,如果只是使用 for 循环则不会创建新的集合。for/yield 的处理过程类似于 map 操作。还有一些别的方法,如:foreach、map、flatMap、collect、reduce等都能完成类似的工作,可以根据需求选择合适的方法。

另外,还有一些特殊的使用方式;

// 简单的处理
a.foreach(println)

// 使用匿名函数
a.foreach(e => println(e.toUpperCase))

// 使用多行的代码块
a.foreach{ e =>
  val s = e.toUpperCase
  println(s)
}

工作原理

for 循环的工作原理:

  • 一个简单的 for 循环对整个集合的迭代转换为在该集合上 foreach 方法的调用
  • 一个带有守卫的 for 循环对整个集合的迭代转换为集合上 withFilter 方法的调用后跟一个 foreach 方法调用
  • 一个 for/yield 组合表达式被转换为该集合上的 map 方法调用
  • 一个带有守卫的 for/yield 组合表达式被转换为该集合上的 withFilter 方法调用后跟 map 方法调用

通过命令scalac -Xprint:parse Main.scala可以查看 Scala 对 for 循环的具体转换过程。

在多个 for 循环中使用多个计数器

可以在 for 循环中同时使用多个计数器:

for {
  i <- 1 to 2
  j <- 1 to 2
} println(s"i = $i, j = $j")

for 循环中的<-标示符被引用为一个生成器(generator)

在 for 循环中使用守卫

有多种风格可以选择:

for (i <- 1 to 10 if i % 2 == 0) println(i)

for { 
  i <- 1 to 10
  if i % 2 == 0 
} println(i)

或者使用传统的方式:

for (file <- files){
  if (hasSoundFileExtension(file) && !soundFileIsLong(file))
  soundFiles += file
}

再或者可读性更强的方式:

for {
  file <- files
  if passFilter1(file)
  if passFilter2(file)
} doSomething(file)

因为 for 循环会被转换为一个 foreach 的方法调用,可以直接使用 withFilter 然后调用 foreach 方法,也能达到同样的效果。

for/yield 组合

for/yield 组合可以通过在已有的集合的没有元素上应用一个新的算法(或转换等)来生成一个新的集合,并且,新的集合类型与输入集合保持一致。

val names = Array("chris", "ed", "maurice")
val capNames = for (e <- names) yield e.capitalize
// Array(Chris, Ed, Maurice)

如果对每个元素的应用部分需要多行,可以使用{}来组织代码块:

val capNames = for (e <- names) yield {
  // multi lines
  e.capitalize
}

实现 break 和 continue

在 Scala 中并没有提供 break 和 continue 关键字,但是通过scala.util.control.Breaks提供了类似的功能:

import util.control.Breaks._

object BreakAndContinueDemo extends App {
  breakable{
    for (i <- 1 to 10){
      println(i)
      if (i > 4) break	// 跳出循环
	}
  }
  
  val searchMe = "peter piper picked a peck of pickled peppers"
  var numps = 0
  for (i <- 0 until seachMe.length){
    breakable{
      if (searchMe.charAt(i) != 'p'){
  		break 		// 跳出 breakable, 而外层的循环 continue
	  } else {
   		numps += 1
	  }
	}
  }
  println("Found " + numPs + " p's in the string.")
}

在 Breaks 源码的定义当中:

def break(): Nothing = { throw breakException }

def breakable(op: => Unit) {
  try {
	op
  } catch {
    case ex: BreakControl =>
      if (ex ne breakException) throw ex
  }
}

break 的调用实际是抛出一个异常,breakable 部分会对捕捉这个异常,因此也就达到了“跳出循环”的效果。

而在上面例子的第二部分,breakable 实际上是控制的 if/else 部分,当满足了 if 的条件,执行 break,否则执行 numps += 1,即在不能满足使numps +=1的条件时跳过了当前元素,从而达到 continue 效果。

通用语法

break:

breakable { for (x <- xs) { if (cond) break } }

continue:

for (x <- xs) { breakable { if (cond) break } }

有些场景需要处理嵌套的 break:

object LabledBreakDemo extends App {
  import scala.util.control._
  
  val Inner = new Breaks
  val Outer = new Breaks
  
  Outer.breakable{
    for (i <- 1 to 5){
      Inner.breakable{
  		for (j <- 'a' to 'e') {
  		  if (i == 1 && j == 'c') Inner.break else println(s"i: $i, j: $j")
  		  if (i == 2 && j == 'b') Outer.break
		}
	  }
	}
  }
}

其他方式

如果不想使用 break 这样的语法,还有其他的方式可是实现。

  • 通过在外部设置一个标记,满足条件是设定该标记,而执行时检查该标记:

    var barrelIsFull = false 
    for (monkey <- monkeyCollection if !barrelIsFull){   
      addMonkeyToBarrel(monkey) 
      barrelIsFull = checkIfBarrelIsFull
    }
    
  • 通过 return 来结束循环

    def sumToMax(arr: Array[Int], limit: Int): Int = { 
      var sum = 0 
      for (i <- arr) { 
        sum += i 
        if (sum > limit) return limit 
      } sum 
    } 
    val a = Array.range(0,10) 
    println(sumToMax(a, 10))
    

使用 if 实现三目运算符

val absValue = if (a < 0) -a else a
println(if (i == 0) "a" else "b")
hash = hash * prime + (if (name == null) 0 else name.hashCode)

if 表达式会返回一个值。

使用 match 语句

val month = i match { 
  case 1 => "January" 
  case 2 => "February" 
  case 3 => "March" 
  ...
  case _ => "Invalid month"
}

当把 match 作为一个 switch 功能使用时,推荐的做法是使用@switch注解。如果当前的用法不能被编译为一个 tableswitch 或 lookupswitch 时将发出警告:

import scala.annotation.switch
class SwitchDemo {
  val i = 1 
  val x = (i: @switch) match { 
    case 1 => "One" 
    case 2 => "Two" 
    case _ => "Other" 
  }
}

在一个 case 语句中匹配多个条件

如果有些场景中,多个不同条件都属于同一个业务逻辑,这时可以在一个 case 语句中添加多个条件,使用符号|分割,各种条件的关系为

val i = 5 
i match { 
  case 1 | 3 | 5 | 7 | 9 => println("odd") 
  case 2 | 4 | 6 | 8 | 10 => println("even") 
}

将匹配表达式的结果赋给你个变量

匹配语句的结果可以作为一个值赋值给一个变量:

val evenOrOdd = someNumber match { 
  case 1 | 3 | 5 | 7 | 9 => println("odd") 
  case 2 | 4 | 6 | 8 | 10 => println("even") 
}

访问匹配语句中默认 case 的值

如果想要访问默认 case 的值,需要使用一个变量名将其绑定,而不能使用通配符_

i match { 
  case 0 => println("1") 
  case 1 => println("2") 
  case default => println("You gave me: " + default) 
}

在匹配语句中使用模式匹配

匹配语句中可以使用多种模式,比如:常量模式、变量模式、构造器模式、序列模式、元组模式或类型模式。

def echoWhatYouGaveMe(x:Any):String = x match{
  // 常量模式
  case 0 => "zero"
  case true => "true"
  case "hello" => "hello"
  case Nil => "an empty list"
  
  // 序列模式
  case List(0,_,_) => "一个长度为3的列表,且第一个元素为0"
  case List(1,_*) => "含有多个元素的列表,且第一个元素为1"
  case Vector(1, _*) => "含有多个元素的 Vector,且第一个元素为1"
  
  // 元组模式
  case (a, b) => "匹配 2 元组模式"
  case (a,b,c) => "匹配 3 元组模式"
  
  // 构造器模式
  case Person(first, "Alexander") => "匹配一个 Person,第二个字段为 Alexander,并绑定第一个字段到变量 first 上"
  case Dog("Suka") => "匹配一个 Dog,却字段值为 Suka"
  case obj @ Some(value) => "匹配一个 Some 并取出构造器中的值,同时将整个对象绑定到变量 obj"
  
  // 类型模式
  case s:String => "String"
  case i: Int => "Int"
  ...
  case d: Dog => "匹配任何 Dog 类型,并将该对象绑定到变量 d"
  
  // 通配模式
  case _ => "匹配所有上面没有匹配到的值"
}

在匹配表达式中使用 case 类

匹配 case 类或 case 对象的多种方式,用法的选择取决于你要在 case 语句右边使用哪部分值:

trait Animal 
case class Dog(name: String) extends Animal 
case class Cat(name: String) extends Animal 
case object Woodpecker extends Animal

object CaseClassTest extends App {
  def determiType(x:Animal):String = x match {
    case Dog(moniker) => "将 name 字段的值绑定到变量 moniker"
    case _:Cat => "仅匹配所有的 Cat 类"
    case Woodpecker => "匹配 Woodpecker 对象"
    case _ => "通配"
  }
}

匹配语句中使用守卫

可以给每个单独的匹配语句添加额外的一个或多个守卫:

i match { 
  case a if 0 to 9 contains a => println("0-9 range: " + a) 
  case b if 10 to 19 contains b => println("10-19 range: " + b) 
  case c if 20 to 29 contains c => println("20-29 range: " + c) 
  case _ => println("Hmmm...") 
}

或者是将一个对象的不同条件分拆到多个 case 语句:

num match { 
  case x if x == 1 => println("one, a lonely number") 
  case x if (x == 2 || x == 3) => println(x) 
  case _ => println("some other value") 
}

使用匹配语句代替 isInstanceOf

如果需要匹配一个类型或多种不同的类型,虽然可以使用isInstanceOf来进行类型的判断,但这样并不遍历同时也不提倡这种用法:

if (x.isInstanceOf[Foo]) { do something ...

更好的方式是使用匹配语句:

trait SentientBeing 
trait Animal extends SentientBeing 
case class Dog(name: String) extends Animal 
case class Person(name: String, age: Int) extends SentientBeing

// later in the code ... 
def printInfo(x: SentientBeing) = x match { 
  case Person(name, age) => // handle the Person 
  case Dog(name) => // handle the Dog 
}

使用匹配语句处理 List

List 结构与其他的集合结构有点不同,它以常量单元开始,以 Nil 元素结束。

val x = List(1,2,3)
val y = 1 :: 2 :: 3 :: Nil

在编写递归算法时,可以利用最后一个元素为 Nil 对象的便利。比如下面的 listToString 方法,如果当前的元素不是 Nil,则继续递归调用列表剩余的部分。一旦当前元素为 Nil,停止递归调用并返回一个空字符串:

def listToString(list: List[String]): String = list match { 
  case s :: rest => s + " " + listToString(rest) 
  case Nil => "" 
}

可以用同样的方式来递归求所有元素之和:

def sum(list: List[Int]): Int = list match { 
  case Nil => 1 
  case n :: rest => n + sum(rest) 
}

或者元素之积:

def multiply(list: List[Int]): Int = list match { 
  case Nil => 1 
  case n :: rest => n * multiply(rest) 
}

注意,这些用法必须记得要处理 Nil 元素。

在 try/catch 中处理多种异常

try {
  openAndReadAFile(filename) 
} catch { 
  case e: FileNotFoundException => println("Couldn't find that file.") 
  case e: IOException => println("Had an IOException trying to read that file") 
}

或者并不关心异常的种类,可以使用一个高阶异常类型来捕获可能的异常:

try {
  openAndReadAFile("foo") 
} catch {
  case t: Throwable => t.printStackTrace() 
}

或者:

try {
  val i = s.toInt 
} catch {
  case _: Throwable => println("exception ignored") 
}

Java 中可以在 catch 部分抛出异常,但是 Scala 中没有受检异常,不必指定一个方法会抛出的异常:

def toInt(s: String): Option[Int] = try {
    Some(s.toInt) 
  } catch {
	case e: Exception => throw e 
  }

如果想要声明抛出的异常类型,或者要与 Java 集成,可以使用@throws标注方法的异常类型:

@throws(class[NumberFormatException])
def toInt(s: String): Option[Int] = try {
    Some(s.toInt) 
  } catch {
	case e: Exception => throw e 
  }

在 try/catch/finally 语句块之外声明一个变量

如果需要在 Try 语句块内使用一个变量,并且需要在最后的 finally 块中访问,比如一个资源对象需要在 finally 中关闭:

object CopyBytes extends App {

  var in = None: Option[FileInputStream] 
  var out = None: Option[FileOutputStream]

  try { 
    in = Some(new FileInputStream("/tmp/Test.class")) 
    out = Some(new FileOutputStream("/tmp/Test.class.copy")) 
    var c = 0 
    while ({c = in.get.read; c != 1}) {
      out.get.write(c) 
    }
  } catch {
	case e: IOException => e.printStackTrace 
  } finally { 
    println("entered finally ...") 
    if (in.isDefined) in.get.close 
    if (out.isDefined) out.get.close 
  }
}

或者使用更简洁的方式:

try { 
  in = Some(new FileInputStream("/tmp/Test.class")) 
  out = Some(new FileOutputStream("/tmp/Test.class.copy")) 
  in.foreach { inputStream => 
    out.foreach { outputStream => 
      var c = 0 
      while ({c = inputStream.read; c != 1}) { 
        outputStream.write(c)
      }
    }
  }
}

自定义控制结构

1.20 - Pattern Match

类比 switch

模式匹配类似于 Java 中的switch语句,但与它不同之处在于:

  • 它是一个表达式,有返回值
  • 不会落空
  • 没有任何匹配到的项则会抛出MatchError

基本语法

case pattern => result

  • case后跟一个pattern
  • pattern必须是合法的模式类型之一
  • 如果与模式匹配上了,结果会被计算并返回

通配符模式

def matchAll(any: Any): String = any match {
  case _ => "It’s a match!"
}

通配符_会对匹配所有对象,会当做一个默认的模式来使用,以避免MatchError

常量模式

def isIt8(any: Any): String = any match {
  case "8:00" => "Yes"
  case 8 => "Yes"
  case _ => "No"		// 对其他任何情况进行匹配,设置一个默认的 No
}

变量模式

def matchX(any: Any): String = any match {
  case x => s"He said $x!"
}

这里的X作为一个标示符,会对任何对象进行匹配。

变量 & 常量

import math.Pi

val pi = Pi

def m1(x: Double) = x match {
  case Pi => "Pi!"				// 大写的常量模式,会引用常量 Pi 的值进行匹配
  case _ => "not Pi!"
}

def m2(x: Double) = x match {
  case pi => "Pi!"				// 小写的变量模式,所有的对象都会进行匹配
  case _ => "not Pi!"
}

println(m1(Pi))		// Pi!
println(m1(3.14))	// not Pi!

println(m2(Pi))		// Pi!
println(m2(3.14))	// Pi!

这里需要注意的地方:

  • 如果标示符为大写,编译器会把它当做一个常量
  • 如果标示符为小写,编译器会把它当做一个变量,仅限匹配表达式内部的作用域

如果需要在匹配表达式中引用一个变量的值,可以通过一下方式:

  • 使用名称限制,比如this.pi
  • 使用引号包围变量,比如 “`pi`”
import math.Pi

val pi = Pi

def m3(x: Double) = x match {
  case this.pi => "Pi!"
  case _ => "not Pi!"
}

def m4(x: Double) = x match {
  case `pi` => "Pi!"
  case _ => "not Pi!"
}

构造器模式 - case 类

case class Time(hours: Int = 0, minutes: Int = 0)
val (noon, morn, eve) = (Time(12), Time(9), Time(20))

def mt(t: Time) = t match {
  case Time(12,_) => "twelve something"
  case _ => "not twelve"
}

嵌套构造器

case class House(street: String, number: Int)
case class Address(city: String, house: House)
case class Person(name: String, age: Int, address: Address)

val peter = Person("Peter", 33, Address("Hamburg", House("Reeperbahn", 45)))
val paul = Person("Paul", 29, Address("Berlin", House("Oranienstrasse", 64)))

def m45(p: Person) = p match {
  case Person(_, _, Address(_, House(_, 45))) => "Must be Peter!"
  case Person(_, _, Address(_, House(_, _))) => "Someone else"
}

序列模式

val l1 = List(1,2,3,4)
val l2 = List(5)
val l3 = List(5,8,6,4,9,12)

def ml(l: List[Int]) = l match {
  case List(1,_,_,_) => "starts with 1 and has 4 elements"
  case List(5, _*) => "starts with 5"
}

另一种用法

import annotation._

@tailrec
def contains5(l: List[Int]): String = l match {
  case Nil => "No"
  case 5 +: _ => "Yes"
  case _ +: tail => contains5(tail)
}

这里的符号+:实际上是一个序列的解析器,其源码中的定义为:

/** An extractor used to head/tail deconstruct sequences. */
object +: {
  def unapply[A](t: Seq[A]): Option[(A, Seq[A])] =
    if(t.isEmpty) None
    else Some(t.head -> t.tail)
}

因此上面的匹配语句实际上等同于:

@tailrec
def contains5(l: List[Int]): String = l match {
  case Nil => "No"
  case +:(5, _) => "Yes"
  case +:(_, tail) => contains5(tail)
}

解析器

  • 一个解析器是一个拥有unapply方法的 Scala 对象
  • 可以吧unapply理解为apply的反向操作
  • unapply会将需要匹配的值当做一个参数(如果这个值与unapply的参数类型一致)
  • 返回结果:
    • 没有变量时返回:Boolean
    • 一个变量时返回:Option[A]
    • 多个变量时返回:Option[TupleN[...]]
  • the returned is matched with your pattern

编写一个解析器

case class Time(hours: Int = 0, minutes: Int = 0)
val (noon, morn, eve) = (Time(12), Time(9), Time(20))

object AM {
  def unapply(t: Time): Boolean = t.hours < 12
}

def greet(t:Any) = t match {
  case AM() => "Good Morning!"	// 这里调用 AM 中的 unapply 方法,t 作为其参数传入
  case _ => "Good Afternoon!"
}

变量绑定

object AM {
  def unapply(t: Time): Option[(Int,Int)] = 
    if (t.hours < 12) Some(t.hours -> t.minutes) else None
}

def greet(t:Time) = t match {
  case AM(h,m) => f"Good Morning, it's $h%02d:$m%02d!"	// 将 t 的字段绑定到变量 h、m
  case _ => "Good Afternoon!"
}

未知数量的变量绑定

val s1 = "lightbend.com"
val s2 = "www.scala-lang.org"

object Domain {
	def unapplySeq(s: String) = Some(s.split("\\.").reverse)	// unapplySeq
}

def md(s: String) = s match {
	case Domain("com", _*) => "business"	// 将其他变量绑定到 _*
	case Domain("org", _*) => "non-profit"
}

正则表达式模式

scala.util.matching.Regex提供了一个unapplySeq方法:

val pattern = "a(b*)(c+)".r
val s1 = "abbbcc"
val s2 = "acc"
val s3 = "abb"

def mr(s: String) = s match {
  case pattern(a, bs) => s"""two groups "$a" "$bs""""
  case pattern(a, bs, cs) => s"""three groups "$a" "$bs" "$cs""""
  case _  => "no match"
}

字符串插值器(string interpolator)

implicit class TimeStringContext (val sc : StringContext) {
  object t {
    def apply (args : Any*) : String = sc.s (args : _*)

    def unapplySeq (s : String) : Option[Seq[Int]] = {
      val regexp = """(\d{1,2}):(\d{1,2})""".r
      regexp.unapplySeq(s).map(_.map(s => s.toInt))
    }
  }
}

def isTime(s: String) = s match {
  case t"$hours:$minutes" => Time(hours, minutes)
  case _ => "Not a time!"
}

类型匹配

def print[A](xs: List[A]) = xs match {
  case _: List[String] => "list of strings"
  case _: List[Int] => "list of ints"
}
import scala.reflect._
def print[A: ClassTag](xs: List[A]) = classTag[A].runtimeClass match {
  case c if c == classOf[String] => "List of strings"
  case c if c == classOf[Int]    => "List of ints"
}
def t(x:Any) = x match {
  case _ : Int => "Integer"
  case _ : String => "String"
}

多重匹配

def alt(x:Any) = x match {
  case 1 | 2 | 3 | 4 | 5 | 6 => "little"
  case 100 | 200 => "big"
}

联合类型

def talt(x:Any) = x match {
  case stringOrInt @ (_ : Int | _ : String) => 
    s"Union String | Int: $stringOrInt"
  case _ => "unknown"
}

@switch

用于检查匹配语句能否被编译为一个tableswitchlookupswitch的跳转表,如果被编译成一系列连续的条件语句,将会报错。

import annotation._ 

def wsw(x: Any): String = (x: @switch) match {
  case 8 => "Yes"
  case 9 => "No"
  case 10 => "No"
}

def wosw(x: Int): String = x match {
  case 8 => "Yes"
  case 9 => "No"
  case 10 => "No"
}

1.21 - Class Object: 基础

类、对象、方法

对象的蓝图。在类的定义中可以放置字段方法,称为成员

字段同时称为实例变量,因为每个类的实例都会有它自己的一组变量。使用val定义不可变字段,var来定义可变字段。

不可变是指,一个变量名始终只能引用一个对象,不能再次修改为引用其他的对象。

所有成员默认为public,使用private来指定私有成员。

所有方法的参数可以在方法内使用。并且这些参数为val。方法使用最后一行表达式的值作为返回值,不需要使用return语句。

分号

在 Scala 中分号通常可以省略,如果一行中有多个语句,仍可以使用它作为分隔。

对于不明显语句,比如:

x
+ y

可以使用小括号将整个语句包围,或者将操作符放在第一行的末尾:

(x
+ y)

x + 
y

语句分隔规则:

行尾通常会作为语句的结束,除非它属于以下情况:

  • 一行的末尾不是一个合法的单词,如:.、中缀操作符等
  • 下一行开头的单词不能作为一个合法的语句开始
  • 在小括号内或中括号内的行尾,因为这些符号内不能存在多行语句

单例对象

Scala 没有静态成员,但是拥有单例对象。单例对象使用关键字object定义。并且不能有参数列表,与其类拥有相同的 name,同时二者必须处于同一个文件内。而这个类就称为该半生对象的伴生类。二者可以互相访问对方的私有成员。

一个伴生对象可以作为类似 Java 中放置静态成员的地方。同时他是一个**一类(first-class)**对象。

定义一个单例对象时并不会定义一个类型(type)。比如定义一个TimeUtil的单例对象并不能创建TimeUtil类型的变量。名字为TimeUtil的类型只能通过单例对象的伴生类来定义。

单例对象可以扩展一个超类或超特质,是单例对象成为这些超类、超特质的一个实例。

单例对象会在第一次没访问时进行初始化。

与类不同名的单例对象称为独立对象(standalone object)。可以用来收集公用性质的方法作为 Scala 应用的入口点。

Scala 应用

运行一个 Scala 应用时必须提供一个独立单例对象,包括一个带有单个Array[String]类型的参数的main方法,同时返回类型为Unit

object Application {
  def main(args:Array[String]) = {
  	// excutable code here
  }
}

所有的 Scala 文件都会自动从 scala 包中自动引入Predef单例对象,其中包含一些常用的定义,比如常用类型别用、常用隐式转换等。

Scala 中类名不需要与文件名一致。

运行一个 Scala 文件中的应用:

$ scalac ChecksumAccumulator.scala Summer.scala
$ fsc ChecksumAccumulator.scala Summer.scala

App 特质

独立单例对象可以混入一个App特质,就不需要再编写main方法,来作为一个程序入口点。

object Application extends App{
  // excutable code here
}

不可变对象

使用不可变对象的优势:

  1. 不可变对象没有会随着时间改变的复杂状态,易于推理。
  2. 可以将不可变对象自由传递,如果是可变对象则需要对他们进行深拷贝
  3. 不会有多个线程并行访问并破会他们最初构造时的状态。
  4. 不可变对象拥有安全的 Hash 键。

1.22 - Class Object: 类

创建主构造器

Scala 的主构造器包括:

  • 主构造器参数
  • 类体中调用的方法
  • 类体中执行的语句和表达式
class Person(var firstName:String, var lastName:String){
  println("the constructor begins")
  
  // 字段
  private val HOME = System.getProperty("user.home")
  var age = 0
  
  // 方法
  override def toString = s"$firstName $lastName is $age years old"
  def printHome = { println(s"HOME = $HOME") }
  def printFullName = { println(this) } // uses toString
  
  // 方法调用
  printHome
  printFullName
  println("still in the constructor")
}

这个例子中,整个类体中的部分都属于构造器,包括字段、表达式执行、方法、方法调用。

在 Java 中你可以清晰的区分是否在主构造器之中,但是 Scala 模糊了这种区分。

  • 构造器参数:这里被声明为 var,表示两个字段是可变字段,类体中的 age 字段也是一样,而 HOME 字段没声明为 val,表示为不可变字段
  • 方法调用:类体中的方法调用同样属于主构造器的一部分

控制构造器字段可见性

影响字段可见性的几种因素:

  • 如果字段声明为 var,会同时生成 setter 和 getter 方法
  • 如果字段声明为 val,只会生成 getter 方法
  • 如果既没有 val 也没有 var,Scala 会以保守的方式不生产任何 setter 或 getter
  • 如果字段声明为 private,无论是 var 还是 val,都不会生成任何 setter 或 getter

而 case 类默认指定字段为 val。

定义辅助构造器

可以同时定义多个辅助构造器,以支持使用不同的方式创建类实例。定义的方式为,使用 this 方法创建辅助构造器,并且所有的辅助构造器需要拥有不同的签名(参数列表),并且,每个辅助构造器都必须调用以定义的上个构造器。

// 主构造器
class Pizza(var crustSize:Int, var crustType: String){
  // 一个参数的辅助构造器
  def this(crustSize:Int){
    this(crustSize, Pizza.DEFAULT_CRUST_TYPE)
  }
  
  // 一个参数的辅助构造器
  def this(crustType:String) {
    this(Pizza.DEFAULT_CRUST_SIZE, crustType)
  }
  
  // 无参数辅助构造器
  def this() {
    this(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
  }
  
  override def toString = s"A $crustSize inch pizza with a $crustType crust"
}

object Pizze {
  val DEFAULT_CRUST_SIZE = 12
  val DEFAULT_CRUST_TYPE = "THIN"
}

然后就可以使用不同的方式来创建实例:

val p1 = new Pizza(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE) 
val p2 = new Pizza(Pizza.DEFAULT_CRUST_SIZE) 
val p3 = new Pizza(Pizza.DEFAULT_CRUST_TYPE) 
val p4 = new Pizza

辅助构造器的使用要点:

  • 使用 this 方法定义
  • 都必须调用在前面已定义的辅助构造器
  • 都必须拥有不同的签名
  • 每个构造器使用 this 调用其他的构造器

为 case 类创建辅助构造器

case 类会比一般的类为你生成更多的模板代码,并且,case 类的构造器并不是真正的构造器,而是伴生对象中的 apply 方法,因此辅助构造器的定义方式也有所不同。

case class Pserson(var name:String, var age:Int)

// 伴生对象
object Person{
  def apply() = new Person("<no name>", 0)
  def apply(name:String) = new Person(name, 0)
}

Person()
Person("Pam")
Persion("Alex", 10)

定义私有构造器

有时候需要定义一个私有构造器,比如在实现单例模式的时候:

class Order private { ... }
class Person private (name:String) { ... }

这时只能在类的内部或半生对象中创建实例:

object Person{
  val person = new Person("Alex")
  def getInstance = person
}

some where {
  val person = Person.getInstance
}

这样就实现了单例模式。大多数时候并不需要使用私有构造器,通常只需要定义一个 object。

为构造器参数提供默认值

class Socket(val timeout:Int = 10000)

这种方式实际上是有两个构造器组成:一个单参数的主构造器,一个无参数的辅助构造器。

class Socket(val timeout: Int) {
  def this() = this(10000) 
  override def toString = s"timeout: $timeout"
}

重写默认的访问器和修改器

比如一个 Person 类:

class Person(private var name: String) { 
  // 实际上是创建了一个循环引用
  def name = name 
  def name_=(aName: String) { name = aName } 
}

这会导致编译错误,因为 Scala 已经自动生成了同名的 getter 和 setter 方法,如果再创建同名的方法,实际上是一个循环引用,并导致编译失败。遍历的方式是修改主构造器中的参数名,而在自定义的 setter 和 getter 方法中使用真正有用的参数名:

class Person(private var _name: String) { 
  def name = _name								// getter
  def name_=(aName: String) { _name = aName }	// setter
}

注意,参数必须被声明为 private,因此只能通过 setter 和 getter 方法来设置和访问字段值。

避免自动生成 setter 和 getter

Scala 会自动为主构造器参数生成 setter 和 getter 方法,如果不想生成这些方法:

class Stock {
// getter and setter methods are generated 
  var delayedPrice: Double = _

// keep this field hidden from other classes 
  private var currentPrice: Double = _
}

私有字段

如果一个字段被声明为 private,则只有类的实例能够访问该字段,或者类的实例访问该类的其他实例的这个字段:

class Stock {
  private var price: Double = _ 
  def setPrice(p: Double) { price = p } 
  def isHigher(that: Stock): Boolean = this.price > that.price 
}

object Driver extends App { 
  val s1 = new Stock 
  s1.setPrice(20) 
  val s2 = new Stock 
  s2.setPrice(100) 
  println(s2.isHigher(s1)) // s2 的 isHigher 方法访问了 s1 的私有字段 price
}

对象私有字段

如果使用private[this]来修饰字段会进一步增强该字段的私密性,被修饰的字段只能被当前对象访问,与普通的 private 不同,这种方式使相同类的不同实例也不能访问该字段。

使用代码块或函数为字段赋值

类字段可以通过一段代码块或一个函数来赋值。这些操作都属于构造器的一部分,只有在该类创建新的实例时执行。

class Foo{
  val text = { var lines = "" try {
	lines = io.Source.fromFile("/etc/passwd").getLines.mkString } catch {
	  case e: Exception => lines = "Error happened" 
	} 
	lines
  }
}

如果使用了 lazy 关键字来修饰字段,则只有该字段在第一次被访问时才会进行初始化。

设置未初始化的可变字段类型

通常的方式是使用 Option 并设置为 None,对于一些基本类型,可以设置为常用的默认值。

case class Person(var name:String, var password:String){
  var age = 0
  var firstName = ""
  var lastName = ""
  var address = None: Option[Address]
}

但是,推荐的方式是设置字段为 val 类型,并在需要的时候使用 copy 方法根据原有的对象创建一个新对象而不是修改原有对象的字段值,同时,避免使用 null 和初始值,应尽量使用 Option 并设置 None 来作为默认值。

类继承时如何处理构造器参数

在子类继承基类时,由于 Scala 已经为基类的构造器参数自动生成了 setter(var) 和 getter 方法,因此子类在声明构造器参数时,可以省略掉参数前面的 var 或 val,以避免重新自动生成的 setter 和 getter 方法。

class Person(var name:String, var age:Int){
  ...
}

class Employe(name:String, age:Int, var gender:Int) extends Person(name, age){
  ...
}

调用父类构造器

可以在子类的主构造器中调用父类构造器或不同的辅助构造器,但是不能在子类辅助构造器中调用父类构造器。

class Animal(var name:String, var age:Int){
  def this(name:String){
    this(name, 0)
  }
}

class Dog(name:String, age:Int) extends Animal(name,age){
  ...
}

class Dog(name:String) extends Animal(name, 0){
  ...
}

因为任何类的辅助构造器都要调用类自身中已定义的其他构造器,因此也就不能调用父类的构造器了。

何时使用抽象类

由于 Scala 中拥有特质,比抽象类更加轻量且支持线性扩展(允许混入多个特质,但是不能继承多个抽象类),只有很少的需求来使用抽象类:

  • 对构造器参数有需求的时候,因为特质没有构造器
  • 这部分代码会被 Java 调用

抽象类语法:

abstract class BaseController(db:Database) {
  def save { db.save }
  def update { db.update }
  
  def connect
  def getStatus:String
  def setServerName(serverName:String)
}

继承自抽象类的类要么实现所有的抽象方法,要么也声明为抽象类。

在抽象基类或特质中定义属性

可以在抽象类或特质中使用 var 或 val 来定义属性,以便在所有子类中都能访问:

  • 一个抽象的 var 字段会自动生成 setter 和 getter 字段
  • 一个抽象的 val 字段会自动生成 getter 字段
  • 定义抽象字段时,并不会在编译后的结果代码中创建这些字段,只是自动生成对应的方法,因此在子类中仍然要使用 val 或 var 来定义这些字段,但是如果抽象类中已经提供了字段的默认值,子类中就不需要再使用 var 或 val 来修饰字段,可以根据需要直接修改字段值

同时,抽象类中不应该使用 null,而应该使用 Option。

通过 case 类生成模板代码

使用 case 类会自动创建一系列模板代码:

  • 生成一个 apply 方法,因此不需要使用 new 关键字创建实例
  • 生成 getter 方法,因为 case 类的参数默认为 val,如果生命为 var,则会自动生成 setter 方法
  • 生成一个好用的 toString 方法
  • 生成一个 unapply 方法,以便能够很好的用于模式匹配
  • 生成 equals 和 hashCode 方法
  • 生成 copy 方法

定义一个 equal 方法(对象相等性)

定义一个 equal 方法用于比较实例之间的相等性:

class Person(name:String, age:Int){
  def canEqual(a:Any) = a.isInstanceOf[Person]
  
  override def equals(that:Any): Boolean = {
    that match{
      case that:Person => that.canEqual(this) && this.hashCode == that.hashCode
      case _ => false
    }
  }
  
  override def hashCode:Int = {
    val prime = 34
    var result = 1
    result = prime * result + age; 
    result = prime * result + (if (name == null) 0 else name.hashCode) 
    return result
  }
}

因为定义了 canEqual 方法,因此可以使用==来比较实例之间的相等性,与 Java 不同的是,==在 Java 中是引用的比较。

原理

Scala 文档中对任何类中 equal 方法的要求:任何该方法的实现必须是值相等性关系。必须包含以下三个属性:

  • 它是反射的:任何类型的实例 x,x.equals(x)必须返回 true
  • 它是对称的:任何类型的实例 x 和 y,当且仅当y.equals(x)返回 true 时,x.equals(y)返回 true
  • 它是传递的:任何类型的实例 x、y、z,如果x.equals(y)y.equals(z)都返回 true,x.equals(z)也必须返回 true

创建内部类

class PandorasBox { 
  case class Thing (name: String) 
  var things = new collection.mutable.ArrayBuffer[Thing]() 
  things += Thing("Evil Thing #1") 
  things += Thing("Evil Thing #2")

  def addThing(name: String) { things += new Thing(name) }
}

外部对 Thing 一无所知,只能通过 addThing 方法来添加。在 Scala 中,内部类会绑定到外部对象上,而不是一个单独的类。

1.23 - Class Object: 方法

控制方法作用域

Scala 方法默认为 public,可见性的控制方法与 Java 类似,但是提供比 Java 更细粒度更有力的控制方式:

  • 对象私有(object-private)
  • 私有(private)
  • 包(package)
  • 指定包(package-specific)
  • 公共(public)

对象私有域

只有该对象的当前实例能够访问该方法,相同类的其他实例无法访问:

private[this] def isFoo = true

私有域

该类或该类的所有实例都能访问该方法:

private def isFoo = true

保护域

只有子类能够访问该方法:

protected def breathe {}

在 Java 中,该域方法可以被当前包(package)的其他类访问,但是在 Scala 中不可以。

包域

当前包中所有成员都可以访问:

package com.acme.coolapp.model {
  class Foo { 
    private[model] def doX {} 	// 定义为包域,model 包中所有成员可以访问
    private def doY {}
  }
}

更多级别的包控制

package com.acme.coolapp.model { 
  class Foo { 
    private[model] def doX {}		// 指定到 model 级别
    private[coolapp] def doY {} 	// 指定到 coolapp 级别
    private[acme] def doZ {}		// 指定到 acme 级别
  }
}

公共域

如果没有任何作用域的声明,即为公共域。

调用父类方法

可以在子类中调用父类或特质中已存在的方法来复用代码:

class WelcomeActivity extends Activity { 
  override def onCreate(bundle: Bundle) { 
    super.onCreate(bundle) 
    // more code here ... 
  } 
}

指定调用不同特质中的方法

如果同时继承了多个特质,并且这些特质都实现了相同的方法,这时不但能指定调用方法名,还能指定调用的特质名:

trait Human {
  def hello = "the Human trait" 
}

trait Mother extends Human {
  override def hello = "Mother" 
}

trait Father extends Human {
  override def hello = "Father" 
}

class Child extends Human with Mother with Father { 
  def printSuper = super.hello 
  def printMother = super[Mother].hello 
  def printFather = super[Father].hello 
  def printHuman = super[Human].hello 
}

但是并不能跨级别的调用,比如:

trait Animal
class Pets extends Animal
class Dog extends Pets

这时 Dog 只能指定 Pets 中的方法,不能再指定 Animal 中的方法,除非显示继承了 Animal。

指定默认参数值

def makeConnection(timeout: Int = 5000, protocol: = "http") { 
  println("timeout = %d, protocol = %s".format(timeout, protocol)) 
  // more code here 
}
c.makeConnection() 			// 括号不能省略,除非方法定义中没有参数
c.makeConnection(2000) 
c.makeConnection(3000, "https")

如果方法有一个参数为默认,而其他参数并没有提供默认值:

def makeConnection(timeout: Int = 5000, protocol: String)
// error: not enough arguments for method makeConnection:
c.makeConnection("https") 

这时任何只提供一个参数值的调用都会报错,可以将定义中带有默认值的参数放在后面,然后就可以通过一个参数来调用:

def makeConnection(protocol: String, timeout: Int = 5000)
makeConnection("https")

调用时提供参数名

methodName(param1=value1, param2=value2, ...)

通过参数名提供参数时,参数顺序没有影响。

方法返回值为元组

def getStockInfo = { 
  // other code here ... 
  ("NFLX", 100.00, 10) // this is a Tuple3 
}

val (symbol, currentPrice, bidPrice) = getStockInfo
val (symbol:String, currentPrice:Double, bidPrice:Int) = getStockInfo

无括号的访问器方法调用

class Pizza { 
  // no parentheses after crustSize 
  def crustSize = 12 
}
val p = new Pizza
p.crustSize

推荐的策略是在调用没有副作用的方法时使用无括号的方式调用。

在纯的函数式编程中不存在副作用,副作用包括:

  • 写入或打印输出
  • 读取输入
  • 修改作为输入的变量的状态
  • 抛出异常,或错误发生时终止程序
  • 调用其他有副作用的函数

接收多变量参数

def printAll(strings: String*) {
  strings.foreach(println) 
}
printAll("a","b","c")
val list = List(1,2,3)
printAll(list:_*)

如果方法拥有多个参数,其中一个是多变量,则这个参数要放在参数列表的末端:

def printAll(i: Int, strings: String*)

声明一个能够抛出异常的方法

如果想要声明一个方法,该方法可能会抛出异常:

@throws(classOf[Exception]) 
override def play {
  // exception throwing code here ... 
}

@throws(classOf[IOException]) 
@throws(classOf[LineUnavailableException]) @throws(classOf[UnsupportedAudioFileException])
def playSoundFileWithJavaAudio {
  // exception throwing code here ... 
}

作用是用于提醒调用者或者与 Java 集成。

支持流式风格编程

如果想要支持调用者以流式方式调用,即方法链接,如下面的方式:

person.setFirstName("Leonard").setLastName("Nimoy")
		.setAge(82) 
		.setCity("Los Angeles") 
		.setState("California")

为了支持这种方式,需要遵循以下原则:

  • 如果你的类会被继承,指定this.type作为方法返回值类型
  • 如果确定你的类不会被继承,你可以直接在方法中返回this
class Person { 
  protected var fname = "" 
  protected var lname = "" 
  def setFirstName(firstName: String): this.type = { 
    fname = firstName 
    this 
  }
  def setLastName(lastName: String): this.type = {
    lname = lastName 
    this 
  }
}

class Employee extends Person { 
  protected var role = "" 
  def setRole(role: String): this.type = { 
    this.role = role
    this 
  } 
  override def toString = {
	"%s, %s, %s".format(fname, lname, role) 
  }
}

然后我们就可以以流式的风格调用方法:

object Main extends App { 
  val employee = new Employee 
  // use the fluent methods 
  employee.setFirstName("Al") 
  			.setLastName("Alexander") 
  			.setRole("Developer") 
  println(employee)
}

如上面的原则所述,如果确定这个类不会被继承,并不需要在 set* 类型的方法中指定this.type作为返回值类型,这种情况可以省略,只需要在方法中返回 this 的引用即可。

1.24 - Class Object: 对象

介绍

object 在 Scala 中有多种意义,可以和 Java 一样当做一个类的实例,但是 object 本身在 Scala 就是一个关键字。

对象映射

如果需要将一个类的实例从一种类型映射为另一种类型,比如动态的创建对象。可以使用 asInstanceOf 来实现这种需求:

val recognizer = cm.lookup("recognizer").asInstanceOf[Recognizer]

类似于在 Java 中:

Recognizer recognizer = (Recognizer)cm.lookup("recognizer");

该方法定于与 Any 类因此所有对象可用。需要注意的是,这种转换可能会抛出 ClassCastException 异常。

Scala 中与 Java 中 .class 等效的部分

如果有些 API 需要传入一个 Class,在 Java 中调用了一个 .class,但是在 Scala 中却不能工作。在 Scala 中等效的方法是 classOf 方法,定义于 Predef 对象,因此所有的类可用。

// java 
info = new DataLine.Info(TargetDataLine.class, null);
// scala
val info = new DataLine.Info(classOf[TargetDataLine], null)

测定对象的 Class

obj.getClass

使用 Object 启动应用

有两种方式启动一个应用,即作为一个程序的入口点:

  • 创建一个 object 并集成 App 特质
  • 创建一个 object 并实现 main 方法
object Hello extends App {
  println("Hello, world") 
}

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

两种方式中,Scala 都是以objct启动应用而不是一个类。

创建单例对象

创建单例对象即确保只有该类一个实例存在。

object CashRegister { 
  def open { println("opened") } 
  def close { println("closed") } 
}

object Main extends App { 
  CashRegister.open 
  CashRegister.close 
}

CashRegister 会以单例的形式存在,类似 Java 中的静态方法。常用语创建功能性方法。

或者用于创建复用的消息对象:

case object StartMessage
case object StopMessage
actorRef ! StartMessage

使用伴生对象创建静态成员

如果需要一个类拥有实例方法和静态方法,只需要在类中创建实例(非静态)方法,在伴生对象中创建静态方法。伴生对象即,以 object 关键字定义,与类名相同并与类处于相同源文件。

// 类定义
class Pizza (var crustType: String) {
  override def toString = "Crust type is " + crustType 
}

// 伴生对象
object Pizza { 
  val CRUST_TYPE_THIN = "thin" 
  val CRUST_TYPE_THICK = "thick" 
  def getFoo = "Foo" 
}

实现步骤:

  • 在同一源文件中定义类和 object,并且拥有相同的命名
  • 静态成员定义在 obejct 中
  • 非静态成员定义在类中

类与伴生对象可以互相访问对方的私有成员。

将通用代码放到包(package)对象

如果需要创建包级别的函数、字段或其他代码,而不需要一个类或对象,只需要将代码以 package object 的形式,放到你期望可见的包中。

比如你想要com.alvinalexander.myapp.model能够访问你的代码,只需要在com/alvinalexander/myapp/model目录中创建一个package.scala文件并进行一下定义:

package com.alvinalexander.myapp

package object model {
  // code
}

不使用 new 关键字创建对象实例

实现步骤:

  • 为类创建一个伴生对象,并以预期的构造器签名创建 apply 方法
  • 直接将类创建为 case 类
class Person {
  var name: String = _ 
}

object Person { 
  def apply(name: String): Person = { 
    var p = new Person 
    p.name = name p 
  } 
}

可以为类创建不同的 apply 方法,类似类的辅助构造器。

通过 apply 实现工厂方法

为了让子类声明创建哪种类型的对象,或者把对象的创建集中在同一个位置管理,这时候需要实现一个工厂方法。

比如创建一个 Animal 工厂,根据你提供的需要创建 Dog 或 Cat 的实例:

trait Animal {
  def speak 
}

object Animal {
  private class Dog extends Animal {
	override def speak { println("woof") } 
  }

  private class Cat extends Animal {
    override def speak { println("meow") } 
  }

  // the factory method 
  def apply(s: String): Animal = { 
    if (s == "dog") new Dog 
    else new Cat 
  }
}

val cat = Animal("cat")
val dog = Animal("dog")

1.25 - 函数式对象

函数式对象,即不拥有任何可变状态的对象的类。

构建一个分数类

比如我们要构建一个分数类,并最终能够实现下面的操作:

scala> val oneHalf = new Rational(1, 2) 
# oneHalf: Rational = 1/2 scala> 
val twoThirds = new Rational(2, 3) 
# twoThirds: Rational = 2/3 
scala> (oneHalf / 7) + (1 - twoThirds) 
# res0: Rational = 17/42

设计分数类

设计一个分数类时需要考虑客户端会如何使用该类来创建实例。同时,我们把分数类的实例设计成不可变对象,并在创建实例时提供所有需要的参数,这里指 分数和分母。

class Rational(n:Int, d:Int)

重新实现 toString

使用上面的类创建实例时:

scala> new Rational(1, 2)
res0: Rational = Rational@2591e0c9

默认情况下,一个类会继承java.String.Object中的toString实现。然而,为了更好的使用toString方法,比如日志、错误追踪等,我们需要实现一个更加详细的方法,比如包含该类的字段值。通过override来重写:

class Rational(n:Int, d:Int) {
  override def toString = n + "/" + d
}

现在就可以获得更详细的信息了:

scala> val x = new Rational(1, 3) 
x: Rational = 1/3

这里需要注意的是,Java 中的类拥有构造器,并使用构造器来接收构造参数。在 Scala 中,类可以直接接收参数,称为类参数,这些类参数能够在类体中直接使用。

如果类参数使用 val 或 var 声明,它们同时成为类的可变或不可变字段,但是如果不适用任何 var 或 val,这些类参数不会成为类的成员,只能在类内部引用。也即本例中的使用方式。

检查前提条件

事实上,分数中分母是不能为 0 的,但是我们的主构造器中没有任何处理。如果使用了 0 作为分母,后续的处理中将会出现错误。

scala> new Rational(5, 0) 
res1: Rational = 5/0

面向对象语言的一个优势就是可以讲数据封装到一个对象,因此可以在该对象整个生命周期中确保数据的状态。在一个不可变对象中,比如这里的Rational,要确保它的状态,就要求在一开始构造的时间对数据做充分的验证,因为一旦创建就不会再进行改变。因此我们可以通过require在其主构造器中定义一个前提条件

class Rational(n:Int, d:Int){
  require(d != 0)
  override def toString = n + "/" + d
}

这时,如果在构造时传入一个 0 作为分母,require则会抛出一个IllegalArgumentException异常。

加法操作

现在我们实现Rational的加法操作,实际也就是其字段的加法操作。因为它是一个不可变类,因此不能在一个Rational对象本身进行操作,而应该创建一个新的对象。

或许我们可以这样实现:

class Rational(n: Int, d: Int) { // This won't compile 
	require(d != 0) 
	override def toString = n + "/" + d 
	def add(that: Rational): Rational = 
		new Rational(n * that.d + that.n * d, d * that.d) 
}

但是当我们尝试编译时:

<console>:11: error: value d is not a member of Rational 
			new Rational(n * that.d + that.n * d, d * that.d)
								  ^

尽管类参数 n 和 d 在add方法的作用域中,但是add方法只能访问调用对象自身的值。因此,add方法中,可以访问并使用 n 和 d 的值。但是却不能使用that.nthat.d,因为that并不是add方法的调用者,只是作为add方法的参数。如果想要使用that的类参数,需要将这些参数放在字段中,以支持使用实例来引用:

class Rational(n:Int, d:Int){
  	require(d != 0) 
  	val numer:Int = n
  	val denom:Int = d
	override def toString = numer + "/" + denom
	def add(that:Rational): Rational = 
		new Rational(
			numer * that.denom + that.numer * that.denom,
			denom * that.denom
		)
}

同时需要注意的时,之前使用类参数的方式来构造对象,但是并不能在外部访问这些类参数,现在可以直接访问类的字段:

scala> r.numer 	# res3: Int = 1
scala> r.denom 	# res4: Int = 2

自引用

关键字this指向当前执行方法被调用的对象实例,或者如果使用在构造器内时,指正在被构建的对象实例。

比如添加一个lessThan方法,测试当前分数是否小于传入的参数:

def lessThan(that:Rational) = 
	this.numer * that.denom < that.numer * this.denom

这里的this指调用lessThan方法的实例对象,也可以省略不写。

再比如添加一个max方法,比较当前对象与传入参数那个更大,并返回大的那一个:

def max(that:Rational) = 
	if (this.lessThan(that)) that else this

这里的this就不能省略了。

辅助构造器

Scala 中朱构造器之外的构造器称为辅助构造器。比如创建一个分母为 1 的分数,可以实现为只需要提供一个分子,分母默认为 1:

class Rational(n:Int, d:Int){
  require(d != 0)
  
  val numer:Int = n
  val denom:Int = d
  
  def this(n:Int) = this(n, 1)	// 辅助构造器
  ....
}

辅助构造器的函数体这是对朱构造器的调用。Scala 中的每个辅助构造器都是调用当前类的其他构造器,可以是主构造器,也可以使已定义的其他辅助构造器。因此最终也都是对主构造器的调用,主构造器是类的唯一入口点

Java 中构造器能够调用同类的其他构造器或超类构造器。Scala 中只有主构造器可以调用超类构造器。

私有字段和方法

分数 66/42 并不是最简化形式,简化过程就是求最大公约数的过程,比如我们定义一个私有字段 g 表示当前分数的最大公约数,定义一个私有方法 gcd 来求最大公约数:

class Rational(n:Int, d:Int){
  ...
  private val g = gcd(n.abs, d.abs)
  val numer = n /g
  val denum = d /g
  private def gcd(a:Int, b:Int):Int = if (b ==0) a else gcd(b, a % b) // 辗转相除
  ...
}

定义操作符

使用 + 来作为求和的方法名,而不是 add。同时定义乘法操作:

class Rational(n:Int, d:Int){
  ...
  def +(that:Rational): Rational = 
  	new Rational(
  	  number * that.denom + that.numer* denom,
  	  denom * that.denom
  	)
  	
  def *(that:Rational): Rational = 
  	new Rational(numer * that.numer, denom * that.denom)
  
  ...
}

以操作符来组合调用时仍然会按照运算操作符的优先级进行。

标识符

字母数字下划线标识符,以字母数字或下划线开始,后跟字母数字下划线。$同样被当做字符,但是被保留作为编译器生成的标识符,因此不做他用。

遵循驼峰命名,避免使用下划线,特别是结尾使用下划线。

常量使用大写并用下划线分割单词。

方法重载

比如分数和整数不能直接做除法,需要首先将整数转换为分数,r * new Rational(2),这样很不美观,因此可以创建新的方法来直接接受整数来进行乘法运算:

def * (that: Rational): Rational = 
	new Rational(numer * that.numer, denom * that.denom)

def * (i: Int): Rational = new Rational(numer * i, denom)

隐式转换

但是如果先要以2 * r的方式进行运算,但是整数并没有一个接受Rational实例作为参数的方法,因此我们可以定义一个隐式转换,将整数在需要的时候自动转换为一个分数实例:

implicit def intToRational(x: Int) = new Rational(x)

1.26 - 整体类层级

Scala 中,所有的类都继承自一个共同的超类,Any。因此所有定义在Any中的方法称为通用方法,任何对象都可以调用。并且在层级的最底层定义了NullNothing,作为所有类的子类。

类层级

NAME

可以看到,顶层类型为Any,下面分为两个大类,AnyVal包含了所有值类AnyRef包含了所有引用类

值类共包含 9 种:Byte, Short, Char, Int, Long, Float, Double, Boolean, 和 Unit。前 8 中与 Java 中的原始类型一致,在运行时都变现为 Java 原始类型。Unit等价于 Java 中的void类型,表示一个方法并没有返回值,而它的值只有一个,写作()

引用类对应于 Java 中的ObjectAnyRef只是java.lang.Object的别名,因此所有 Scala 或 Java 中编写的类都是AnyRef的子类。

Any中定义了一下多个方法:

final def ==(that: Any): Boolean 
final def !=(that: Any): Boolean 
def equals(that: Any): Boolean 
def ##: Int 
def hashCode: Int 
def toString: String

因此所有对象都能够调用这些方法。

原始类的实现方式

Scala 与 Java 以同样的方式存储整数:以 32 位数字存放。这对在 JVM 上的效率以及与 Java 库的互操作性都很重要。标准的操作如加减乘除都被实现为基本操作。

但是,当整数需要被当做 Java 对象看待时,比如在整数上调用toString或将整数赋值给Any类型的变量,这时,Scala 会使用“备份”类java.lang.Integer。需要的时候,Int类型的整数能被透明转换为java.lang.Integer类型的装箱整数

比如一个 Java 程序:

boolean isEqual(int x, int y){
  return x == y;
}
System.out.println(isEqual(1,1))		// true

但是如果将参数类型改为Integer

boolean isEqual(Integer x, Integer y){
  return x == y;
}
System.out.println(isEqual(1,1))		// false

在调用isEqual时,整数 1 会被自动装箱Integer类型,而Integer为引用类型,==在比较引用类型时比较的是引用相等性,因此结果为false

但是在 Scala 中,==被设计为对类型表达透明。对于值类来说,就是自然(数学)的相等。对于引用类型,==被视为继承自Objctequals方法的别名。而这个equals方法最初始被定义为引用相等,但被许多子类重写实现为自然理念上(数据值)的相等。因此,在 Scala 中使用==来判断引用类型的相等仍然是有效的,不会落入 Java 中关于字符串比较的陷阱。

而如果真的需要进行引用相等的比较,可以直接使用AnyRef类的eq方法,它被实现为引用相等并且不能被重写。其反义比较,即引用不相等的比较,可以使用ne方法。

底层类型

类层级的底部有两个类型,scala.Nullscala.Nothing。他们是用统一的方式来处理 Scala 面向对象类型系统的某些边界情况的特殊类型。

Null类是null引用对象的类型,它是每个引用类的子类。Null不兼容值类型,比如把null赋值给值类型的变量。

Nothing类型在 Scala 类层级的最底端,它是任何其他类型的子类型。并且该类型没有任何值,它的一个用处是标明一个不正常的终止,比如scala.sys中的error方法:

def error(message: String): Nothing

调用该方法始终会抛出异常。因为error方法的返回值是Nothing类型,我们可以简便的利用该方法:

def divide(x:Int, y:Int): Int = 
  if (y != 0) x / y
  else error("can't divide by zero!")	// 返回 Nothing,是 Int 的子类型,兼容

另外,空的列表Nil被定义为List[Nothing],因为List[+A]是协变的,这使得Nil可以是任何List[T]实例,T为任意类型。

1.27 - 组合继承

类之间的两种关系:组合、继承。组合即持有另一个类的引用,借助被引用的类完成任务。继承是超类与子类的关系。

实例:二维布局库

目标是建立一个创建和渲染二维元素的库。每个元素都将显示一个由文字填充的矩形,称为 Element。提供一个工厂方法 elem 来通过传入的数据构建新元素:

elem(s:String):Element

可以对元素调用 above 和 beside 方法并传入第二个元素,来获取一个将二者合并后生成的新元素:

val column1 = elem("hello") above elem("***")
val cloumn2 = elem("***") above elem("workd")

获得的结果为:

hello *** 
 *** world

above 和 beside 可以称为组合操作符,或连接符,它们把某些区域的元素组合成新的元素。

抽象类

Element 代表布局元素类型,因为元素是二维的字符矩形,因此它包含一个 content 成员表示元素内容。内容有字符串数组表示,每个字符串代表一行:

abstract class Element{
  def contents: Array[String]
}

定义无参数方法

需要向 Element 添加显示高度和宽度的方法,height 返回 contents 的行数,也就表示高度,width 返回第一行的长度,没有元素则返回 0。

abstract class Element{
  def contents: Array[String]
  def height:Int = contents.length
  def width:Int = if (height == 0) 0 else contents(0).length
}

这三个方法都是无参方法,甚至没有空的参数列表括号。

如果方法中不需要参数,并且,方法只能通过读取所包含的对象的属性去访问可变状态(即方法本身不能改变可变状态),就使用无参方法。

这一惯例支持统一访问原则,即客户端不应由属性是通过方法实现还是通过字段实现而受影响(访问字段与调用无参方法看上去没有差别)。

如果是直接或间接的使用了可变对象,应该使用空的括号,以此来说明该调用触发了计算。

比如,可以直接将 height 方法和 width 方法改成字段实现的形式:

abstract class Element{
  def contents: Array[String]
  val height:Int = contents.length
  val width:Int = if (height == 0) 0 else contents(0).length
}

客户端不会感觉到任何差别。唯一的区别是访问字段比调用方法略快,因为字段值在类初始化的时候被预计算,而方法调用在每次调用的时候都要计算。同时,使用字段需要为每个 Element 对象分配更多的存储空间。

如果需要将字段改写为方法时,方法是由纯函数构成,即没有副作用也没有可变状态,那么客户端代码就不需要做出改变。

扩展类

为了穿件 Element 对象,我们需要实现一个子类扩展抽象类 Element 并实现其抽象方法 contents。

class ArrayElement(conts:Array[String]) extends Element{
  def contents: Array[String] = conts
}

关键字 extends 的作用:使 ArrayElement 类继承了 Element 的所有非私有成员,并成为其子类。

重写方法和字段

Scala 中的字段和方法属于相同的命名空间。字段可以重写无参数方法。比如父类中的 contents 是一个无参方法,可以在子类中重写为一个字段而不需要修改父类中的定义:

class ArrayElement(conts:Array[String]) extends Element{
  val contents: Array[String] = conts
}

同时,禁止在一个类中使用相同的名称定义方法和字段。这在 Java 中是支持的,因为 Java 提供了四个命名空间:字段、方法、类型、包。但是 Scala 中仅提供两个命名空间:

  • 值(字段、方法)
  • 类型(类、特质名)

定义参数化字段

上面 ArrayElement 类的构造器参数 conts 的实际作用是将值复制给 contents 字段,这里存在了冗余,因为 conts 实际上就是 contents,只是取了一个与 contents 类似的变量名以作区分,实际上可以使用参数化字段,而不需要再进行多余的传递:

class ArrayElement(val contents: Array[String]) extends Element

构造器中的 val,是同时定义同名的参数和字段的简写方式,同时,这个 contents 被定义为一个不可变字段,并且使用参数初始化。如果使用 var 来定义,则该字段是一个可变字段。

对于这样参数化的字段,同样可以进行重写,同时也能使用可见性修饰符:

class Cat {
  val dangerous = false
}

class Tiger(
  override val dangerous:Boolean,
  private var age:Int
) extends Cat

这个例子中,子类 Tiger 通过参数化字段的方式重写了父类中的字段 dangerous,同时定义了一个私有字段 age。或者以更完整的方式:

class Tiger(param1:Boolean, param2:Int) extends Cat(
  override val dangerous:Boolean = pararm1,
  private var age = param2
)

这两个 Tiger 的实现是等效的。

调用超类构造器

现在系统中已经有了两个类:Element 和 ArrayElement。如果客户想要创造由单行字符串构成的布局元素,我们可以实现一个子类:

class LineElement(s:String) extends ArrayElement(Array(s)) {
  override def width = s.length
  override def height = 1
}

因为子类 LineElement 要继承 ArrayElement,但是 ArrayElement 有一个参数,这时 LineElement 需要给超类的构造器传递一个参数。

需要调用超类的构造器,只需要把要传递的参数列表放在超类之后的括号里即可。

使用 override 修饰符

如果子类成员重写父类具体成员,则必须使用 override 修饰符;如果父类中是抽象成员时,可以省略;如果子类未重写或实现基类中的成员,则禁用该修饰符。

常用习惯是,重写或实现父类成员时均使用该修饰符。

多态和动态绑定

前面的例子中:

val elem:Element = new ArrayElement(Array("hello","world"))

这样将一个子类的实例赋值给一个父类的变量应用,称为多态。这种情况下,Element 可以有多种形式,现在已经定义的有 ArrayElement 和 LineElement,可以通过继承 Element 来实现更多的形式。比如,下面定义一个拥有给定长度和高度并通过提供的字符进行填充的实现:

class UniformElement(
  ch:Char,
  override val width:Int,
  override val height:Int
) extends Element{
  private val line = ch.toString *width
  def contents = Array.make(height, line)
}

现在,Element 类型的变量可以接受多种子类的实现:

val e1:Element = new ArrayElement(Array("hello","world"))
val ae:ArrayElement = new LineElement("hello")
val e2:Element = ae
val e3:Element = new UniformmElement('x',2,3)

另一方面,变量和表达式上的方法调用是动态绑定的。被调用的实际方法取决于运行期对象基于的类,而不是变量或表达式的类型。

定义 final 成员

有时需要确保一个成员不会被子类重写,这时可以使用 final 修饰符限定。

或者有时候需要确保整个类都不会有子类,也可以在类的声明上添加 final 修饰符。

使用组合与继承

组合与继承是使用其他现存类定义新类的两种方法。如果追求的是根本上的代码重用,通常推荐采用组合而不是继承。组合可以避免脆基类的问题,因为在修改基类时会在无意中破换子类。

在使用继承时需要确定,是否建模了一种 “is-a” 的关系,同时,客户端是否想把子类型当做超类来用。

实现 above、beside、toString

在 Element 中实现 above 方法,将一个元素放在另一个上面:

def above(that:Element):Element = {
  new ArrayElement(this.contents ++ that.contents)
}

实现 beside 方法,把两个元素靠在一起生成一个新元素,新元素的每一行都来自原始元素的相应行的串联(这里先假设两个元素的长度相同):

def beside(that:Element):Element = {
  val contents = new Array[String](this.contents.length)
  for(i <- 0 until this.contents.length)
  	contents(i) = this.contents(i) + that.contents.(i)
  new ArrayElement(contents)
}

或者以更简洁的方式实现:

def beside(that:Element):Element = new ArrayElement(
  for(
    (line1, line2) <- this.contents zip that.contents
  ) yiied line1 + line2
)

然后实现一个 toString 方法:

override def toString = contents.mkString("\n")

最后的 Element 实现:

abstract class Element{
  def contents:Array[String]
  def width:Int = if(height == 0) 0 else contents(0).length
  def height:Int = contents.length
  def above(that:Element):Element = {
    new ArrayElement(this.contents ++ that.contents)
  }
  def beside(that:Element):Element = new ArrayElement(
    for(
      (line1, line2) <- this.contents zip that.contents
    ) yiied line1 + line2
  )
  override def toString = contents.mkString("\n")
}

定义工厂对象

现在已经拥有了布局元素的类层级,可以将这些层级直接暴露给用户使用,或者可以把这些层级隐藏在工厂对象之后,在工厂对象中包含构建其他对象等方法,客户使用这些工厂方法构建对象而不是直接使用 new 关键字和各层级类来构建对象。

比如在伴生对象中提供工厂方法:

object Element{
  def elem(contents:Array[String]):Element = new ArrayElement(contents)
  def elem(chr:Char,width:Int,height:Int):Element = new UniformElement(chr,wirdh,height)
  def elem(line:String):Element = new LineElement(line)
}

为了能够直接使用 elem 方法而不是 Element.elem,可以直接在 Element 定义文件的头部显示引入该方法,然后对 Element 的实现进行简化:

import Element.elem

abstract class Element{
  def contents:Array[String]
  def width:Int = if(height == 0) 0 else contents(0).length
  def height:Int = contents.length
  def above(that:Element):Element = {
    elem(this.contents ++ that.contents)
  }
  def beside(that:Element):Element = elem(
    for(
      (line1, line2) <- this.contents zip that.contents
    ) yiied line1 + line2
  )
  override def toString = contents.mkString("\n")
}

既然有了工厂方法,所有的子类都可以为私有类,引文他们不再需要直接被客户端使用。可以在类和单例对象的内部定义其他的类和单例对象,因此,可以将 Element 的子类放在其单例对象中实现这些子类的私有化:

object Element{
  private class ArrayElement(val contents:Array[String]) extends Element
  private class LineElement(s:String) extends Element{
    val contents = Array(s)
    override def width = s.length
    override def height = 1
  }
  private class UniformElement(
    ch:Char,
    override val width:Int,
    override val height:Int,
  ) extends Element{
    private val line = ch.toString * width
    def contents = Array.make(height, line)
  }
  def elem(contents:Array[String]):Element = new ArrayElement(contents)
  def elem(chr:Char,width:Int,height:Int):Element = new UniformElement(chr,width,height)
  def elem(line:String):Element = new LineElement(line)
}

1.28 - Package

介绍

Scala 中会自动导入两个包:

  • java.lang._
  • scala._

这其中,包括 scala.Predef,预定义了一些常用的功能。

以大括号的方式定义包

package com.acme.store {
  class Foo { override def toString = "I am com.acme.store.Foo" } 
}
// 等同于
package com.acme.store 
class Foo { override def toString = "I am com.acme.store.Foo" }

使用这种大括号的方式可以在一个文件内定义多个包,或者嵌套的包。

引入一个或多个成员

import java.io.File
import java.io.{File, IOException, FileNotFoundException}
import java.io._
  • 可以在任意位置引入成员,类中、对象中、方法或代码块中
  • 可以引入任意成员,类、包、对象
  • 可以隐藏或重命名引入的成员

最佳实践时:除非需要引入的对象超过3个,则一般不适用通配符引入,避免不必要的冲突。

重命名引入的成员

有时候引入的成员会和当前作用域中的成员名冲突,或者需要一个更有意义的名字,这时候可以将引入的成员重命名:

import java.util.{ArrayList => JavaList}
import java.util.{Date => JDate, HashMap => JHashMap}

但是重命名之后,就不能再使用原有的成员名了。

引入时隐藏部分成员

import java.util.{Random => _, _}

这个语法会引入除 Random 之外的所有包,仅仅是把 Random 隐藏了。

或者同时隐藏多个成员,只引入剩余的其他成员:

import java.util.{List => _, Map => _, Set => _, _}

使用静态引入

如果想要以 Java 静态引入的方式引入一个成员,以便能够直接引用成员的名字:

import java.lang.Math._

然后就可以使用 Math 中的所有成员,sin(0)、cos(PI),而不再需要以Match.sin(0)的方式使用。

在任何地方引入

唯一需要注意的是,引入语句的位置必须处于使用的位置之前,否则会找不到使用的对象。

1.29 - SBT

子项目构建

基本配置文件

首先编辑project目录下的build.propertiesplugins.sbt文件:

// project/build.properties
sbt.version = 0.13.11

// project/plugins.sbt
logLevel := Level.Warn
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0")
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.1")

项目通用配置

项目配置相关的文件均位于project/路径,创建新的CommonSettings.scala,别写整个项目的基本配置,包括代码风格配置、依赖仓库配置、依赖冲突配置等:

import sbt._
import Keys._
import sbtassembly.AssemblyPlugin.autoImport._
import sbtassembly.PathList
import com.typesafe.sbt.SbtScalariform.{ScalariformKeys, scalariformSettings}
import scalariform.formatter.preferences._

object CommonSettings {
  	// 代码风格配置 
  	val customeScalariformSettings = ScalariformKeys.preferences := ScalariformKeys.preferences.value
    	.setPreference(AlignSingleLineCaseStatements, true)
    	.setPreference(AlignSingleLineCaseStatements.MaxArrowIndent, 200)
    	.setPreference(AlignParameters, true)
    	.setPreference(DoubleIndentClassDeclaration, true)
    	.setPreference(PreserveDanglingCloseParenthesis, true)
    // 基本配置与仓库
    val settings: Seq[Def.Setting[_]] = scalariformSettings ++ customeScalariformSettings ++ Seq(
      organization := "com.promisehook.bdp",
      scalaVersion := "2.11.8",
      scalacOptions := Seq("-feature", "-unchecked", "-deprecation", "-encoding", "utf8"),
      updateOptions := updateOptions.value.withCachedResolution(true),
      fork in run := true,
      test in assembly := {},
      resolvers += Opts.resolver.mavenLocalFile,
      resolvers ++= Seq(
        DefaultMavenRepository,
        Resolver.defaultLocal,
        Resolver.mavenLocal,
        Resolver.jcenterRepo,
        Classpaths.sbtPluginReleases,
        "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases",
        "Atlassian Releases" at "https://maven.atlassian.com/public/",
        "Apache Staging" at "https://repository.apache.org/content/repositories/staging/",
        "Typesafe repository" at "https://dl.bintray.com/typesafe/maven-releases/",
        "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
        "Java.net Maven2 Repository" at "http://download.java.net/maven/2/",
        "softprops-maven" at "http://dl.bintray.com/content/softprops/maven",
        "OpenIMAJ maven releases repository" at "http://maven.openimaj.org",
        "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
        "Eclipse repositories" at "https://repo.eclipse.org/service/local/repositories/egit-releases/content/"
      )
    )
    // 依赖冲突合并配置
    val commonAssemblyMergeStrategy = assemblyMergeStrategy in assembly := {
      case PathList("org", "ansj", xs @ _*)                   => MergeStrategy.first
      case PathList("org", "joda", xs @ _*)                   => MergeStrategy.first
      case PathList("org", "apache", xs @ _*)                 => MergeStrategy.first
      case PathList("org", "nlpcn", xs @ _*)                  => MergeStrategy.first
      case PathList("org", "w3c", xs @ _*)                    => MergeStrategy.first
      case PathList("org", "xml", xs @ _*)                    => MergeStrategy.first
      case PathList("javax", "xml", xs @ _*)                  => MergeStrategy.first
      case PathList("edu", "stanford", xs @ _*)               => MergeStrategy.first
      case PathList("org", "cyberneko", xs @ _*)              => MergeStrategy.first
      case PathList("org", "xmlpull", xs @ _*)              => MergeStrategy.first
      case PathList("org", "objenesis", xs @ _*)              => MergeStrategy.first
      case PathList("com", "esotericsoftware", xs @ _*)        => MergeStrategy.first
      case PathList(ps @ _*) if ps.last endsWith ".dic"       => MergeStrategy.first
      case PathList(ps @ _*) if ps.last endsWith ".data"      => MergeStrategy.first
      //  case "application.conf"                             => MergeStrategy.concat
      //  case "unwanted.txt"                                 => MergeStrategy.discard
      case x =>
        val oldStrategy = (assemblyMergeStrategy in assembly).value
        oldStrategy(x)
    }
}

子项目依赖配置

为各个子项目编写不同的依赖配置:

// project/CommonDependencies.scala
object CommonDependencies{
  val specsVersion = "3.6.6"
  val specs = Seq(
    "specs2-core", "specs2-junit", "specs2-mock").
    map("org.specs2" %% _ % specsVersion % Test)
  val jodaTime = "joda-time"  % "joda-time" % "2.8.2"
  val PlayJson = "com.typesafe.play" % "play-json_2.11" % "2.5.2"
  val commonDependencies: Seq[ModuleID] = specs ++ Seq(jodaTime, PlayJson)
}

编写主配置文件

编写build.sbt文件:

name := "Root-Project-Name"

version := "1.0"

scalaVersion := "2.11.8"

lazy val common = project.
  settings(Commons.settings: _*).
  settings(libraryDependencies ++= Dependencies.databaseDependencies).
  settings(libraryDependencies ++= Dependencies.commonDependencies)
  
lazy val webserver = project.
  dependsOn(common).
  settings(Commons.settings: _*).
  settings(libraryDependencies ++= Seq(specs2,filters,evolutions)).	// Play插件
  settings(libraryDependencies ++= Dependencies.akkaDependencies).
  settings(libraryDependencies ++= Dependencies.playDependencies).
  enablePlugins(PlayScala)
  
lazy val proserver = project.
  dependsOn(common).settings(CommonSettings.settings: _*).
  settings(libraryDependencies ++= Dependencies.akkaDependencies).
  settings(libraryDependencies ++= Dependencies.processDependencies).
  settings(CommonSettings.commonAssemblyMergeStrategy)		// 合并依赖冲突

此时,在主项目路径运行sbt -> compile即可生成子项目目录,同样,可以在各个子项目的目录中添加需要的配置。

1.30 - Predef

Predef 提供了一些定义,可以在所有 Scala 的编译单元中可见且不需要明确的限制。在所有的 Scala 代码中自动引入。

最常用的类型

提供了一些最常用类型的类型别名(alias)。比如一些不可变集合及其构造器。

控制台 I/O

提供了一些用于控制台 I/O 的函数,比如:printprintln等。这些函数都是scala.Console中提供的函数的别名。

断言

一组assert函数用于注释和动态检查代码中的常量。

在命令行中添加参数-Xdisable-assertions可以完成编译器的assert调用。

隐式转换

这里和其父类型scala.LowPriorityImplicits提供了一组最常用的隐式转换。为一些类型提供了一些扩展功能。

1.31 - I/O

原文链接:更好的Scala I/O: better-files

添加依赖

libraryDependencies += "com.github.pathikrit" %% "better-files" % version

实例化

import better.files._
import java.io.{File => JFile}
val f = File("/User/johndoe/Documents")                      // using constructor
val f1: File = file"/User/johndoe/Documents"                 // using string interpolator
val f2: File = "/User/johndoe/Documents".toFile              // convert a string path to a file
val f3: File = new JFile("/User/johndoe/Documents").toScala  // convert a Java file to Scala
val f4: File = root/"User"/"johndoe"/"Documents"             // using root helper to start from root
val f5: File = `~` / "Documents"                             // also equivalent to `home / "Documents"`
val f6: File = "/User"/"johndoe"/"Documents"                 // using file separator DSL
val f7: File = home/"Documents"/"presentations"/`..`         // Use `..` to navigate up to parent

文件读写

val file = root/"tmp"/"test.txt"
file.overwrite("hello")
file.appendLine().append("world")
assert(file.contentAsString == "hello\nworld")

或者类似 Shell 风格:

file < "hello"     // same as file.overwrite("hello")
file << "world"    // same as file.appendLines("world")
assert(file! == "hello\nworld")

或者:

"hello" `>:` file
"world" >>: file
val bytes: Array[Byte] = file.loadBytes

流式接口风格:

(root/"tmp"/"diary.txt")
 .createIfNotExists()  
 .appendLine()
 .appendLines("My name is", "Inigo Montoya")
 .moveTo(home/"Documents")
 .renameTo("princess_diary.txt")
 .changeExtensionTo(".md")
 .lines

Stream和编码

生成迭代器:

val bytes  : Iterator[Byte]            = file.bytes
val chars  : Iterator[Char]            = file.chars
val lines  : Iterator[String]          = file.lines
val source : scala.io.BufferedSource   = file.newBufferedSource // needs to be closed, unlike the above APIs which auto closes when iterator ends

编解码:

val content: String = file.contentAsString  // default codec
// custom codec:
import scala.io.Codec
file.contentAsString(Codec.ISO8859)
//or
import scala.io.Codec.string2codec
file.write("hello world")(codec = "US-ASCII")

与Java交互

转换成Java对象:

val file: File = tmp / "hello.txt"
val javaFile     : java.io.File                 = file.toJava
val uri          : java.net.uri                 = file.uri
val reader       : java.io.BufferedReader       = file.newBufferedReader 
val outputstream : java.io.OutputStream         = file.newOutputStream 
val writer       : java.io.BufferedWriter       = file.newBufferedWriter 
val inputstream  : java.io.InputStream          = file.newInputStream
val path         : java.nio.file.Path           = file.path
val fs           : java.nio.file.FileSystem     = file.fileSystem
val channel      : java.nio.channel.FileChannel = file.newFileChannel
val ram          : java.io.RandomAccessFile     = file.newRandomAccess
val fr           : java.io.FileReader           = file.newFileReader
val fw           : java.io.FileWriter           = file.newFileWriter(append = true)
val printer      : java.io.PrintWriter          = file.newPrintWriter

以及:

file1.reader > file2.writer       // pipes a reader to a writer
System.in > file2.out             // pipes an inputstream to an outputstream
src.pipeTo(sink)                  // if you don't like symbols
val bytes   : Iterator[Byte]        = inputstream.bytes
val bis     : BufferedInputStream   = inputstream.buffered  
val bos     : BufferedOutputStream  = outputstream.buffered   
val reader  : InputStreamReader     = inputstream.reader
val writer  : OutputStreamWriter    = outputstream.writer
val printer : PrintWriter           = outputstream.printWriter
val br      : BufferedReader        = reader.buffered
val bw      : BufferedWriter        = writer.buffered
val mm      : MappedByteBuffer      = fileChannel.toMappedByteBuffer

模式匹配

/**
 * @return true if file is a directory with no children or a file with no contents
 */
def isEmpty(file: File): Boolean = file match {
  case File.Type.SymbolicLink(to) => isEmpty(to)  // this must be first case statement if you want to handle symlinks specially; else will follow link
  case File.Type.Directory(files) => files.isEmpty
  case File.Type.RegularFile(content) => content.isEmpty
  case _ => file.notExists    // a file may not be one of the above e.g. UNIX pipes, sockets, devices etc
}
// or as extractors on LHS:
val File.Type.Directory(researchDocs) = home/"Downloads"/"research"

通配符

val dir = "src"/"test"
val matches: Iterator[File] = dir.glob("**/*.{java,scala}")
// above code is equivalent to:
dir.listRecursively.filter(f => f.extension == Some(".java") || f.extension == Some(".scala"))

或者使用正则表达式:

val matches = dir.glob("^\\w*$")(syntax = File.PathMatcherSyntax.regex

文件系统操作

file.touch()
file.delete()     // unlike the Java API, also works on directories as expected (deletes children recursively)
file.clear()      // If directory, deletes all children; if file clears contents
file.renameTo(newName: String)
file.moveTo(destination)
file.copyTo(destination)       // unlike the default API, also works on directories (copies recursively)
file.linkTo(destination)                     // ln file destination
file.symbolicLinkTo(destination)             // ln -s file destination
file.{checksum, md5, sha1, sha256, sha512, digest}   // also works for directories
file.setOwner(user: String)    // chown user file
file.setGroup(group: String)   // chgrp group file
Seq(file1, file2) >: file3     // same as cat file1 file2 > file3
Seq(file1, file2) >>: file3    // same as cat file1 file2 >> file3
file.isReadLocked / file.isWriteLocked / file.isLocked
File.newTemporaryDirectory() / File.newTemporaryFile() // create temp dir/file

UNIX DSL

提供了UNIX风格的操作:

import better.files_, Cmds._   // must import Cmds._ to bring in these utils
pwd / cwd     // current dir
cp(file1, file2)
mv(file1, file2)
rm(file) /*or*/ del(file)
ls(file) /*or*/ dir(file)
ln(file1, file2)     // hard link
ln_s(file1, file2)   // soft link
cat(file1)
cat(file1) >>: file
touch(file)
mkdir(file)
mkdirs(file)         // mkdir -p
chown(owner, file)
chgrp(owner, file)
chmod_+(permission, files)  // add permission
chmod_-(permission, files)  // remove permission
md5(file) / sha1(file) / sha256(file) / sha512(file)
unzip(zipFile)(targetDir)
zip(file*)(zipFile)

文件属性

file.name       // simpler than java.io.File#getName
file.extension
file.contentType
file.lastModifiedTime     // returns JSR-310 time
file.owner / file.group
file.isDirectory / file.isSymbolicLink / file.isRegularFile
file.isHidden
file.hide() / file.unhide()
file.isOwnerExecutable / file.isGroupReadable // etc. see file.permissions
file.size                 // for a directory, computes the directory size
file.posixAttributes / file.dosAttributes  // see file.attributes
file.isEmpty      // true if file has no content (or no children if directory) or does not exist
file.isParentOf / file.isChildOf / file.isSiblingOf / file.siblings

chmod操作:

import java.nio.file.attribute.PosixFilePermission
file.addPermission(PosixFilePermission.OWNER_EXECUTE)      // chmod +X file
file.removePermission(PosixFilePermission.OWNER_WRITE)     // chmod -w file
assert(file.permissionsAsString == "rw-r--r--")
// The following are all equivalent:
assert(file.permissions contains PosixFilePermission.OWNER_EXECUTE)
assert(file(PosixFilePermission.OWNER_EXECUTE))
assert(file.isOwnerExecutable)

文件比较

file1 == file2    // equivalent to `file1.isSamePathAs(file2)`
file1 === file2   // equivalent to `file1.isSameContentAs(file2)` (works for regular-files and directories)
file1 != file2    // equivalent to `!file1.isSamePathAs(file2)`
file1 =!= file2   // equivalent to `!file1.isSameContentAs(file2)`

排序操作:

val files = myDir.list.toSeq
files.sorted(File.Order.byName) 
files.max(File.Order.bySize) 
files.min(File.Order.byDepth) 
files.max(File.Order.byModificationTime) 
files.sorted(File.Order.byDirectoriesFirst)

解压缩

// Unzipping: val zipFile: File = file"path/to/research.zip" val research: File = zipFile.unzipTo(destination = home/“Documents”/“research”) // Zipping: val zipFile: File = directory.zipTo(destination = home/“Desktop”/“toEmail.zip”) // Zipping/Unzipping to temporary files/directories: val someTempZipFile: File = directory.zip() val someTempDir: File = zipFile.unzip() assert(directory === someTempDir) // Gzip handling: File(“countries.gz”).newInputStream.gzipped.lines.take(10).foreach(println)

轻量级的ARM (自动化的资源管理)

Auto-close Java closeables:

for {
  in <- file1.newInputStream.autoClosed
  out <- file2.newOutputStream.autoClosed
} in.pipeTo(out)

better-files提供了更加便利的管理,因此下面的代码:

for {
 reader <- file.newBufferedReader.autoClosed
} foo(reader)

可以改写为:

for {
 reader <- file.bufferedReader    // returns ManagedResource[BufferedReader]
} foo(reader)
// or simply:
file.bufferedReader.map(foo)

Scanner

val data = t1 << s"""
  | Hello World
  | 1 true 2 3
""".stripMargin
val scanner: Scanner = data.newScanner()
assert(scanner.next[String] == "Hello")
assert(scanner.lineNumber == 1)
assert(scanner.next[String] == "World")
assert(scanner.next[(Int, Boolean)] == (1, true))
assert(scanner.tillEndOfLine() == " 2 3")
assert(!scanner.hasNext)

或者可以写定制的Scanner。

文件监控

普通的Java文件监控:

import java.nio.file.{StandardWatchEventKinds => EventType}
val service: java.nio.file.WatchService = myDir.newWatchService
myDir.register(service, events = Seq(EventType.ENTRY_CREATE, EventType.ENTRY_DELETE))

better-files抽象了一个更加简单的接口:

val watcher = new ThreadBackedFileMonitor(myDir, recursive = true) {
  override def onCreate(file: File) = println(s"$file got created")
  override def onModify(file: File) = println(s"$file got modified")
  override def onDelete(file: File) = println(s"$file got deleted")
}
watcher.start()

或者使用下面的写法:

import java.nio.file.{Path, StandardWatchEventKinds => EventType, WatchEvent}
val watcher = new ThreadBackedFileMonitor(myDir, recursive = true) {
  override def dispatch(eventType: WatchEvent.Kind[Path], file: File) = eventType match {
    case EventType.ENTRY_CREATE => println(s"$file got created")
    case EventType.ENTRY_MODIFY => println(s"$file got modified")
    case EventType.ENTRY_DELETE => println(s"$file got deleted")
  }
}

使用Akka进行文件监控

import akka.actor.{ActorRef, ActorSystem}
import better.files._, FileWatcher._
implicit val system = ActorSystem("mySystem")
val watcher: ActorRef = (home/"Downloads").newWatcher(recursive = true)
// register partial function for an event
watcher ! on(EventType.ENTRY_DELETE) {    
  case file if file.isDirectory => println(s"$file got deleted") 
}
// watch for multiple events
watcher ! when(events = EventType.ENTRY_CREATE, EventType.ENTRY_MODIFY) {   
  case (EventType.ENTRY_CREATE, file) => println(s"$file got created")
  case (EventType.ENTRY_MODIFY, file) => println(s"$file got modified")
}

1.32 - 保留字

关键字和保留字符

// 关键字符
<-			// 用于 for 表达式,从生成器(generator)中分离元素
=>			// 用于函数类型,函数字面值和引入(import)重命名

# 保留字符
( )			// 划界表达式和参数
[ ]			// 划界类型参数
{ }			// 划界块(block)
.			// 方法调用和路径分割
// /* */	// 注释
# 			// 用于类型标记
:			// 类型归属或上下文界限(context bounds)
<: >: <%	// 上界、下界或视图(view)界限
" """		// 字符串
'			// 标示符号或字符
@			// 注解和模式匹配中的变量绑定
`			// 标示常量或使称为任意标示符
,			// 参数分割
;			// 语句分割
_*			// 可变参数展开
_			// 不同场景有多种意义

下划线

import scala._			// 通配符,引入 Scala 包中所有的资源
import scala.{ Predef => _, _ }		// 排除,除了 Predef,引入其他所有
def f[M[_]]				// 高阶类型参数
def f(m: M[_])			// 存在的类型
_ + _ 					// 匿名函数参数占位符
m _						// 将方法转换为方法的值
m(_)					// 偏函数应用
_ => 5					// 丢弃的参数
case _ =>				// 通配符,匹配任何
f(xs: _*)				// 序列 xs 作为多个参数传入函数 f(ys: T*)
case Seq(xs @ _*)		// 将标识符 xs 绑定到所有匹配到的值

通用方法

一些标识符其实是一些类、特质、对象的方法。

List(1, 2) ++ List(3, 4)	// 将右边序列的元素追加到左边序列的末尾
List(1, 2).++(List(3, 4))	// 同上
1 :: List(2, 3)				// 将一个元素放到一个序列的首部
List(2, 3).::(1)			// 同上
1 +: List(2, 3) :+ 4		// +: 绑定到右边,:+ 绑定到左边

以冒号(:)结尾的方法会绑定到右边,而不是左边,作为右边对象的一个方法。

类型和对象同样也会有象征性的名字,比如:对于有两个类型参数的类型来说,名字可以写在参数之间,Int <:< Any<:<[Int, Any]是相同的。

隐式转换提供的方法

Scala 代码会自动进行三个部分的引入:

// 顺序无关
import java.lang._
import scala._
import scala.Predef._

前两者用于类和单例对象,然而 Predef中定义了一些象征性的名字:

class <:<		// 一个 A <:< B 的实例,表示类型 A 是类型 B的子类型
class =:=		// 一个  A =:= B 的实例,表示类型 A 与类型 B 相同
object =:=
object <%< 		// removed in Scala 2.10
def ???			// 将一个方法为未实现

同时还有::,没有出现在文档中但是在注释中提到了。Predef通过隐式转换的方式激活一些方法。

语法糖和语法组合

class Example(arr: Array[Int] = Array.fill(5)(0)) {
  def apply(n: Int) = arr(n)
  def update(n: Int, v: Int) = arr(n) = v
  def a = arr(0); def a_=(v: Int) = arr(0) = v
  def b = arr(1); def b_=(v: Int) = arr(1) = v
  def c = arr(2); def c_=(v: Int) = arr(2) = v
  def d = arr(3); def d_=(v: Int) = arr(3) = v
  def e = arr(4); def e_=(v: Int) = arr(4) = v
  def +(v: Int) = new Example(arr map (_ + v))
  def unapply(n: Int) = if (arr.indices contains n) Some(arr(n)) else None
}
val ex = new Example
println(ex(0))  // means ex.apply(0)
ex(0) = 2       // means ex.update(0, 2)
ex.b = 3        // means ex.b_=(3)
val ex(c) = 2   // calls ex.unapply(2) and assigns result to c, if it's Some; throws MatchError if it's None
ex += 1         // means ex = ex + 1; if Example had a += method, it would be used instead
(_+_) // An expression, or parameter, that is an anonymous function with
      // two parameters, used exactly where the underscores appear, and
      // which calls the "+" method on the first parameter passing the
      // second parameter as argument.

1.33 - Exception

Java 中的异常

Java 中异常分为两类:受检异常、非受检异常(RuntimeException, 运行时异常)。

两种异常的处理方式:

  1. 非受检异常
    1. 捕获
    2. 抛出
    3. 不处理
  2. 受检异常(除了 RuntimeException 都是受检异常)
    1. 继续抛出,消极的方式,一致抛出到 JVM 来处理
    2. 使用try..catch块来处理

受检异常必须处理,否则不能编译通过。

异常原理

异常,即程序运行期键发生的不正常事件,它会打断指令的正常流程。异常均出现在程序运行期,编译期的问题成为语法错误。

异常的处理机制:

  1. 当程序在运行过程中出现异常,JVM 会创建一个该类型的异常对象。同时把这个异常对象交给运行时系统,即抛出异常。
  2. 运行时系统接收到一个异常时,它会在异常产生的代码上下文附近查找对应的处理方式。
  3. 异常的处理方式有两种:
    1. 捕获并处理:在抛出异常的代码附近显式使用try..catch进行处理,运行时系统捕获后会查询相应的catch处理块,在catch处理块中对异常进行处理。
    2. 查看异常发生的方法是否向上声明异常,有向上声明,向上级查询处理语句,如果没有向上声明, JVM 中断程序的运行并处理,即使用throws向外声明

异常分类

所有的错误和异常均继承自java.lang.Throwable

  1. Error:错误,JVM 内部的严重问题,无法恢复,程序人员不用处理。
  2. Exception:异常,普通的问题,通过合理的处理,程序还可以回到正常的处理流程,要求编程人员进行处理。
  3. RuntimeException:非受检异常,这类异常是编程人员的逻辑问题,即程序编写 BUG。Java 编译器不强制要求处理,因此这类异常在程序中可以处理也可以不处理。比如:算术异常、除零异常等。
  4. 非 RuntimeException:受检异常,这类异常由外部的偶然因素导致。Java 编译器强制要求处理,程序人员必须对这类异常进行处理。比如:Exception、FileNotFoundException、IOException等。

除了受检异常,都是非受检异常。

异常处理方式

try/catch

try{
  // 可能会出现异常的代码块
} catch(异常类型1 变量名1){
  // 对该类型异常的处理代码块
} catch(异常类型2 变量名2){
  // ...
} finally{
  // 无论是否发生异常都会执行的代码块
  // 常用来释放资源,比如关闭文件
}

向上声明

即使用thorws关键字,将异常向上抛出,声明一个方法可能会抛出的异常列表。

... methodName(参数列表) throws 异常类型1, 异常类型2 {
  // 方法体
}

这种方式通过声明,告诉本方法的调用者,在使用本方法时,应该对那些异常进行处理。

手动抛出

当程序逻辑不符合预期,要终止后面的代码执行时使用这种方式。

在方法的代码段中,可以使用throw关键字手动抛出一个异常。

如果手动抛出的是一个受检异常,那么本方法必须处理(应该采用向上抛出这个异常),如果是非受检异常,则处理是可选的。

自定义异常

当需要一些跟特定业务相关的异常信息类时,可以根据实际的需求,继承Exception来定义受检异常,或者继承RuntimeException来定义非受检异常

最佳实践

捕获那些已知如何处理的异常,即使用try/catch来处理已知类型的异常。

向上抛出那些不知如何处理的异常。

减少异常处理的嵌套。

Scala 中的异常

Scala 中定义所有的异常为非受检异常,即便是SQLExceptionIOException

最简单的处理方式是定义一个偏函数:

val input = new BufferReader(new FileReader(file))
try{
  for(line <- Iteratro.continually(input.readLine()).takeWhile(_ != null))
      println(line) 
} catch{
  case e:IOException => errorHandler(e)
  // case SomeOtherException(e) => ???
} finally{
  imput.close()
}

或者使用 control.Exception来组合需要处理的多个异常:

Exception.handling(classOf[RuntimeException], classOf[IOException]) by println apply {
  throw new IOException("foo")
}

更上面的最佳实践里提到的一样,不能以通配的方式捕获所有异常,这会捕获到类似内存溢出这样的异常:

try{	
  ...
} catch{
  case _ => ...	// Don't do this!
}

如果需要捕获大部分可能出现的异常,并且不是严重致命的,可以使用NonFatal

try{
  ...
} catch{
  case NonFatal(e) => println(e.getMessage)
}

NonFatal意为非致命错误,不会捕获类似VirtualMachineError这样的虚拟机错误。

object NonFatal {
   /**
    * Returns true if the provided `Throwable` is to be considered non-fatal, or false if it is to be considered fatal
    */
   def apply(t: Throwable): Boolean = t match {
     // VirtualMachineError includes OutOfMemoryError and other fatal errors
     case _: VirtualMachineError | _: ThreadDeath | _: InterruptedException | _: LinkageError | _: ControlThrowable => false
     case _ => true
   }
  /**
   * Returns Some(t) if NonFatal(t) == true, otherwise None
   */
  def unapply(t: Throwable): Option[Throwable] = if (apply(t)) Some(t) else None
}

在使用NonFatal捕获异常时定义的偏函数case NonFatal(e) => ???,这类似于模式匹配的构造器模式,会调用NonFatalunapply方法,在unapply中会对异常进行判断,即调用apply方法,如果不属于虚拟机错误则进行捕获,否则将不进行捕获。这是一种捕获异常的快捷方式。

通常还有一些为了逻辑处理而主动抛出的异常需要处理,比如assertrequireassume

Option

在处理异常时,一般讲结果置为一个Option,成功时返回Some(t),失败时返回None

Either

Either是一个封闭抽象类,表示两种可能的类型,它只有两个终极子类,LeftRright,因此Either的实例要么是一个Left要么是一个Right

类似于Option,通常也可以用于异常的处理,Option只能表示有结果或没有结果,Either则可以表示有结果时结果是什么,没有结果时“结果”又是什么,比如失败时的结果是一个异常信息等等。

sealed abstract class Either[+A, +B]
final case class Left[+A, +B](a: A) extends Either[A, B]
final case class Right[+A, +B](b: B) extends Either[A, B]

通常,Left表示失败,Right表示成功。伴生对象中提供了多种转换方法,比如toOption:

// 如果是 Left 实例
def toOption = e match {
  case Left(a) => Some(a)
  case Right(_) => None
}
// 如果是 Right 实例
def toOption = e match {
  case Left(_) => None
  case Right(b) => Some(b)
}

一个使用的实例:

case class FailResult(reason:String)

def parse(input:String) : Either[FailResult, String] = {
  val r = new StringTokenizer(input)
  if (r.countTokens() == 1) {
    Right(r.nextToken())
  } else {
    Left(FailResult("Could not parse string: " + input))
  }
}

这时如果只想要处理成功结果:

val rightFoo = for (outputFoo <- parse(input).right) yield outputFoo

或者使用fold

parse(input).fold(
  error => errorHandler(error),
  success => { ... }
)

或者模式匹配:

parse(input) match {
  case Left(le) => ???
  case Riggt(ri) => ???
}

并不限制于用在解析或验证,也可以用在业务场景:

case class UserFault
case class UserCreatedEvent

def createUser(user:User) : Either[UserFault, UserCreatedEvent]

或者二选一的时候:

def whatShape(shape:Shape) : Either[Square, Circle]

或者与Option进行嵌套,返回一个异常,或者成功时包含有值或无值两种情况:

def lookup() : Either[FooException,Option[Foo]]

这种方式比较冗余,可以直接返回一个异常或结果:

def modify(inputFoo:Foo) : Either[FooException,Foo]

**不要在 Either 中返回异常,而是创建一个 case 类来表示异常的结果。**比如:

Either[FailResult,Foo]

Try

TryEither类似,但它不像Either将一些结果类包装在LeftRight中,它会直接返回一个Failure[Throwable]Succese[T]。它是try/catch的一种简写方式,内部仍然是对NonFatal的处理。

它实现了flatMap方法,因此可以使用下面的方式,任何一个Try失败都会返回Failure

val sumTry = for {
  int1 <- Try(Integer.parseInt("1"))
  int2 <- Try(Integer.parseInt("2"))
} yield {
  int1 + int2
}

或者通过模式匹配的方式对Try的结果进行处理:

sumTry match {
  case Failure(thrown) => Console.println("Failure: " + thrown)
  case Success(s) => Console.println(s)
}

或者获取失败时的异常值:

if (sumTry.isFailure) {
  val thrown = sumTry.failed.get
}

如果是成功的结果,get方法会返回对应的值。

可以使用recover方法处理多个Try链接中任意位置的异常:

val sum = for {
  int1 <- Try(Integer.parseInt("one"))
  int2 <- Try(Integer.parseInt("two"))
} yield {
  int1 + int2
} recover {
  case e => 0
}
// or
val sum = for {
  int1 <- Try(Integer.parseInt("one")).recover { case e => 0 }
  int2 <- Try(Integer.parseInt("two"))
} yield {
  int1 + int2
}

使用toOption方法将Try[T]转换为一个Option[T]

或者与Either混合使用:

val either : Either[String, Int] = Try(Integer.parseInt("1")).transform(
  { i => Success(Right(i)) }, { e => Success(Left("FAIL")) }
).get
Console.println("either is " + either.fold(l => l, r => r))

将方法的返回值声明为 Try 可以告诉调用者该方法可能会抛出异常,可以达到受检异常的效果,即调用者必须要处理对应的异常,因此可以使代码更安全。虽然使用常规的 try/catch 也可以做到,但是这样更清晰。

与 Future 组合使用

Try 的存在意义就是为了用于 Future,参考 Future 对应的整理记录。

使用Future包装阻塞的Try代码块:

def blockMethod(x: Int): Try[Int] = Try {
    // Some long operation to get an Int from network or IO
    Thread.sleep(10000)
    100
  }

def tryToFuture[A](t: => Try[A]): Future[A] = {
    future {
      t
    }.flatMap {
      case Success(s) => Future.successful(s)
      case Failure(fail) => Future.failed(fail)
    }
  }

// Initiate long operation
val f = tryToFuture(blockMethod(1))

或者如果经常需要将FutureTry进行链接:

object FutureTryHelpers{
  implicit def tryToFuture[T](t:Try[T]):Future[T] = {
    t match{
      case Success(s) => Future.successful(s)
      case Failure(ex) => Future.failed(ex)
    }
  }
}

def someFuture:Future[String] = ???
def processResult(value:String):Try[String] = ???

import FutureTryHelpers._
val result = for{
  a <- someFuture
  b <- processResult(a)
} yield b
result.map { /* Success Block */ } recover { /* Failure Block */ }

或者使用PromsefromTry方法来构建Future

implicit def tryToFuture[T](t:Try[T]):Future[T] = Promise.fromTry(t).future

用法总结

  • 在纯函数代码中将异常抛出到单独的非预期错误
  • 使用Option返回可选的值
  • 使用Either返回预期的错误
  • 返回异常时使用Try而不是Either
  • 捕获非预期错误时使用Try而不是trt/catch
  • 在处理Future时使用Try
  • 在公共接口暴露Try类似于受检异常,直接使用异常替代

自定义异常

case class PGDBException(message:Option[String] = None, cause:Option[Throwable] = None) extends RuntimeException(PGDBException.defaultMessage(message, cause))

object PGDBException{
    def defaultMessage(message:Option[String], cause:Option[Throwable]) = {
      (message, cause) match {
        case (Some(msg), _) => msg
        case (_, Some(thr)) => thr.toString
        case _ => null
      }
    }

    def apply(message:String) = new PGDBException(Some(message), None)

    def apply(throwable: Throwable) = new PGDBException(None,Some(throwable))
  }

// usage
throw PGDBException("Already exist.")
throw PGDBException(new Throwable("this is a throwable"))

1.34 - Inject Type

Scala 注解作为元数据或额外信息添加到程序源代码。类似于注释,注解可以添加到变量、方法、表达式或任何其他的程序元素。

注解可以添加到任何类型的定义或声明上,包括:var, val, class, object, trait, def, type

可以同时添加多个注解,顺序无关。

给主构造器添加注解时需要将注解放在主构造器之前:

class Credentials @Inject() (var username: String, var password: String)

给表达式添加注解时,需要在表达式之后添加冒号,然后添加注解本身:

(myMap.get(key) : @unchecked) match {...} 

可以为类型参数添加注解:

class MyContainer[@specialized T]

只对实际类型的注解应该放在类型名之前:

String @cps[Unit] // @cps带一个类型参数 

声明一个注解的语法类似于:

@annot(exp_{1}, exp_{2}, ...)  {val name_{1}=const_{1}, ..., val name_{n}=const_{n}}

annot用于指定注解的类型,所有的注解都要包含这个部分。一些注解不需要提供参数,因此圆括号可以被省略或者提供一个空的圆括号。

传递给注解的精确参数需要依赖于注解类的实际定义。大多数注解执行器支持直接的常量,比如Hi678。关键字this可以用于在当前作用域引用的其他变量。

类似name=const这样的参数可以在比较复杂的拥有可选参数的注解中见到。这个参数是可选的,并且可以按任意顺序指定。等到右边的值建议使用一个常量。

Java 注解的参数类型只能是:数值型字面量、字符串、类字面量、枚举、其他注解,或上述类型的数组但不能是嵌套数组。

Scala 注解的参数可以是任何类型。

Scala 中的标准注解

  • scala.SerialVersionUID:为一个可序列化的类指定一个SerialVersionUID字段
  • scala.deprecated:表示这个定义已经被移除,即废弃的定义
  • scala.volatile:告诉开发者在并发程序中允许使用可变状态
  • scala.transient:标记为非持久字段
  • scala.throws:指定一个方法抛出的异常
  • scala.cloneable:标明一个类以复制(cloneable)的方式应用(apply)
  • scala.native:原生方法的标记
  • scala.inline:这个方法上的注解,请求编译器需要尽力内联这个被注解的方法
  • scala.remote:标明一个类以远程(remotable)的方式应用(apply)
  • scala.serializable:标明一个类以序列化(serializable)的方式应用(apply)
  • scala.unchecked:适用于匹配表达式中的选择器。如果存在,表达式的警告会被禁止
  • scala.reflectBeanProperty :当附加到一个字段时,根据JavaBean 的管理生成 getter 和 setter 方法

废弃注解:@deprecated

有时需要写一个类或方法,后来又不再需要。可以为类或方法添加一个提醒一面他人使用,但是为了兼容性有不能直接移除。方法或类可以使用@deprecated标记,然后在使用时会有一个提醒。

不稳定字段:@volatile

有些开发者想要在并发程序中使用可变状态,这种场景中可以使用@volatile注解,通知编译器这个变量会被多个线程使用。

二进制序列化:@serializable/@SerialVersionUID/@transient

序列化框架将对象转换为流式字节,以节省磁盘占用或网络传输。Scala 并没有自己的序列化框架。@serializable注解表示一个类是否可以被序列化。默认的,类是不支持序列化的,因此需要添加该注解。

@SerialVersionUID用于处理可序列化的类并根据时间改变,自增数值可以以@SerialVersionUID(678)的方式附上当前的版本,678 即为自增 ID。

如果一个字段被标记为@transient,框架序列化相关的对象是不会保存该字段。当该对象被重新加载时,该字段会被设置为一个默认值。

自动生成 getter/setter 方法

一个带有@scala.reflect.BeanProperty注解的字段,编译器会自动为其生成 getter 和 setter 方法。

模式匹配忽略部分用例:Unchecked

@unchecked注解通过编译器在模式匹配时解释。告诉编译器如果匹配语句遗漏了可能的 case 时不用警告。

1.35 - Type Class

《Demystifying Implicits and Typeclasses in Scala》一文的整理翻译。

The idea of typeclasses is that you provide evidence that a class satisfies an interface。

类型类思想是你提供了一个满足于一个接口证明

trait CanFoo[A] {
  def foos(x: A): String
}

case class Wrapper(wrapped: String)

object WrapperCanFoo extends CanFoo[Wrapper] {
  def foos(x: Wrapper) = x.wrapped
}

类型类思想是你提供了一个(Wrapper)满足于一个接口*(CanFoo)的证明(WrapperCanFoo)。

Wrapper不是直接的去继承一个接口,类型类让我们把类的定义和接口的实现分开。这表示,我可以为你的类实现一个接口,或者第三方可以为你的类实现我的接口,并且一切基本结束工作。

但是有一个明显的问题,如果你想把一个东西实现为CanFoo,你需要同时询问你的调用者类的实例和协议。

def foo[A](thing: A, evidence: CanFoo[A]) = evidence.foos(thing)

1.36 - Map Flatmap

Map, Map and flatMap in Scala的翻译整理,点击查看原文.

map

map操作会将集合中的每个元素作用到一个函数上:

scala> val l = List(1,2,3,4,5)

scala> l.map( x => x*2 )
res60: List[Int] = List(2, 4, 6, 8, 10)

或者有些场景,你想让这个函数返回一个序列或列表,或者一个Option:

scala> def f(x: Int):Option[Int] = if (x > 2) Some(x) else None

scala> l.map(x => f(x))
res63: List[Option[Int]] = List(None, None, Some(3), Some(4), Some(5))

flatMap

flatMap的作用是,将一个函数作用的到列表中每个序列的各个元素上,注意这里是一个嵌套的序列,然后将这些元素展开到原始的列表中.使用一个实例来解释会比较清晰:

scala> def g(v:Int) = List(v-1, v, v+1)
g: (v: Int)List[Int]

scala> l.map(x => g(x))
res64: List[List[Int]] = List(List(0, 1, 2), List(1, 2, 3), List(2, 3, 4), List(3, 4, 5), List(4, 5, 6))

scala> l.flatMap(x => g(x))
res65: List[Int] = List(0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 6)

这种操作对于处理Option类型的元素非常方便,因为Option同样是一种序列,只是可能包含一个元素或不包含元素:

scala> l.map(x => f(x))
res66: List[Option[Int]] = List(None, None, Some(3), Some(4), Some(5))

scala> l.flatMap(x => f(x))
res67: List[Int] = List(3, 4, 5)

使用 map 处理 Map

让我们看一下这些概念如何作用到Map类型上.一个Map可以通过多种方式实现,事实上他是一个包含二元组键值对的序列,这个二元组的第一个值是键,第二个值是值.

scala> val m = Map(1 -> 2, 2 -> 4, 3 -> 6)
m: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 2 -> 4, 3 -> 6)

scala> m.toList
res69: List[(Int, Int)] = List((1,2), (2,4), (3,6))

然后通过_1_2来方位元组的值:

scala> val t = (1,2)
t: (Int, Int) = (1,2)

scala> t._1
res70: Int = 1

scala> t._2
res71: Int = 2

这时如果我们要使用 map 和 flatMap 来操作Map,但是 map 操作在这看起来会没有意义,因为我们不会想去将我们的函数作用到一个元组,而是要将其作用到该元组的值.不过 map 提供了一种方式来处理Map的值,但是不包括对 key 的处理:

scala> m.mapValues(v => v*2)
res73: scala.collection.immutable.Map[Int,Int] = Map(1 -> 4, 2 -> 8, 3 -> 12)

scala> m.mapValues(v => f(v))
res74: scala.collection.immutable.Map[Int,Option[Int]] = Map(1 -> None, 2 -> Some(4), 3 -> Some(6))

使用 flatMap 处理 Map

但是在我的需求中我想要的处理效果更类似于 flatMap. flatMap 与 mapValues 处理Map的方式不同,它获取传入的元组,如果返回一个单项的List它会返回一个List; 如果返回一个元组,它会返回一个Map:

scala> m.flatMap(e => List(e._2))
res85: scala.collection.immutable.Iterable[Int] = List(2, 4, 6)

scala> m.flatMap(e => List(e))
res86: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 2 -> 4, 3 -> 6)

这样我们可以很漂亮的使用 flatMap 来处理Option,我需要过滤出所有的None,如果仅仅使用e => f(e._2)会得到所有不为None的值并组成一个List返回.但是我需要的是一个Option[Tuple2],如下所示:

scala> def h(k:Int, v:Int) = if (v > 2) Some(k->v) else None
h: (k: Int, v: Int)Option[(Int, Int)]

然后调用这个函数:

scala> m.flatMap ( e => h(e._1,e._2) )
res109: scala.collection.immutable.Map[Int,Int] = Map(2 -> 4, 3 -> 6)

这已经达到了我的要求,但是这些e._1,e._2并不是很优雅,如果有一个更好的方式将元组解包为变量就再好不过了.如果按 Python 的方式,或许在 Scala中也能工作:

scala> m.flatMap ( (k,v) => h(k,v) )
:10: error: wrong number of parameters; expected = 1

报错了,这并不符合预期.原因是unapply只能够在PartialFunction中执行,在 Scala中就是一个 case 语句,即:

scala> m.flatMap { case (k,v) => h(k,v) }
res108: scala.collection.immutable.Map[Int,Int] = Map(2 -> 4, 3 -> 6)

注意这里使用了大括号而不再是小括号了,表示这里是一个函数块而不是参数,而这个函数块是一个 case 语句.这意味着我们传给 flatMap 的函数块是一个partialFunction,并且只有与这个 case 语句匹配时才会调用,同时 case 语句中元组的unapply方法被调用,以将元组的值解析为变量.

其他方式

当然除了使用 flatMap 还有别的方式. 因为我们的目的是移除Map中所有不满足断言的元素,这里同样可以使用filter方法:

scala> m.filter( e => f(e._2) != None )
res114: scala.collection.immutable.Map[Int,Int] = Map(2 -> 4, 3 -> 6)

scala> m.filter { case (k,v) => f(v) != None }
res115: scala.collection.immutable.Map[Int,Int] = Map(2 -> 4, 3 -> 6)

scala> m.filter { case (k,v) => f(v).isDefined }
res116: scala.collection.immutable.Map[Int,Int] = Map(2 -> 4, 3 -> 6)

1.37 - 泛型型变

里氏替换原则

所有引用父类型的地方必须能透明地使用其子类型的对象。

这里面暗含了一条重要的信息:所有你想由父类完成的操作,子类都能够完成,而且子类能比父类做的更多。

概念

  • 协变(covariance)、逆变(contravariance)、不可变(invariant) 统称为型变(variance)。
  • 型变表述了:具有父子类型关系的类型经过“类型转换”后,所构造出的更复杂的对应类型之间的关系是如何变化的。
  • 协变:具有父子类型关系的类型,经过“类型转换”所构造的复杂类型之间仍然保持着相同的父子类型关系。
  • 逆变:具有父子类型关系的类型,经过“类型转换”所构造的复杂类型之间保持着与原类型之间相反的父子关系。
  • 不变:不支持协变或逆变。

型变描述的是一组类型转换规则。

公式

假设两种类型 X 和 Y,<: 表示子类型关系,>: 表示父类型关系,即 X <: Y 表示 X 是 Y 的子类。f 表示类型转换,即一个类型构造操作,通过传入一个类型来构造一个新类型。因此 X、Y 对应的构造类型分别为 f(X)f(Y)

因此,型变描述的是:基于具有父子关系的 X 和 Y,通过 f 构造而成的 f(X)f(Y)f 是如何影响 f(X)f(Y) 之间的父子关系的。

规则如下:

  • 如果 X <: Y, 经过 f 构造类型之后之后 f(X) <: f(Y), f 具有协变性。
  • 如果 X <: Y, 经过 f 构造类型之后之后 f(X) >: f(Y), f 具有逆变性。
  • 如果以上两种情况都不符合,则类型构造、或称类型转换 f 具有不可变性。

容器类型

这里的容器类型泛指集合类型,比如 Array

协变

假设以下类型:FujiApple <: Apple <: Fruit, Orange <: Fruit

根据协变规则的定义,子类型数组可以赋值给一个类型为父类型数组的变量,这里使用的 f 类型构造即为 Java 中的 Array 类型,这是一个协变类型:

Fruit[] fruits = new Apple[0];	// 1
fruits[0] = new Apple();		// 2
fruits[1] = new FujiApple();	// 3

fruits[2] = new Fruit();		// 4
fruits[3] = new Orange();		// 5

首先我们可以确认两点:

  • fruits 中取出的元素一定是 Fruit 类型;
  • 在编译期,编译器绝对允许我们将 Fruit 及其子类型的元素填充到 fruits 协变数组,因为 fruits 的类型为 Fruit[],可以包含任意 Fruit 及其子类型的元素。

从协变数组 fruits 中读取元素是绝对安全的,无论是编译期还是运行时,都不会出现任何问题。上面的操作中,fruits 实际引用的是一个 Apple 数组,即便取出的是 Apple 元素,但仍然是 Fruit 类型。

但是将 Fruit 类型或其 Fruit 类型的子类型元素填充到协变数组 fruits 时,可能会在运行时出现问题。这时 fruits 实际上引用的是一个 Apple[] 类型数组,填充 AppleApple 子类型之外的任意类型的元素都是不合法的,最终会引起 ArrayStoreException

上面的代码中,如果 fruits 是其他接口返回的数组,我们仅知道它是一个 Fruit[] 类型的数组,因此可以按照里氏替换原则向其填充 Fruit 及其子类型元素,编译器也不会抛出错误。但如果像上面的代码中的应用一样实际上引用的是一个子类型数组,则最终会引发运行时异常。

因此,在实际使用或定义协变容器类型时,总是将其当做一个只读容器是绝对安全的。

逆变

Java 实际上是不支持逆变数组的,但这里我们可以假设一下支持逆变的情况:

Fruit[] fruits = new Fruit[10]{ new Apple(), new Orange(), new FujiApple() }
Orange[] oranges = fruits;	// 应用逆变特性
oranges[0] = new Orange();	// 1
Orange orange = oranges(2);	// 2

这里我们将父类型数组 fruits 赋值给类型为子类型数组的变量 oranges,如果这样的操作理论上成立,即支持逆变,也同样会出现问题。

这里假如我们从 oranges 数组读取一个元素,因为该数组为 Orange[],则其返回的元素一定会是 Orange 类型,但实际上并非如此,它实际上引用的是一个 Fruit[] 数组,元素类型可能是 Fruit 的任何其他子类型。

因此,在实际使用或定义逆变容器类型时,总是将其当做一个只写容器是绝对安全的。

Scala:声明点型变(declaration-site variance)

trait List[+T]

trait A
trait B extends A

这时,List[A] 即为 List[B] 的父类型。

Java:使用点型变(use-site variance)

在 Java 中,实际上参数化类型是不可型变的,比如 List<String> 并非 List<Object> 的子类型,即便 String 实际是 Object 的子类型。但有些时候需要一些比类型型变更多的灵活性。

Java 中的泛型并没有对协变的内建支持,因此 Java 泛型具有不可变型性。但是,为了兼容遗留代码而被保留下来的原生类型确实可以做出这样的行为,但同时我们也非常清楚使用它就像使用数组的协变性一样意味着代码变得不再安全。所以,制定Java标准的那群人想出了个法子使得泛型像数组一样具有这些特性(Java 中的数组具有协变性),同时这种代替原生类型的方案必须是绝对安全的:他们通过给泛型增加通配符特性使得泛型在参数化后具有协变性或逆变性。

因此提供了一种方式来实现型变:

List<? extends Object> list = new ArrayList<String>();

这里,将 List<String> 赋值给 List<Object>,实际上表示了他们之间的父子类型关系。

Java 中提供的这种特性称为“类型界限通配符”:

  • ? extends T:T 表示类型通配符的上界。即由 ? 表示的未知类型参数必须是 T 的子类,或 T 本身。
  • ? super T:T 表示类型通配符的下界。即由 ? 表示的未知类型参数必须是 T 的父类,或 T 本身。

但是何时使用这两种通配符呢,在 Effective Java 中(3rd, Item-31)详细解释了 PECS 原则:

Producer use “Extends” and Consumer uses “Super”.

这里的 Extends 和 Super 对应类型边界声明符 extendssuper。为了更大的灵活性,在方法的输入参数中使用通配符类型来表示生产者或消费者。即从一个容器(集合)类型的视角出发:

  • 如果仅需要从集合中读取元素,这时集合作为一个生产者,应该在定义集合时使用 extends 来声明类型上界;
  • 如果仅需要向集合中填充元素,这时集合作为一个消费者,应该在定义集合时使用 super 来声明类型下界。
  • 如果同时需要读取和填充操作,则需要制定精确的类型,不能使用上下界来修饰类型参数。
public class Stack<E> { 
	public Stack(); 
    public void push(E e); 
    public E pop(); 
    public boolean isEmpty(); 
    
    public void pushAll(Iterable<? extends E> src) {	// 1
		for (E e : src)
			push(e); 
	}
    
    public void popAll(Collection<? super E> dst) {		// 2
		while (!isEmpty())
			dst.add(pop()); 
        }
	}
}
  1. src 作为生产者集合,所包含的元素均为 E 的子类或 E,这时可以对一个 Stack<Number> 调用 pushAll 传入一个 Iterable<Int>
  2. dst 作为消费者集合,所包含的元素均为 E 的父类或 E,这时可以对一个 Stack<Number> 调用 popAll 传入一个 Iterable<Object>

返回值与参数中的型变

这种情况的典型应用便是 Scala 中的 Function1 接口:

trait Function1[-T1, +R] extends AnyRef

在 Scala 中,-T 表示逆变,+T 表示协变,T 表示不变。

假定我们拥有以下类型:Garfield <: Cat <: Animal, Husky <: Dog <: Animal。对于 Function1[Cat, Dog] 来说,其子类必须保证两个功能:

  1. 你可以向其传入任意类型的 Cat,即 Cat 的的所有子类;
  2. 你可以使用其返回值调用 Dog 的任意方法。

逆变参数

假如参数是协变的,即 Function1[Garfield, _] <: Function1[Cat, _],根据里氏替换原则,任何需要父类的地方都可以使用一个子类来替换,这里作为父类的 Function1[Cat, _] 可以接收任意类型的 Cat,但作为子类的 Function1[Garfield, _] 仅能接收 Garfield。因此没有意义。

假如参数是逆变的,即 Function[Animal, _] <: Function[Cat, _],根据里氏替换原则,这里作为父类的 Function1[Cat, _] 可以接收任意类型的 Cat,同样作为子类的 Function[Animal, _] 也可以接收更多类型的 Cat,而且不仅仅是 Cat,甚至是 Dog (子类能做的更多)。

因此对于 A <: B 来说,要求 Function1[B, _] <: Function1[A, _]

协变返回值

假如返回值是协变的,即 Function[_, Husky] <: Function[_, Dog],根据里氏替换原则,这里作为父类的 Function[_, Dog] 可以在其返回值上调用任意 Dog 的方法,而作为子类的 Function[_, Husky] 同样可以在其返回值上调用任意 Dog 的方法,甚至还能调用 Husky 特有的方法 destoryHouse(子类能做的更多)。

假如返回值是逆变的,即 Function[_, Animal] <: Function[_, Dog],根据里氏替换原则,这里作为父类的 Function[_, Dog] 可以在其返回值上调用任意 Dog 的方法,而作为子类的 Function[_, Animal] 则可能无法执行 Dog 特有的方法,因此没有意义。

反向推导

因此,对于一个 Function1[Animal, Husky],它可以完成 Function1[Cat, Dog] 所能完成的所有工作:可以传入任意类型的 Cat, 甚至是其他 Animal,可以调用 Dog 的任意方法,甚至是 Husky 特有的方法。总的来说,Function1[Animal, Husky] <: Function1[Cat, Dog]。因此对于 Function1[Cat, Dog] 本身来说,其参数类型可以替换为父类(逆变),返回值类型可以替换为子类(协变)。

1.38 - 常见问题

根据类的不同属性进行排序-1

需要对类的集合按照不同的字段进行排序,排序的规则可以指定:

case class Item(good:String, bad:String, gross:Int, warn:String)

trait BaseSort{
  def sort[A, B: Ordering](data: List[A], desc: Boolean)(measure: A => B): List[A] = {
    val baseOrdering = Ordering.by(measure)
    val ordering = if (desc) baseOrdering.reverse else baseOrdering
    data.sorted(ordering)
  }
}

trait ItemsSort extends BaseSort{
  def sortItems(items:List[Item], by:String, desc:Boolean): List[Item] = by match {
    case "good" => sort(items, desc=desc)(_.good)
    case "bad" => sort(items, desc=desc)(_.bad)
    case "gross" => sort(items, desc=desc)(_.gross)
    case "warn" => sort(items, desc=desc)(_.warn)
    case _ => items
  }
}

val items:List[Item] = List(Item("a","a",1,"a"),Item("b","b",2,"b"),Item("c","c",3,"c"))
sortItems(items, "good",desc = true).foreach(println)

根据类的不同属性进行排序-2

另一种方式是首先预定义需要的排序规则,然后在需要的位置做为隐式参数引入:

case class Item2(id:Int, firstName:String, lastName:String)

object Item2{
  // 注意,因为`Ordering[A]`不是逆变的,如果`Item`的子类想要使用该排序方式,则必须声明为参数化类型
  implicit def orderByName[A <: Item2]: Ordering[A] =
    Ordering.by(e => (e.firstName,e.lastName))

  val orderingById: Ordering[Item2] = Ordering.by(_.id)
}

object CustomClassSort2 extends App{
  val items:List[Item2] = List(Item2(2,"ccc","ddd"),Item2(1,"aaa","bbb"))

  import Item2.orderByName						// 直接引入隐式参数
  items.sorted.foreach(println)

  implicit val ording = Item2.orderingById		// 引入排序规则后定义为隐式参数
  items.sorted.foreach(println)
}

隐式转换

将类自动转换为元组

有时候需要将一些case class自动转换为元组以方便处理:

case class Foo(a:String, b:String)
implicit def asTuple(foo:Foo):(String,String) = Foo.unapply(foo).get
val foo1 = Foo("aa","bb")
val (a:String,b:String):(String,String) = foo1

不可过度使用元组

对元组过度的使用会使代码难于理解,特别是元素特别多的元组,比如:

def bestByName(query:String, actors:List[(Int, String, Double)]) = 
  actors.filter { _._2 contains query}
		.sortBy { _._3}
		.map { _._1}
		.take(10)

而是应该讲使用频繁的元组作为一个case class,并且将各阶段的中间处理过程进行易于理解的命名:

case class Actor(id:Int, name:String, score:Double)

def bestByName(query:String, actors:List[Actor]) = {
  val candidates = actors.filter{ _.name contains query}
  val ranked = canditates.sortBy { _.score }
  val best = ranked take 10
  best map { _.id }
}

可变数据类型的选择

在一个函数或者私有类中时,如果一个可变的数据类型能够有效的缩减代码,这时就可以使用可变数据类型,比如var或者colleciton.mutable,因为没有外部的动作可以对他们造成改变。

函数重复参数传入

比如定义一个方法:

def echo(args:String*) = args.forearh(println)

该函数能够接受一个或多个 String 类型的参数,比如:

echo("aa")
echo("bb","cc")

这个String*实际上是一个Array[String],但是当传入一个已存在的序列时,需要先将其展开:

val seq:Seq[String] = Seq("aa","bb","cc")
echo(seq:_*)

这个标注告诉编译器将序列的每个元素当做一个参数,而不是将整个序列做为一个单一的参数传入。直接传入序列将会报错。

并行集合

为并行集合指定线程数

Configuring Parallel Collections

scala> import scala.collection.parallel._
scala> val pc = mutable.ParArray(1, 2, 3)
scala> pc.tasksupport = new ForkJoinTaskSupport(newscala.concurrent.forkjoin.ForkJoinPool(2))
scala> pc map { _ + 1 }
res0: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4)

异常

NoSuchMethodError

此类异常多为使用了错误版本的 JDK 或 Scala。

获取代码运行时间

val t1 = System.nanoTime

/* your code */

val duration = (System.nanoTime - t1) / 1e9d

为Future添加一个基于时间的监控器

import scala.concurrent.duration._
import java.util.concurrent.{Executors, ScheduledThreadPoolExecutor}
import scala.concurrent.{Future, Promise}

val f: Future[Int] = ???

val executor = new ScheduledThreadPoolExecutor(2, Executors.defaultThreadFactory(), AbortPolicy)

def withDelay[T](operation:  T)(by: FiniteDuration): Future[T] = {
  val promise = Promise[T]()
  executor.schedule(new Runnable {
    override def run() = {
      promise.complete(Try(operation))
    }
  }, by.length, by.unit)
  promise.future
}

Future.firstCompletedOf(Seq(f, withDelay(println("still going"))(30 seconds)))
Future.firstCompletedOf(Seq(f, withDelay(println("still still going"))(60 seconds)))

logback 避免日志重复

<logger name="data-logger" level="info" additivity="false">

Loggers are hierarchical, and any message sent to a logger will be sent to all its ancestors by default. You can disable this behavior by setting additivity=false.

OkHttp异步请求

BUG:大量请求之后资源耗尽导致服务不可用,虽然已经关闭了response

import java.util.concurrent.TimeUnit
import okhttp3._

trait OkHttpBuilder {

  val httpClient: OkHttpClient = new OkHttpClient().newBuilder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .writeTimeout(10, TimeUnit.SECONDS)
    .readTimeout(45, TimeUnit.SECONDS)
    .retryOnConnectionFailure(true)
    .followSslRedirects(true)
    .followRedirects(true)
    .build()

  def cloneHttpClient() = httpClient.newBuilder().build()
}

object OkHttpBuilder extends OkHttpBuilder
private def asyncDownload(src: String, superior: ActorRef) = {
    try {
      val client: OkHttpClient = OkHttpBuilder.cloneHttpClient()
      val request = new Request.Builder().url(src).get().addHeader("User-Agent", web).build()
      client.newCall(request).enqueue(new Callback {
        override def onFailure(call: Call, e: IOException): Unit = {
          logger.error(s"ImageProcessor.DownloadErr - 1: $src, ${e.getMessage}")
          superior ! Redownload(src)
        }
        override def onResponse(call: Call, response: Response): Unit = {
          response.isSuccessful match {
            case false =>
              logger.error(s"ImageProcessor.DownloadErr - 2: $src, ${response.code()}")
              superior ! Discard(src)
              response.close()
            case true => try {
              val stream: InputStream = response.body().byteStream()
              val bytes: Array[Byte] = IOUtils.toByteArray(stream)
              save(src, bytes) match {
                case Some(path) =>
                  upload(path) match {
                    case Some(pathU) => superior ! Oss(src, pathU)
                    case None        => superior ! Redownload(src)
                  }
                case None => superior ! Discard(src)
              }
            } catch {
              case NonFatal(e) =>
                logger.error(s"ImageProcessor.DownloadErr - 3: $src, ${e.getMessage}")
                superior ! Redownload(src)
            } finally {
              response.close()
            }
          }
        }
      })
    } catch {
      case ex: IOException =>
        logger.error(s"ImageProcessor.DownloadErr- IOException: $src, ${ex.getMessage}")
        superior ! Redownload(src)
      case NonFatal(e) =>
        logger.error(s"ImageProcessor.DownloadErr - 0: $src, ${e.getMessage}")
        superior ! Redownload(src)
    }
  }

获取系统包路径

java -XshowSettings:properties -version		// java 8

或:

public class PrintLibPath{
    public static void main(String[] args){
        System.out.println(System.getProperty("java.library.path"));
    }
}
javac PrintLibPath.java
java -cp /path PrintLibPath
/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib	# centos 6.5

1.39 - 占位符

Scala 的匿名函数语法arg => expr,提供来非常简洁的方式来构造函数字面值,甚至函数中包含多个语句

同时,匿名函数中可以使用占位符语法:

List(1, 2).map { i => i + 1 }
// equivalent
List(1, 2).map { _ + 1 }

当时,如果在上面的例子中添加一个 debug 信息,比如:

List(1, 2).map { i => println("Hi"); i + 1 }
Hi 
Hi 
List[Int] = List(2, 3) 

List(1, 2).map { println("Hi"); _ + 1 }
Hi 
List[Int] = List(2, 3)

可以发现,结果并不符合预期。

因为函数经常被当做参数传递,经常可以看到他们被{...}包围。通常会认为大括号表示一个匿名函数,但是它只是一个块表达式:一条或多条语句,最后一条决定了这个块的结果。

上面的例子中,两个块被解析的方式不同,决定了他们的不同行为。

第一条语句中,{ i => println("Hi"); i + 1 }被认为是一个arg => expr方式的一个函数字面值语句,而expr在这里就是println("Hi"); i + 1。因此,println语句也是函数体的一部分,每当函数被调用时,他都会被执行。

scala> val printAndAddOne = (i: Int) => { println("Hi"); i + 1 } 
printAndAddOne: Int => Int = <function1>

scala> List(1, 2).map(printAndAddOne) 
Hi 
Hi 
res29: List[Int] = List(2, 3)

第二条语句中,代码块被识别为两个表达式,println("Hi")_ + 1println语句并不是函数体的一部分,它会在整个语句块{ println("Hi"); _ + 1 }当做参数传递给 map 方法时执行,而不是 map 方法执行的时候。而整个块的计算结果,即最后一行语句的值,_ + 1,作为一个Int => Int的匿名函数传递给 map 方法。

scala> val printAndReturnAFunc = { println("Hi"); (_: Int) + 1 } 
Hi 												// println 语句已经被调用
printAndReturnAFunc: Int => Int = <function1>	// 整个块已经计算完成,得到匿名函数

scala> List(1, 2).map(printAndReturnAFunc) 
res30: List[Int] = List(2, 3)

总结

这个地方的关键是:使用占位符定义的匿名函数的作用域仅延伸到包含占位符(_)的表达式普通语法的匿名函数,其函数体包含从标示符(=>)开始直到语句块结束

普通语法的匿名函数:

scala> val regularFunc = { a:Any => println("foo"); println(a); "baz"}
regularFunc: Any => String = <function1>

scala> regularFunc("hello")
foo
hello
res0: String = baz

占位符语法的匿名函数,下面这两个函数是等效的:

scala> val anonymousFunc = { println("foo"); println(_: Any); "baz" }
foo 
anonymousFunc: String = baz

scala> val confinedFunc = { println("foo"); { a: Any => println(a) }; "baz" } 
foo 
confinedFunc: String = baz

1.40 - 多变量赋值

如果需要以简便的方式对多变量赋值,比如:

var MONTH = 12; var DAY = 24 
var (HOUR, MINUTE, SECOND) = (12, 0, 0)

但是结果并不符合预期:

MONTH: Int = 12
DAY: Int = 24
<console>:11: error: not found: value HOUR
       var (HOUR, MINUTE, SECOND) = (12, 0, 0)
            ^
<console>:11: error: not found: value MINUTE
       var (HOUR, MINUTE, SECOND) = (12, 0, 0)
                  ^
<console>:11: error: not found: value SECOND
       var (HOUR, MINUTE, SECOND) = (12, 0, 0)
                          ^

Scala 允许使用大写字母作为普通的变量名,就和 MONTH、DAY 一样并没有报错。但是第二条语句中使用的多变量赋值方式却不同。

因为多变量赋值实质上基于模式匹配,但是在模式匹配中,以大写字母开头的变量代表着特殊的意义:它们是稳定标示符

稳定标识符是给常量预留的,比如:

final val TheAnswer = 42
def checkGuess(guess: Int) = guess match { 
	case TheAnswer => "Your guess is correct" 
	case _ => "Try again" 
}
scala> checkGuess(21) 	// res8: String = Try again
scala> checkGuess(42) 	// res9: String = Your guess is correct

相反,小写的变量名定义为变量模式,表示对变量的赋值:

var (hour, minute, second) = (12, 0, 0)
// hour: Int = 12 minute: Int = 0 second: Int = 0

因此在一开始的例子中,并不是对变量的赋值,而是对常量的匹配。

总结

如果想要使用大写的变量名,在极端的情况下,会对当前作用域中的值进行匹配,这个模式匹配会编译成功,并且最终的结果依赖于值是否真正匹配:

val HOUR = 12; val MINUTE, SECOND = 0;
scala> var (HOUR, MINUTE, SECOND) = (12, 0, 0)	// 1 - 匹配成功
val HOUR = 13; val MINUTE, SECOND = 0;
scala> var (HOUR, MINUTE, SECOND) = (12, 0, 0) 	// 2 - 匹配失败
scala.MatchError: (12,0,0) (of class scala.Tuple3) ...

在上面的第一个语句中,即便是匹配成功也不会进行任何赋值操作:稳定标示符在模式匹配期间不会进行任何赋值

小写的变量名同样可以使用重音符(`)包围的方式当做稳定标示符,同时它们必须是 val,因此把他们当做常量来处理:

final val theAnswer = 42 
def checkGuess(guess: Int) = guess match { 
	case `theAnswer` => "Your guess is correct" 
	case _ => "Try again" 
}

大写的变量名并声明为 var ,在 Scala 中是不推荐的做法,而且要完全避免。使用大写变量名来声明常量,同时,常量声明为 final。这样避免被子类覆写,同时编译器将他们内联(inline)以提升性能。

1.41 - 构造器

继承中的构造器初始化顺序

很多编程语言通过构造器参数类初始化类的成员变量:

class MyClass(param1, param2, ...) { 
	val member1 = param1 
	val member2 = param2 
	... 
}

在 Scala 中,构造器参数既是成员变量,避免了重复赋值:

class MyClass(val member1, val member2, ...) { 
	... 
}

但是下面的代码:

trait A { 
	val audience: String 
	println("Hello " + audience) 
}

// 通过成员实现接口字段
class BMember(a: String = "World") extends A { 
	val audience = a 						
	println("I repeat: Hello " + audience) 
}

// 通过构造器实现接口字段
class BConstructor(val audience: String = "World") extends A {
	println("I repeat: Hello " + audience)
}
new BMember("Readers") 
new BConstructor("Readers")

其执行结果为:

scala> new BMember("Readers") 
Hello null 					// <======= null
I repeat: Hello Readers 
res3: BMember = BMember@1aa6f6eb

scala> new BConstructor("Readers") 
Hello Readers 
I repeat: Hello Readers 
res4: BConstructor = BConstructor@64b6603a

这表示,A 中 audience 的值,随着该成员是在构造器参数列表中声明或在构造器体重声明而不同。

要理解这两种成员声明方式的不同,需要了解类测初始化顺序。B 的两种构造器都是以下面的形式声明:

class c(param1) extends superclass { statements }

new BMember("Readers")new BConstructor("Readers")的初始化会以下面的序列进行:

  1. 参数值 ”Readers“ 被求值,当然这里他直接是一个字符串,不需要计算,但如果他是一个表达式,比如"readers".capitalize,则会首先计算
  2. 被构造的类以下面的模板进行计算:superclass { statements }
    1. 首先,是 A 的构造器,A 的构造体
    2. 然后,是子类构造体

因此,在 BMember 中,第一步是将 “Readers” 赋值给构造器参数 a,然后是 A 的构造器被调用,但是这时成员 audience 还没有被初始化,所以默认值为 null。接着,子类 BMember 的构造体被执行,变量 a 的值被赋值给成员 audience,最终打印出了 audience 的值。

而 BConstructor 中,”Readers“ 被计算并以直接的方式赋值给 audience,因为这是构造器参数计算的一部分。因此当 A 的构造器被调用时,audience 的值已经被初始化为 ”Readers“。

总结

通常,BConstructor 的模式作为首选的方式。

同样可以使用**字段提前定义(early field definition)**来实现同样的结果。这样可以支持你在构造器参数上执行额外的计算,或者以正确初始化的值来创建匿名类:

class BEarlyDef(a: String = "World") 
  extends { val audience = a } 			// 字段提前定义部分
  with A { println("I repeat: Hello " + audience) }

scala> new BEarlyDef("Readers") 
Hello Readers 
I repeat: Hello Readers 
res7: BEarlyDef = BEarlyDef@44c93da7
scala> new { val audience = "Readers" } with A {
	println("I repeat: Hello " + audience) } 
Hello Readers 
I repeat: Hello Readers 
res0: A = anon1@71e16512

**提前定义(Early definitions)**在超类构造器调用之前定义并赋值成员。

因此,加上之前顺序后的完整顺序:

  1. 执行子类构造器参数求值
  2. 执行字段提前定义
  3. 执行父类、父特质构造器、构造体,被混入的特质按照出现的顺序从左到右执行
  4. 执行子类、子特质构造体

一个完整的示例:

trait A { 
	val audience: String 
	println("Hello " + audience) 
}

trait AfterA { 
	val introduction: String 
	println(introduction) 
}

class BEvery(val audience: String) extends { 
	val introduction = { println("Evaluating early def"); "Are you there?" } } 		with A 
	with AfterA {
		println("I repeat: Hello " + audience) 
	}

scala> new BEvery({ println("Evaluating param"); "Readers" }) 
Evaluating param 				// 参数计算
Evaluating early def 			// 提前定义计算
Hello Readers 					// 第一个父类构造器、构造体计算
Are you there? 					// 第二个父类构造器、构造体计算
I repeat: Hello Readers 		// 第三个匿名父类构造器、构造体计算
res3: BEvery = BEvery@6bcc2569

1.42 - 函数式入门

翻译自:A Beginner-Friendly Tour through Functional Programming in Scala

函数式编程的基本核心十分简单:通过组合函数(function)来构建程序。

这里,“function”并非指“计算机科学”中的函数,它指一段机器代码,而是一个“数理函数(mathematical  function)”:

  1. Totality:一个函数必须为所有可能的输入生成一个值;
  2. Determinism:一个函数必须为相同的输入返回相同的值;
  3. Purity:函数唯一的副作用必须是计算它的返回值。

所有这些属性,给你一种前所未有的能力来解释你的代码:调用函数并传入任何输入,你总会得到一个有效的值,而且相同的输入总是会得到相同的结果,同时函数不会再做其他任何事,比如发射核导弹….

这种微小的想法对于大型软件工程有着深刻的简化作用,因为这意味着,你的大脑只需要追踪更少的东西就能理解程序的行为。事实上,你可以通过理解程序的个别部分来理解程序的整个行为 - 而无需一次在脑子中掌握一切!

目前,函数式编程并不总是拥有一个“简单”的声誉,但我认为是由于以下几个因素:

  1. Unfamiliarity(不太常见):函数式编程与大多数专业人员使用的编程类型有着很大的不同。因为它不太常见,看起来很难的样子。最好是将函数式编程与第一次学习编程的经验相比较(而不是学习一种你已经知道的新的编程语言的经验)。
  2. Jargon(术语太多):函数式编程中有太多术语,比如:“immutability”, “recursion”, “induction”, “hylomorphism”, “transducer”, “functor”等等一大堆。这些概念并不一定很难,但拥有可怕的冠冕堂皇的名字,命令式方式的大量编程经验也不能帮助你理解任何新的术语。
  3. Motivation(学习动机):从我的经验来看,人们对于学习那些可以清晰的看到如何达成个人目标的东西会感觉更容易。然而,随着越来越多的人开始学习函数式编程(并与他人分享知识),情况也在改善,但是对于函数式编程的核心概念并没有太大积极性。

有时函数式编程通常被关联为“高等静态类型函数式编程(advanced statically-typed functional programming)”,这时,学习“函数式编程”的人其实在一次学习两种东西:函数式编程和一个高级类型系统(多数命令式语言拥有相对简单的类型系统)。

然而,有些人可以用弱类型语言进行函数式编程,或者使用高级类型系统进行命令式编程,因此这二者并无关联。

很多函数式开发者喜欢展示我们为何对函数式编程如此兴奋。然而,我所写的大部分内容并非对函数式的好奇,甚至不是面向新手函数式开发者。相反,我写的都是我感兴趣的东西,这通常是高级函数编程的中间形式,对于那些多年来一直在进行函数式编程的人比较有用。

所以我在做一个实验:我将展示一些函数式编程的思想以帮助我们构建一些实际程序。但是我会以一种有助于前面提到的那些因素的方式进行。

这是一篇为非函数式开发者准备的文章,或者那些了解一点但想知道更多的开发者。

希望你发现它会有用 - 但相比有用,我更希望你发现它鼓舞人心。激发足够的投入和必要的努力,来推动你的函数式编程知识,尽管看起来似乎会有点难。

或者也不一定,看完这篇文章之后一切看起来都会变得很容易!

不切实际的 FP

展示函数式编程能力的一个典型例子就是泛型排序函数:

def sort[A: Orderig](as:List[A]):List[A] = as match{
  case Nil => Nil
  case a :: as =>
    val (before, after) = as partition (_ < a)
    sort(before) ++ (a :: sort(after))
}

这个例子很漂亮,因为它展示了函数式编程如何以如此简化的效果来表达一个程序。

看完代码,你自己可能会确认下面这几点:

  • 空列表排序仍会得到空列表;
  • 一个首元素为 a 后很 as 的列表,经过排序后由 小于 a 的列表、a、大于 a 的列表依次构成。

该函数的每个部分都可以独立推理。如果相信一段是正确的,那么就可以相信整体的正确性。

此外,这个sort函数,因为它是数学意义上的函数,更容易测试和复用。我们可以传入任何列表并预期得到一个排序后的列表。因为我们知道对于同一个输入,函数总是会返回相同的结果。

因此我们的测试表示起来也会非常简单:

assert(sort[Int](Nil) == Nil)
assert(sort[Int](3 :: 2 :: 1 :: Nil) == 1 :: 2 :: 3 :: Nil)

(事实上,可以以函数式编程的方式来更加有力的表示,不过这就是另外一篇文章的主题了。)

虽然在这个例子中解释的函数式编程的好处看起来很简单,但这是一个很大的延伸,想象如果或如何对这些好处进行扩展以超越我们的玩具样例。

事实上,函数式编程的头号反对者就声称它只适合这些玩具例子,在”现实世界“编程中完全失败。

让我们找一个我们想到的现实世界中最简单的例子:一个函数失败时并不返回一个值。

完整性

我以完整性和正确性定义了一个例子,如果丢掉完整性要求会发生什么呢?

好吧,这个函数不需要返回任何东西。更实际的说,函数会一直运行,或者通过宿主语言支持的其他方式来转义这个返回值的需求 - 通常是抛出一个异常。

一个永不返回的函数是因为没有跳出而一直运行(脱离循环的边缘条件,或类似其他原因),但异常又是什么呢?

在异常出现之前,程序员使用一个返回值来表示函数的成功或失败。在 C 代码中,比如:

int parse_config(config *cfg){
  FileHandle handle;
  char *bytes;
  
  handle = new Handle();
  handle = file_open("config.cfg");
  if(handle == NULL) return -1;
  
  char *bytes = file_read(handle);
  if(bytes == null) return -2;
  .....
  return 0;
}

在这种世界里,引入异常处理似乎不可思议。程序员可以避免混乱的错误应用逻辑处理问题,从异常的短路行为中获益。

异常的主要问题是,在一个支持它的语言中,你无法保证他们可能会发生的地方。这意味着他们可能在任何地方触发。这表示,如果时间够长,他们可以无处不在(甚至在 Java 中,未检异常可以随处抛出,其中经常还包含受检异常!)。

同时由于 null 的存在,导致激增了大量防御性、攻击性的异常处理,尽管有些并没有什么意义,但导致了更多错误的发生和怪异的边缘问题。

幸运的是,我们可以轻松的同时实现完整性和整洁代码,但比老旧语言需要更多设施的支持。

我了实现这个想法,我们定义一个函数来返回列表的第一个元素:

def head[A](as: List[A]): A = as.head

这个函数并不完整,取决于你传入的是一个什么列表,可能会返回可能也不会返回列表的首元素。如果你传入一个空列表,函数永远都不会返回,而不是抛出一个异常。

如果想要该函数完整,仅需要引入一个数据结构来建模optionality的概念 - 一个东西可能有也可能没有。

让我们称之为Maybe

sealed trait Maybe[A]
final case class There[A](value: A) extends Maybe[A]
final case class NotThere[A]() extends Maybe[A]

通过这个数据结构,我们可以把这个”伪函数“head转换为真的函数:

def head[A](as:List[A]):Maybe[A] = as match {
  case Nil => NotThere[A]()
  case a :: _ => There[A](a)
}

现在当我们考虑使用该函数的代码时,不再需要考虑该函数没有返回的可能性。因为该函数总是能够返回。由于不需考虑这种可能性,使用head函数的代码表述起来也更为简单,包含更少需要分享的场景。

Maybe数据结构并没有提供跟异常一样的能力。有一条,它不会包含任何对于head函数来说意味着 OK 的错误信息(因为我们知道错误是什么-空列表),但对于其他函数来说可能并不有效。

为了解决这个问题,我们可以引入一个新的数据结构,称为Resullt,对exceptionality进行建模:

sealed trait Result[E, A]
final case class Error[E, A](error: E) extends Result[E, A]
final case class Success[E, A](value: A) extends Result[E, A]

这种类型支持我们创建一个file_open这样的完整性函数:

def file_open(path:String):Result[FileError, FileHandle] = ???

现在我们可以拥有跟异常一行的信息。然而,如果我们需要很多操作需要执行,同时每个操作都返回一个Result,这看起来我们会拥有相当多的模板代码,不免让人回忆起异常之间的日子:

def parse_config:Result[FileError, Config] = {
  file_open("config.cfg") match {
    case Error(e) => Error(e)
    case Success(handle) =>
      file_read(handle) match {
        case Error(e) => Error(e)
        case Success(bytes) => ???
      }
  }
}

我们已经创建了file_openfile_read函数,等等,简化了我们的表述,同时也引入了不少模板代码,使代码难以阅读。

为了夺回之前异常的优势,我们需要识别上面代码的模式。如果你研究几分钟,则会发现下面的模式:

doX match {
  case Error(e) => Error(e)
  case Value(x) => doY(x) match {
    case Error(e) => Error(e)
    case Success(y) => doZ(y) match {
      case Error(e) => Error(e)
      case Success(z) => doW(w) match {
        ...
      }
    }
  }
}

你会发现,doYdoZdoW都会从上一个操作产生的Result[E, A]那接收一个 A,然后生成一个新的Result[E, A]

这暗示我们可以通过一个chain方法分解重复的代码:

def chain[E, A, B](result:Result[E, A])(f: A => Result[E, B]): Result[E, B] = 
  result match{
    case Error(e) => Error(e)
    case Success(a) => f(a)
  }

现在,可以使用chain方法来重新实现原来的parse_config

def parse_config: Result[FileError, Config] = {
  chain(file_open("config.cfg")) {handle => 
    chain(file_read(handle)){ bytes =>
      ???
    }
  }
}

这样通过在Result[E,A]上调用chain来减少模板代码。通过这种方式,我们既可以拥有异常的短路优势,错误处理逻辑又由chain方法拆分,应用逻辑就无需再关注这些。

如果你使用Result这样的结构来建模异常场景,通常你会发现需要一个下面这样的工具方法:

// change the A in a Result[E, A] into a B by using the provided function f
def change[E, A, B](result:Result[E, A])(f: A => B):Result[E, B] = result match{
  case Error(e) => Error(e)
  case Success(a) => Success(f(a))
}

这是一个类 map 的函数(将列表中的元素映射为其他类型)。如果你喜欢 OO 风格,可以把chainchange方法包括在Result类之中:

sealed trait Result[E, A] {
  def change[B](f: A => B):Result[E, B] = this match {
    case Error(e) => Error(e)
    case Success(a) => Success(f(a))
  }
  
  def chain[B](f: A => Result[E, B]):Result[E, B]= this.match{
    case Error(e) => Error(e)
    case Success(a) => f(a)
  }
}

final case class Error[E, A](error:E) extends Result[E, A]
final case c;ass Success[E, A](value:A) extends Result[E, A]

这样一来代码就会更可读:

def parse_config:Result[FileError, Config] = {
  file_open("config.cfg").chain{ chanle =>
    file_read(handle).chain{bytes =>
      ???
    }
  }
}

更进一步,如果你把changechain方法称作mapflatMap,Scala 则会提供更加灵巧的方式来进一步简化代码格式:

def parse_config:Result[FileError, Config] = {
  for{
    handle <- file_open("config.cfg")
    bytes <- file_read(handle)
  } yield ???
}

最终,我们首先了函数的完整性,同时也拥有异常的短路特性和关注点分离。

我们所需要的也就是chain函数(即 flatMap)和Result数据结构。其余的则自然引入。

注意,chain函数接收一个函数作为参数。这个参数作为一个匿名函数(在其上下文中捕获任意引用)提供,这论证了为什么这种技术永远不会出现在 C 代码中。如果 C 拥有一类函数、垃圾回收或者引用计数,很可能这种模式会自行出现而无需函数式编程社区的任何输入。

函数的其他要求是确定性纯洁性,下面的章节会讲到。

确定性 & 纯洁性

一个函数,如果拥有不确定性,或者所做的并非仅仅是计算一个返回值,那它就会变得非常难以推理。

比如我用过的一个库,会在构造器中执行 IO:

class Logging{
  private OutputStream ostream;
  public logging(File file){
    ostream = new FileOutputStream(file);
  }
}

该构造器可能会抛出异常,并且难以预料!

另一个例子,当你把 Java 中的URL类放入一个数据结构时,它会执行一个网络连接。原因是 equals 和 hash 编码方法会触发地址的识别。

除了不纯函数的意味性质外(谁知道他们什么时候会干些什么!),非确定性(non-determinism)导致函数的测试和表述变得尤其困难。你可能会被迫对影响函数行为的那些状态的表述代码进行 mock。

纯函数,确定性函数,在现场之外不会做任何不正当的事。你可以期望他们每次都会根据相同的输入返回相同的结果,这意味着代码容易测试且易于理解。

但如果你尝试使所有的函数都是完整的、确定的,并且是纯的,当你执行一些输入输出或副作用操作时则会很快撞到墙上 - 一堵说明了太多函数式编程与真实软件不切实际的墙。

事实上,这墙早就被粉碎了,而不是沿着这条路走下去,让我们看一下一个 IO 的例子,看能不能推荐一种方案。

Console IO

假如我们正开发一个控制台程序,改程序需要从控制台读取输入,然后再会写数据到控制台上。这样的程序能解决很多有用的问题,如果我们能想出如何以确定性、纯函数的方式来构建一个控制台程序,那就能推广到其他类型的程序了。

如果你针对该问题思考了一会,可能会得出下面的想法:可以定义一些描述控制台副作用的数据结构来构建程序,而不是调用那些不确定或不纯的函数。

假如这个ConsoleIo是我们这个程序的说明书,首先,我们需要一种方式来描述”写入输入到控制台“这种副作用。

一种方式看起来可能会是这样:

sealed trait ConsoleIO
final case class WriteLine(line:String) extends ConsoleIO

这种方式未免也太简单了,因为这样我们只能将文本的一行写入到控制台:

def helloWorld: ConsoleIO = WriteLine("Hello World!")

这是一个完整的、确定的纯函数 – 但是并没有什么卵用。它顶多能描述一个程序将文本的一行写入到控制台。文本可能会变,当然,甚至是函数的一个参数,但最终,程序只会对控制台有一个副作用。

幸运的是,将该结构扩展为支持多个顺序的副作用也很简单:

sealed trait ConsoleIO
final case class WriteLine(line:String, then:ConsoleIO) extends ConsoleIO

通过这种方式,我们可以描述更加复杂的”副作用化“程序,比如:

def infiniteHelloWorld:ConsoleIO = WriteLine("Hello World", infiniteHelloWorld)

如果我们引入一个”thunk“以避免在构造中压栈(blow stack),该结构就能够描述一个向控制台写入无限个”Hello World“的程序。像这种无限制的程序并没有什么用,不过我们可以给ConsoleIO添加另一项,让他可以支持终止:

sealed trait ConsoleIO
final case class WriteLine(line:String, then:ConsoleIO) extends ConsoleIO
final case object End extends ConsoleIO

现在我们可以描述一个将文本行打印指定次数到控制台的程序:

def printTextNTimes(text:String, n:Int):ConsoleIO = {
  if(n <= 0) End
  else WriteLine(text, printTextNTimes(text, n -1))
}

该函数也是一个完整的、确定的纯函数。当然,他实际上不会打印任何东西到控制台,但我们很快就能做到。

目前,我们只能描述一个写入文本到控制台的程序。因此扩展一个从控制台读取输入的程序也相当简单:

sealed trait ConsoleIO
final case class WriteLine(line:String, then:ConsoleIO) extends ConsoleIO
final case class ReadLine(process:String => ConsoleIO) extends ConsoleIO
final case class object End extends ConsoleIO

注意构造一个ReadLine的值时我们需要提供一个函数,传入从控制台读到的行,返回另一个ConsoleIO,代表该副作用程序的剩余部分。

我们向ReadLine传入一个函数,它作为一个保证,将来的某个时间某人会通过控制台给我们一行输入,然后我们再将其返回给程序的”剩余“部分。

该结构有足够的能力来描述我们的交互程序。比如,下面的程序会问你的名字并向你 Say hello:

def socialProgram:ConsoleIO = WriteLine(
  "hello, what is your name?",
  ReadLine(name =>
    WriteLine("Hello, " + name + "!", End)
  )
)

记得这是一个完整的、确定的纯函数。人们想象使用这个结构来描述非常复杂的副作用程序。事实上,任何仅需要控制台 IO 的程序都可以使用该结构来描述。

注意任何使用ConsoleIO描述的程序都会在某个点终止。这些程序不能返回一个值。

如果我们需要这样的”控制台式程序“:该程序生成的东西在其他程序又能够使用。这样我们需要泛型化End来接收一些类型为A的值,这迫使我们需要给ConsoleIO添加一个新的类型参数A,并贯穿于其他项。

最终的结果看起来稍微有点复杂:

sealed trait ConsoleIO[A]
final case class WriteLine[A](line:String, then:ConsoleIO[A]) extends ConsoleIO[A]
final case class ReadLine[A](process: String => ConsoleIO[A]) extends ConsoleIO[A]
final case class EndWith[A](value: A) extends ConsoleIO[A]

现在,ConsoleIO[A]能够描述一个读写控制台并被一个类型为A的值终止的程序。这支持我们构建生成值的程序,然后这些值再被其他程序消费。

我们能够使用该结构创建之前的”Hello, !“程序,但这次,我们能从程序中返回用户的名字:

val userNameProgram:ConsoleIO[String] = WriteLine(
  "Hello, what is your name?",
  ReadLine(name =>
    WriteLine("Hello, " + name + "!", EndWith(name))  
  )
)

ConsoleIO中我们唯一丧失的是修改返回值类型的能力。比如,你想构建另一个程序并不是返回用户名,而是名字的长度,如何复用userNameProgram呢?

当前这种方式是不可能的。我们需要一些更强大的东西来实现这个打算。如果我们拥有一个 List 则可以使用 map 来改变结果类型。而这正是ConsoleIO所需要的。

我们可以直接给他添加一个 map 函数:

sealed trait ConsoleIO[A]{
  def map[B](f: A=>B):ConsoleIO[B] = Map(this, f)
}
final case class WriteLine[A](line: String, then: ConsoleIO[A]) extends ConsoleIO[A]
final case class ReadLine[A](process: String => ConsoleIO[A]) extends ConsoleIO[A]
final case class EndWith[A](value: A) extends ConsoleIO[A]
final case class Map[A0, A](v: ConsoleIO[A0], f: A0 => A) extends ConsoleIO[A]

现在给出任何一个ConsoleIO[A],我们都可以通过函数A => B将其转换为ConsoleIO[B]。因此现在我们可以编写一个新的userNameLenProgram,计算用户名字的字符长度:

def userNameLenProgram:ConsoleIO[Int] = userNameProgram.map(_.length)

随着map的加入,EndWith的作用发生了改变:我们不再需要它从程序中返回一个值,因为我们可以把拥有的任何值转换为返回值。比如你有一个ConsoleIO[String],我们可以通过一个String => Int函数转换为Console[Int]

然后,EndWith仍然可以用于构造一个不执行任何副作用的“纯“程序(但能够与其他程序组合)。因此它仍然是有用的,虽然与其最初的目的不同。因此,我们可以将其重新命名为Pure

sealed trait ConsoleIO[A] {
  def map[B](f: A => B): ConsoleIO[B] = Map(this, f)
}

final case class WriteLine[A](line:String, then:ConsoleIO[A]) extends ConsoleIO[A]
final case class ReadLine[A](process: String => ConsoleIO[A]) extends ConsoleIO[A]
final case class Pure[A](value: A) extends ConsoleIO[A]
final case class Map[A0, A](v: ConsoleIO[A0], f: A0 => A) extends ConsoleIO[A]

通过这些封装,我们可以没有任何限制和约束的构建并复用这个控制台 IO 程序。所有这些描述都是拥有确定性、完整性的纯函数,因此也获得了函数式代码的强大优势:易于理解、易于测试、易于安全的调整,且易于组合。

最终 ,我们需要把一个程序的描述转换为实际的程序-一个能真正执行副作用的程序(这意味这没有确定性、也不纯)。通常这个程序会叫做interpretation

可以写一个简单的,如果没有确定性、也不纯洁,解释器可以基于ConsoleIO[A]使用一下类似的代码:

def interpret[A](program:ConsoleIO[A]):A = program match {
  case WriteLine(line, next) => println(line); interpret(next)
  case ReadLine(process) => interpret(process(readLine()))
  case Pure(value) => value
  case Map(v, f) => f(interpret(v))
}

还有更好的方式,不过这里就不再过多演示了。到目前为止,我认为这已经相当有意思了,我们同时能够描述一个充满副作用的,但是又满足完整性、确定性、纯函数的要求,又能很方便的转换为一个真实执行副作用的程序。

注意,这种转换需要、也非常必要在程序的最后进行(将副作用尽可能推迟到最后)!在 Haskell 中,这会发生在 Haskell 运行时的主函数之外。然而在其他的语言中,背后并没有这些函数式级别的机制支持,你总是可以在程序的入口点以副作用的方式来解释你的程序。

这么做是为了确保你在程序中拥有最大程度的完整性、确定性以及纯洁性。在你边缘的地方,你可能有个小层很难去表示,但正是在这里来基于底层将程序转换为可执行副作用的描述。

可扩展性

这种方式在一个固定副作用集的世界里会运行的很多好。在我们目前的用例中,控制台 IO—从一个控制台读写文本行。但是在实际的程序中,多种原因之下的副作用要复杂的多,我们受益于使用不同的副作用组合进所有副作用来构建程序的能力。

第一步是识别附加结构。实质上,我们可以把对控制台程序的描述拆分成生成值(WriteLine)和接收值(ReadLine)。程序剩余的部分则是由纯模板组成:要么是通过映射(map)返回值将一个程序转换为另外一个,要么是,一个程序依赖另外一个程序的结果,将这两个程序进行链接(chain/flatMap)。

如果这没有任何意义,可以研究一下下面的例子:

sealed trait ConsoleIO[A]{
  def map[B](f: A=>B):ConsoleIO[B] = Map(this, f)
  def flatMap[B](f: A=>ConsoleIP[B]):ConsoleIO[B] = Chain(chis, f)
}

final case class WriteLine(line:String) extends ConsoleIO[Unit]
final case class ReadLine() extends ConsoleIO[String]
final case class Pure[A](value: A) extends ConsoleIO[A]
final case class Chain[A0,A](v:ConsoleIO[A0], f: A0=>ConsoleIO[A]) extends ConsoleIO[A]
final case class Map[A0, A](v: ConsoleIO[A0], f: A0 => A) extends ConsoleIO[A]

这里只有一点不同,增加了更多令人迷惑的方式来表示同一个东西。使用这个结构,我们的交互程序会表示的更复杂一点:

def userNameProgram:ConsoleIO[String] = 
  Chain[Unit, String](
    WriteLine("What is your name?"),
    _ => Chain[String, String](
      ReadLine(),
      name => Chain[Unit, String](
        WriteLine("Hello, " + name + "!"),
        _ => Pure(name)
      )
    )
  )

在这个模型中,ConsoleIO拥有一组看上去泛型的项,chain、Map、Pure,他们不会跟我们控制台程序的副作用打交道,另外又有两个额外的项用来描述这些副作用:ReadLine、WriteLine,他们在这个模型中则被简化了。

这种模型构建的解释器也会有一点复杂:

def interpret[A](program: ConsoleIO[A]):A = program match{
  case WriteLine(line) => println(line); ();
  case ReadLine() => readLine()
  case Pure(value) => value
  case Map(v, f) => f(interpret(v))
  case Chain(v, f) => interpret(f(interpret(b)))
}

这种简明的描述看起来不会完全不同,但关键点在于只有两项用来描述副作用,剩余则都是纯函数装置。这些装置可以被抽象到另一个类然后复用于其他所有的副作用类型。比如:

sealed trait Sequential[F[_], A] {
  def map[B](f: A=> B):Sequential[F, B] = Map[F, A, B](this, f)
  def flatMap[B](f:A => Sequential[F, B]):Sequential[F, B] = Chain[F, A, B](this, f)
}

final case class Effect[F[_], A](fa: F[A]) extends Sequential[F, A]
final case class Pure[F[_]](value:A) extends Sequential[F, A]
final case class Chain[F[_], A0, A](v: Sequential[F, A0], f: A0=>Sequential[F,A]) extends Sequential[F,A]
final case class Map[F[_], A0, A](v:Sequential[F, A0], f:A0=>A) extends Sequential[F,A]

sealed trait ConsoleF[A]
final case class WriteLine(line: String) extends ConsoleF[Unit]
final case class ReadLine() extends ConsoleF[String]

type ConsoleIO[A] = Sequential[ConsoleF, A]

Sequential来并不直接引用ConsoleIO,因此可以复用与其他不同的副作用类型。

这允许我们清晰地将副作用从计算拆分开。因此新版本的 hello world 程序看起来会是这样:

def userNameProgram:ConsoleIO[String] = {
  Chain[ConsoleF, Unit, String](
    Effect[ConsoleF, Unit](WriteLine("What is your name?")),
    _ => Chain[ConsoleF, String, String](
      Effect[ConsoleF, String](ReadLine()),
      name => Chain[ConsoleF, Unit, String](
        Effect[ConsoleF, Unit](WriteLine("Hello, " + name + "!")),
        _ => Pure(name)
      )
    )
  )
}

如果我们利用 Scala 的 for 符号,然后添加一个隐式类来更方便的将副作用包含到Effect的构造器中:

def userNameProgram:ConsoleIO[String] = {
  for{
    _ <- WriteLine("What is your name?").effect
    name <- ReadLine().effect
    _ <- WriteLine("Hello, " + name + "!").effect
  } yield name
}

这种方式可以进一步简化,比如,为所有项添加帮助函数。这些函数使程序更加简洁:

def userNameProgram:ConsoleIO[String] = {
  for{
    _ <- writeLine("What is your name?")
    name <- readLine()
    _ <- writeLine("Hello "+ name + "!")
  } yield name
}

这与上一种实现没有什么不同。结构也大致相同,仅有一点语法不同。在我们的例子中,我们构建了一个程序的description,完整、确定的纯函数,他本身对外部世界没有任何副作用(除了利用 CPU 和内存来计算结构)。

此外,使用这种清晰的分离,实际的副作用集也可以进行扩展。这意味着我们可以在两个程序中使用采用不同的副作用,然后组合成一个程序来同时拥有两种副作用。

为了达到这种效果,你仅需要一些”EitherOr“来表达这是一种副作用(控制台 IO)或是另一种副作用(文件 IO):

sealed trait EitherOr[F[_], G[_], A]
final case class IsLeft[F[_], G[_], A](left:F[A]) extends EitherOr[F, G, A]
final case class IsRight[F[_], G[_], A](right:G[A]) extends EitherOr[G, G, A]

type CompositeProgram[A] = Sequential[EitherOr[ConsoleIO, FileIO, ?], A]

可以基于这个结构来时间简洁的解释器,并可以根据指定的副作用类型(控制台或文件)复用到其他的解释器中。

现在我们已经从第一个原则开发到这,而且没有任何术语,你看到的这种抽象实际上称为著名的”Free monad“,它让不知情的 Scala 程序员无处不在害怕!

这看起来不是太糟,对吧?

在我们结束整个教程之前,看一下这种抽象的其他好处。

纯函数的能力

通常对这种副作用的编码方式的反应是,”有什么意义?“

使用这种风格来描述副作用化程序确实有一些非常实际的好处,

  • 你的程序的类型精确描述了它到底能做什么。因为没有无处不在的大块机器代码嵌入,代码推理能做的事就变得非常简单。相反,你以声明的方式准确描述了你的程序是什么(或更进一步,你描述是了如何将一个抽象层的副作用转换为一个较低抽象层的副作用)。
  • 你可以将程序的描述与其实际所做的事分开。例如,一个 Web 应用程序可以创建 HTML,但这样的描述可以解释为 HTML DOM 节点,Canvas 节点,或服务器上的 PDF。
  • 你可以模拟依赖。如果你的外部依赖(文件系统、网络、API 等)都由数据结构描述,那么你可以轻松遍历这些数据结构,以确保你提供了正确的值,他们也返回了正确的响应。与 mock 库不同,这种方式是完全类型安全的,而却不需要任何运行时支持。
  • 你可以在程序运行时执行检查。比如,你可以添加日志行,以打印出每个指令要做什么,以及是否成功。这些日志可以是相当详细的,可以取代手工日志的需要。或者跟多,他们可以在组合解释器时”编织(wave in)“进去,日志在被禁用时可以没有任何开销。没有模板代码也没有开销—对我来说听起来真的不错。
  • 除了日志,你可以在运行时将整个切面添加到程序中。比如添加认证、授权、审计,仅通过组合解释器而无需修改代码库。就像面向切面编程,但更加类型安全且灵活。

除了所有的这些好处之外,你还可以得到非常明显的好处,能够很好的推理,完整、确定、纯函数,即使是存在副作用的情况下。这些好处可以是新的开发者收益,维护已有的代码、解决 bug,引进新的团队成员等等。

总结

我希望至少文章中涵盖的部分创造了一些意义。更重要的是,我希望你们看到我们是如何使用完整的、确定的纯函数编写完整程序,以及这种方式带来的好处,其中一些也是我们刚刚发现的。

如果没有别的,这是一个学习更多函数式编程的呼唤!尽管拥有奇怪的名字、不完整的文档、混乱的类型也要坚持下去。

函数式编程非常有力,拥有巨大的力量。根据我的经验,那些花时间学习函数式编程的人最终会变得对他充满热情,永远不会回到过去的老路上。函数式编程给了开发者强大的力量—以简化的方式编写软件并输出简洁代码的能力,维护成本更低,更易组合、更易推理,等等等等。

如果你有兴趣,我期待你坚持下去,如果你卡主了,你要知道我还有社区的其他很多成员都站在你背后。我们会帮助你达到下一个层次,从值到值,从类型到类型,从 lambda 到 lambda。

1.43 - Monoid

整理自《Functional Programming In Scala》第十章。

Monoid(幺半群) 是一个代数定义,是*纯代数(purely algebraic)*的一种,它简单、普遍存在且很实用。除了满足同样的代数法则外不同 Monoid 实例之间很少有关联,但这种代数结构定义了用于实现实用的多态函数所必须的所有法则。

操作列表、连接字符串或在循环中累加都可以被拆解成 Monoid。下面介绍它在两个方面的使用方式:将问题拆分成小部分然后并行计算;将简单的部分组装成复杂的计算。

1. 什么是 Monoid

比如在字符串拼接的代数表达中,“foo” + “bar” 得到“foobar”,空串是这个操作的单位元(identity)(或称“幺元”)元素,即 “”+s 与 s + “” 的值都是 s。进一步,如果将三个串相加,r + s + t,由于这个操作是可结合的(associative),因此 (r +s) +t 与 r + (s+t) 的结果是一样的。

该规则同样适用于整数相加,它也是可结合的。(x+y)+z 与 x +(y+z) 的结果相同,而且由一个单位元素 0,它去其他整数相加时不会影响结果。同样乘法也是一样,它的单位元元素是 1。

布尔操作符 || 和 && 同样是可结合的,它们的单位元元素是 true 和 false。

像这样的代数便成为 Monoid,结合律(associativity)和同一律(identity)则一起被称为monoid法则。一个 Monoid 由如下几部分构成:

  • 一个类型 A;
  • 一个可结合的二元操作 OP,接收两个参数后返回相同类型的值,对于任何x:A, y:A, z:A来说,OP(OP(x,y), z)OP(x, OP(y,z))是等价的;
  • 一个值zero:A,它是一个单位元,对于任何x:A来说,zero与它的操作都等于 x 自身:OP(x, zero) == xOP(zero, x) == x

可以使用 Scala 表示:

trait Monoid[A] {
  def op(a1:A, a2:A): A
  def zero: A
}

然后是 String 实例:

val stringMonoid = new Monoid[String] {
  def op(a1:String, a2:String) = a1 + a2
  def zero:String = ""
}

或者是 List 的连接:

// 注意这里是 def,一个返回 Monoid 实例的函数,否则将丢失类型参数 A
def listMonoid[A] = new Monoid[List[A]] {
  def op(a1:List[A], a2:List[A]) = a1 ++ a2
  def zero: Nil
}

“有”一个 Monoid,还是“是”一个 Monoid:

当程序员和数学家讨论:一个类型是 Monoid,或,有一个 Monoid实例,有两种不一致的表述方式。程序员易于认为一个Monoid[A]的实例是 Monoid,但这并不准确。Monoid 实际上是类型和定义法则的实例。更准确的说是类型 A 和 Monoid[A]实例定义的操作构成了一个 Monoid。

一个类型、一个此类型的二元操作(满足结合律)、一个单位元元素,这三者构成一个 Monoid。

2. 使用 Monoid 折叠列表

Monoid 和列表联系紧密,从 List 的foldLeft/foldRight签名中可以发现参数的类型很特别:

def foldRight[B](z: B)(f: (A, B) => B): B
def foldLeft[B](z: B)(f: (B, A) => B): B

当 A 和 B 类型一样时:

def foldRight(z: A)(f: (A, A) => A): A
def foldLeft(z: A)(f: (A, A) => A): A

如果一个字符串的列表,可以传递 StringMonoid 中的 OP 和 zero,用于将字符串进行拼接:

val words = List("Hic", "Est", "Index")
val s = words.foldRight(stringMonoid.zero)(stringMonoid.op)	// "HicEstIndex"
val t = words.foldLeft(stringMonoid.zero)(stringMonoid.op)	// "HicEstIndex"

会发现两个操作的结果一样,这正是因为结合律与同一律法则,无论左右结合效果都一样。

words.foldRight("")(_ + _) == (("" + "Hic") + "Est") + "Index"
words.foldLeft("")(_ + _) == "Hic" + ("Est" + ("Index" + ""))

可以编写一个通用的 concatenate 函数,使用 Monoid 去折叠列表:

def concatenate[A](as:List[A], m:Monoid[A]) :A = as.foldLeft(m.zero)(m.op)

但是假如列表中的元素类型不是 Monoid 实例该如何处理呢,总是可以将列表 map 成另外的类型:

def foldMap[A, B](as:List[A], m:Monoid[B])(f: A => B): B

3. 结合律与并行化

Monoid 操作的结合律意味着可以自由选择如何进行数据结构的折叠操作。前面展示了使用列表的 foldLeft 和 foldRight 去调用满足结合律的函数,对列表按照顺序向左或向右的 reduce。如果有个 Monoid 可以使用*平衡折叠法(balance fold)*对列表进行 reduce,这样一些操作可能更加高效或支持并行化。

假设一个有序集 a, b, c, d,三种不同的折叠方式:

op(a, op(b, op(c, d)))	// foldRight
op(op(op(a, b), c), d)	// foldLeft
op(op(a, b), op(c, d))	// balance fold

在平衡折叠中,因为两个 op 是独立的,因此支持同时运行。当每个 op 的时间花费与参数的长度成正比时平衡树的结构可以变得更加高效,比如下面的表达式:

List("lorem", "ipsum", "dolor", "sit").foldLeft("")(_ + _)

其求值轨迹为:

List("lorem", "ipsum", "dolor", "sit").foldLeft("")(_ + _)
List("ipsum", "dolor", "sit").foldLeft("lorem")(_ + _)
List("dolor", "sit").foldLeft("loremipsum")(_ + _)
List("sit").foldLeft("loremipsumdolor")(_ + _)
List().foldLeft("loremipsumdolorsit")(_ + _)
"loremipsumdolorsit"

每次折叠,分配一个临时的字符串(foldLeft 的第一个参数)然后丢弃,下次又要分配一个更大的字符串。字符串的值是不变的,当 a + b 时,需要分配一个字符数组然后将 a 和 b 的值复制到这个新数组。这个时间花费与 a、b 的总长度是成正比的。相比更高效的方式是对半组合顺序集,先构建“loremipsum”和“dolorsit”,然后将他们加在一起。

4. 例子:并行解析

如果需要统计字符串中的单词数,可以按顺序扫描字符串,寻找空格然后对连续的非空格字符计数。这样按顺序解析,解析器的状态可以表达成最后一个字符是否是空格。

但是如果要处理一个巨大的文件,达到单机内存装不下,需要对文件进行切分才能处理。策略是将文件拆分成多个可以管理的块(chunk),并行处理这些块,最后将结果合并起来。这时,解析器的状态可能会复杂一些,需要可以合并中间结果,无论这个部分是文件的开头、中间或结尾。这意味这合并操作需要时可结合的。

把下面的字符串当做一个大文件:

"lorem ipsum dolor sit amet"

假如对半拆分字符串,可能会将一个单词拆分。当累加这些字符串的计算结果时需要避免重复计入同一个单词。所以这里仅仅将单词作为整体来计数是不严谨的。需要一个数据结构能处理部分结果,并能记录完整的单词。单词计数的结果则可以表示成一个代数数据结构:

sealed trait WC
case class Stub(chars: String) extends WC
case class Part(lStub:String, words:Int, rStum:String) extends WC

Stub表示没有看到任何完整的单词,Part保存看到的完整的单词的个数,以及左边的部分单词和右边的部分单词。

比如上面的字符串,拆分成“lorem ipsum do”和“lor sit amet”,对前者计数的结果为Part("lorem", 1, do),对后者的计数结果为Part("lor", 2, "")

Monoid 同态

可能你会发现 Monoid 的函数之间有个法则。比如字符串的连接 Monoid 和整数累加 Monoid。假如取两个字符串的长度相加,等于连接两个字符串然后取其长度:

"foo".length + "bar".length == ("foo" + "bar").length

length是一个函数,它将 String 转化为 Int 并保存 Monoid 结构。这样的结构称为Monoid 同态(homomorphism),一个 Monoid 同态 f 定义为在 Monoid M 和 N 之间对所有的值及 x、y 都遵守以下规则:

M.op(f(x), f(y)) = f(N.op(x, y))

当设计自己的库时这个特性很有用,加入两个类型是 Monoid 并且他们之前存在函数,好的做法是考虑这个函数是否可以保持 Monoid 结构,并且测试其是否为 Monoid 同态。

某些时候两个 Monoid 之间是双向同态的,**同质(isomorphic)**是在 M 和 N 之间存在的两个同态的函数 f 和 g,而且f andThen gg andThen f是等同的函数。

比如, String 和 List[Char] monoid 的连接操作是同质的。两个 Boolean monoid (false, ||) 和 (true, &&)通过取反(!)同样也是同质的。

5. 可折叠数据结构

现在需要为 IndexedSeq 实现一个折叠函数。一般处理这类数据结构中的额数据时,通常不在意具体结构是什么,也不在意是否延时或者提供有效的随机读写,等等。

比如有个结构中是整形,需要计算他们的总和,可以使用 foldRight:

ints.foldRight(0)(_ + _)

不需要关心 ints 的具体结构类型,他可以是 Vector、Stream 或其他列表,或者任何一个包含 foldRight 方法的类型。把这种通用性表达成下面的 trait:

trait Foldable[F[_]] {
  def foldRight[A,B](as:F[A])(z:B)(f: (A,B) => B): B
  def foldLeft[A,B](as:F[A])(z:B)(f: (B,A) => B): B
  def foldMap[A,B](as:F[A])(f: A => B)(mb:Monoid[B]): B
  def concatenate[A](as:F[A])(m:Monoid[A]): A = foldLeft(as)(m.zero)(m.op)
}

这里抽象出一个类型构造器 F,就像在之前章节中构建的 Parser 类型。表示为F[_],这里的下划线表示 F 不是一个类型而是一个类型构造器,它接收一个类型参数。就像接收别的函数作为参数的函数被称为高阶函数,Foldable 是高阶类型构造函数高阶类型

6. Monoid 组合

Monoid 的真正强大之处在于组合。比如 类型 A、B 是 Monoid,那么 Tuple 类型 (A, B) 也是 Monoid (称 product)。

6.1. 组装更加复杂的 Monoid

只需要包含的元素是 monoid,某些数据结构就能构建成 Monoid。比如当 value 类型是 Monoid 时,合并 key-value 映射的操作就能够构建 monoid:

def mapMergeMonoid[K, V](V: Monoid[V]): Monoid[Map[K, V]] = {
  new Monoid[Map[K, V]] {
    def zero = Map[K, V]()
    def op(a:Map[K,V], b:Map[K,V]) = 
      (a.keySet ++ b.keySet).foldLeft(zero) { (acc, k) =>
        acc.updated(k, v.op(a.getOrElse(k, V.zero), b.getOrElse(k, V.zero)))
      }
  }
}

使用这个简单的组合子(combinator)既可以组装出复杂的 Monoid:

val m:Monoid[Map[String, Map[String, Int]]] = 
  mapMergeMonoid(mapMergeMonoid(intAddition))

6.2.使用组合的 Monoid 融合多个遍历

多个 Monoid 可以被组合在一起,则折叠数据时可以同时执行多个计算。比如同时获得一个列表的总和与长度,来计算平均值:

val m = prodocutMonoid(intAddition, intAddition)
val p = listFoldable.foldMap(List(1,2,3,4,5))(a => (1,a))(m)
val mean = p.1 / p.2.toDouble	//=> 2.5

7. 总结

Monoid 是第一个纯抽象代数,它定义为抽象的操作和对应的法则。可以在不知道参数是什么,仅知道其类型可以构建 monoid的情况下编写可用的函数。

1.44 - 函数式与类型类

翻译自原文:On Scala, Functional Programming and Type-Classes

我曾经在 Coursera 上追随一个名为“ Functional Programming Principles in Scala”的精彩课程,该课程由 Martin Odersky(Scala 作者) 执教。这并不是我第一次遇到 Scala,因为我已经把它用在了日常工作当中。与此同时,我感觉需要找一个 Javascript 语言的替代者,因为优秀的 ClojureScript,我也开始了对 Clojure 的学习。

我对这两种语言都非常喜欢,真的说不上来更喜欢哪一个。这篇文档代表了我使用 Scala 的(菜鸟)经验,完全的瞎扯,或者你可以称它为“一个傻瓜的精神自慰”。

1. 函数式编程的双赢

它并非银弹,但整体来说非常棒。你真的有必要经历一次,同时撇开那些通过多年的必要技能而建立起的成见和偏见。学生学习起函数式编程会相对容易,他们并无任何经验,否则学习的过程将会很痛苦。

但在过去的 20 万年里我们进化的并不多,所以我们的大脑总是能在那些吸引我们内在兽性(inner-animal)的地方找到乐趣,对繁衍、吃饭、睡觉和躲避野兽感兴趣。学习是一种乐趣,但对于陌生的领域并非如此,因此如果你已经开始,那就要坚持下去。

首先我们需要一些对于函数式编程的定义:

  • 通过“引用透明”对函数求值来处理计算;(引用透明:函数的行为类似数学函数,相同的输入总会得到相同的结果)
  • 一个计算的最终输出是对输入的多次转换结果的组合,而非通过那些构建可变状态的方式;

一个函数式编程语言:

  • 将函数当做“一类(first-class)对象”,这表示处理高阶函数不但是可能的,而且是很以很舒服的方式;
  • 为你提供用于组合函数与类型的工具。

根据定义,像 Ruby、Javascript 这些也可以被认为是像样的函数式语言。然而我还要加几条:

  • 拥有丰富的不可变、持久化数据结构;
  • 提供有效的处理“expression problem”的类型系统。Rich Hickey 称之为“polymorphism a la carte

你也可以指定所有的副作用(side-effect)必须通过一元(monadic)类型来建模,不过这有点太清规戒律的意思(IMHO),因为只有一种符合主流的语言 - Haskell。

2. Scala 是一个函数式语言吗

当然是。你只需要追随上面我提到的 Coursera 上的精彩课程、完成作业,你就会意识到 Scala 真正是一个非常函数式的语言。该课程虽短,不过有后续计划。因此现在就行动吧….

3. Polymorphism À la Carte

这是我从 Rich Hickey 那听来的名词,当他谈论到开放式类型系统(open type-system),主要引用了 Clojure 的 Protocol 和 Haskell 的 Type-Class。

这些多态机制能够很好的解决表达式问题,这与我们已知的 Java、C++ 这些面向对象语言形成鲜明对比。

OOP 通常是一个封闭的类型系统(closed type-system),特别是在静态语言中使用时。将一个新类添加到层级结构、添加新函数来操作整个层级结构、给接口添加新的抽象成员、使内置类型以某种方式运转,所有这些都难以处理。

Haskell 通过 Type Classes 来处理。Clojure 通过  Multi-Methods 和 Protocol 来处理,Protocol 是动态的,相当于 动态类型系统(dynamic type-system)中的 type-class。

4. Yes Virginia,Scala 拥有 Type-Class

那什么又是 type-class?类似于 Java 中的接口,除了你可以使任何现有类型遵循它而不用修改该类型的实现。

比如,我们想要一个泛型函数能够将事物加起来….比如一个foldLeft()sum(),但是相较于如何 fold,你想要环境知道如何处理每个特殊的类型。

在 Java 或 C# 中这样做有很多问题:

  • 对于那些支持相加操作的类型,并没有为+定义接口,比如:Integer/BigInteger/BigDecimal/Float/String…
  • 我们需要从一些"0"开始(你想要折叠的列表可能为空)

或许你可以定义一个这样的类型类:

trait CanFold[-T, R]{
  def sum(acc:R, elem:T): R
  def zero: R
}

但是等等,这不就是一个类 Java 的接口吗?对,他就是。这就是 Scala 最棒的地方,Scala 中任何实例都是对象,任何类型(type)都是一个类(class)。

那又是什么让这个接口成为了一个 type-class?当然是因为“伴生对象中带有隐式参数的对象”(Objects in combination with implicit parameters)。我们看一下如何使用这些来实现sum函数:

def sum[A, B](list: Traversable[A])(implicit adder: CanFold[A, B]): B = 
  list.foldLeft(addr.zero)((acc,e) => adder.sum(acc,e))

因此,如果 Scala 编译器能够在作用域中找到一个为 A 定义的 隐式CanFold,就会使用它生产一个 B。它的出色表现在多个级别:

  • 类型 A 的隐式定义建立在返回类型 B 之上
  • 可以为任何你需要的类型定义一个CanFold,整数、字符串、列表等等等

隐式定义是有范围的,因此需要导入。如果你需要一些类型的默认隐式定义(全局可见),可以在CanFold特质的伴生对象中定义:

object CanFold{
  // default implementation for integers
  implicit object CanFoldInts extends CanFold[Int, Long] {
    def sum(acc:Long, e:Int) = acc + e
    def zero = 0
  }
}

使用时则和预期一样:

// notice how the result of summing Integers is a Long
sum(1 :: 2 :: 3 :: Nil)
//=> Long = 6

我不会骗你这些方式有多难学或者如何学,你最终会拉起头发,期盼这些都不再是问题的动态类型。然而你要分清 hard 和 complex 的区别,前者是相对的、主观上的,后者是绝对的、可观上的。

我们实现中的一个难题是如何为一个基本类型提供默认实现。这也是为什么在CanFold[-T,R]的定义中我们将类型参数 T 设为逆变(contravariant)。逆变性代表的意思是:

if B inherits from A (B <: A), then
CanFold[A, _] inherits from CanFold[B, _] (CanFold[A,_] <: CanFold[B,_])

这允许我们为任何 Traversable 定义一个 CanFold,该 CanFold 可以支持任何 Seq/Vector/List 等等。

implicit object CanFoldSeqs extends CanFold[Traversable[_], Traversable[_]] {
  def sum(x:Travrsable[_], y:Travsesable[_]) = x ++ y
  def zero = Traversable()
}

这可以将任何类型的Traversable相加。问题是会在过程中丢失类型参数:

sum(List(1,3,4) :: List(4,5) :: Nil)
//=> Traversable[Any] = List(1,2,3,4,6)

为什么我会说它难的原因是在我把头发拉出来之后,不得不去StackOverFlow请教怎么才能够返回一个Traversable[Int]。因此,你可以使用一个隐式的def替换之前的具体隐式对象,来帮助编译器识别容器中嵌入的类型:

implicit def CanFoldSeqs[A] = new CanFold[Traversable[A], Traversable[A]] {
  def sum(x: Traversable[A], y:Traversable[A]) = x ++ y
  def zero = Traversable()
}

sum(List(1,2,3) :: List(4,5) :: Nil)
//=> Traversable[Int] = List(1,2,3,4,5)

Implicit 比眼见的要灵活。显然编译器同样能够使用返回你需要的实例的函数,而不是具体的实例。作为一个旁注,我上面做的是很难的,甚至在 Haskell 中,因为子类化(sub-typing)是复杂的,但是 Clojure 中也很简单,因为你无需关注返回类型。

NOTE:上面的实现并不严谨,可能会发生冲突。

未完…

1.45 - 异步编程

翻译自:Asynchronous Programming and Scala

现在随处可见异步性的身影,同时它也被包括在并发性之内。这篇文章解释了什么是异步处理和它面临的挑战。

1. 介绍

它作为一个比多线程更加综合的概念,但是人们往往将二者混淆。如果需要一种关系来表示,可以是这样:

Multithreadiing <: Asynchrony

我们可以将异步计算表示成一个type

type Async[A] = (Try[A] => Unit) => Unit

如果这些Unit返回类型看起来很丑陋,那是因为异步本身就是丑陋的。一个异步计算可以是网络中拥有如下特征的任何任务(Task)、线程、进程或节点:

  • 在你程序的主流程之外执行,或者从调用者的角度来看,它并不在当前调用栈(call-stack)执行;
  • 接收一个回调,并在结果处理完成之后调用;
  • 它不能对结果在何时发送做出任何保证,甚至一点也不能保证一个结果会不被发送。

知道异步属于并发的范畴是很重要的,但多线程则没必要。要记得在 Javascript 中,大部分 I/O 操作都是异步的,甚至繁重的业务逻辑也被异步化处理(使用基于调度的 setTimeout)以保证接口是可响应的。但是并不涉及内核级别的多线程,Javascript 成了一个 N:1 的多线程化平台。

将异步化引入到程序中的同时也意味着你要面对并发问题,因为你无法知道异步计算具体何时会完成。因此,将多个异步计算的结果组合并在同一时间运行意味着你需要进行额外的同步操作,因此你也不能再依赖顺序。不能依赖顺序则会带来更多的不确定性。

维基百科:一个不确定的算法,相对于确定性的算法来说,尽管提供了相同的输入,可能会在不同的运行过程表现出不同的行为。一个并行算法因为竟态条件会在多次运行时以不同的方式执行。

Nondet

敏锐的读者可以会注意到这些类型随处可见,基于用例和规约做一些调整:

这些抽象有什么共同点呢?他们都提供了处理异步化的方式,其中一些更为优秀。

2. 巨大的错觉

我们喜欢假装能将函数的异步结果转换为同步:

def await[A](fa: Async[A]): A

问题的实质是我们不能假装这些异步处理与普通函数相同。如果你对此需要一刻,只需要了解一下为什么 CORBA 失败了。

针对异步处理,我们有以下非常常见的分布式计算谬误( fallacies of distributed computing):

  • 网络是可靠的
  • 延迟为 0
  • 带宽是无限的
  • 网络是完全的
  • 拓扑结构不会发生变化
  • 拥有一位管理员
  • 传输消耗为 0
  • 网络是同质的(homogeneous)

当然这些没有一条是真的。这意味着代码是按这些情形来编写的:极少的网络错误处理,忽略了网络延迟和丢包,忽略了带宽限制和随之而来的诸多不确定性。

人们尝试各种方式来对付这些问题:

有这么多不同的实现,是因为没有任何一种是适用于通用目的的机制来处理异步。没有银弹的窘境在这里很切题,内存管理和并发成为我们开发者面临的巨大问题。

注意 - 个人观点和一些碎碎念:人们喜欢吹嘘像 Golang 这样的 M:N 平台,然而我更偏向于 1:1 的多线程平台,比如 JVM 或 .NET。

因为你可以在编程语言中基于 1:1 平台搭建 M:N 的多线程来提供足够的表现力(比如:Scala 的 Future、Task、Clojure 的 core.async 等等),但是一旦 M:N 的运行时不再适用于你的场景,你则无法修改或替换平台。是的,大多数 M:N 平台都被一种方式或另一种打破。

真正的学习所有可行方案或做出选择是很痛苦的,但总比做出无知的选择要痛苦的少,TOOWTDI(?) 和 “worse is better”在这种情况下害处则会更大。人们在解释难于学习一门新的或更有表现力的语言时,比如 Scala 或 Haskell,往往没有提到点上,因为如果他们不得不处理并发问题,这是学习一种新的编程语言将会使他们最小的问题。我了解到一些人因为并发问题而离开了软件行业。

3. 回调地狱

让我们创建一个仿造的例子来阐明我们的疑问。比如开启两个异步处理并将他们的结果结合在一起。

首先定义一个异步执行的函数:

import scala.concurrent.ExecutionContext.global

type Async[A] = (A => Unit) => Unit

def timeTwo(n: Int): Async[Int] = {
  onFinish => {
    global.execute(new Runnable{
      def run():Unit = {
        val result = n * 2
        onFinish(result)
      }
    })
  }
}

// Usage
timesTwo(20) { result => println(s"Result: $result")}
// => Result: 40

3.1. 顺序化(副作用炼狱)

让我们来结合两个异步结果,以平滑的顺序让一个在另一个发生之后执行:

def timesFour(n:Int):Async[Int] = {
  onFinish => {
    timesTwo(n){ a =>
      timesTwo(n){ n =>
        // Combining the two results
        onFinish(a + b)
      }
    }
  }
}

// Usage
timesFour(20) { result => println(s"Result: $result")}
// => Result: 80

看起来很简单,但是我们仅结合了两个结果,一个跟在另一个之后。

巨大的问题仍然是它触及到的所有异步化副作用。我们假设由于参数的缘故我们以一个纯函数开始:

def timesFour(n:Int):Int

但是这是你的企业架构师听说了这些企业 JavaBean 和 a lap dance(?),决定让你基于这些异步的timsTwo函数。这时我们的timesFour实现从一个精确的纯函数编程一个有副作用的函数。同时伴随一个并不成熟的Async类型,我们需要面对在整个管道(pipeline)处理副作用。同时,阻塞结果也没有任何帮助,你只是隐藏了问题所在(第二节所述)。

但是等等,事情还会变得更糟。

3.2. 并行化(梦境中的不确定性)

第二个调用并不基于第一个调用,因此他们可以并行运行。在 JVM 我们可以并行运行 CPU-bound 的任务,但这并不适用于 Javascript,我们可以发起 Ajax 请求或于其他网页工作者(web worker)交谈。

不幸的是事情会变的有点复杂。首先使用所有自然(navie)方式来做都会非常错误:

// REALLY BAD SAMPLE

def timesFourInParallel(n:Int):Async[Int] = {
  onFinish => {
    var cacheA = 0
    
    timesTwo(n) { a => cacheA = a}
    
    timesTwo(n) { b =>
      // Combing the two results
      onFinish(cacheA + b)
    }
  }
}

timesFourInParallel(20) {result => println(s"Result: $result")}
// => Result: 80

timesFourInParallel(20) {result => println(s"Result: $result")}
// => Result: 40

这里的例子展示了实际中的不确定性。我们得不到顺序保证哪个会先结束,因此如果我们要并行执行,需要建模一个迷你状态机来进行同步。

首先,定义 ADT 来描述状态机:

// Define the state machine
sealed trait State
// Initial state
case object Start extends State
// We got a B, waiting for an A
final case class WaitForA(b:Int) extends State
// We got a A, waiting for a B
final case class WaitForB(a:Int) extends State

然后以异步的方式来演化这个状态机:

// BAD SAMPLE FOR THE JVM(only works for Javascript)

def timesFourInParallel(n:Int):Async[Int] = {
  onFinish => {
    var state:State = Start
    
    timesTwo(n) { a =>
      state match {
        case Start => state = WaitForB(a)
        case WaitForA(b) => onFinish(a + b)
        case WaitForB(_) => 
          // Can't be caught b/c async, hopefully it gets reported
          throw new IllegalStateException(state.toString)
      }
    }
    
    timesTwo(n) { b =>
      state match {
        case Start => state = WaitForA(b)
        case WaitForB(a) => onFinish(a + b)
        case WaitForA(_) => 
          // Can't be caught b/c async, hopefully it gets reported
          throw new IllegalStateException(state.toString)
      }
    }
  }
}

为了更好的视觉化我们处理的问题,下图是状态机:

Callback hell stm

但是等等,我们还没结束,因为 JVM 拥有真实的 1:1 多线程,这表示我们要沉浸于可共享内存的并行化,因此对state的访问必须是同步的。

一种方案是使用synchronized块,或称为intrinsic块:

// We need a common reference to act as our monitor
val lockkk = new AnyRef
var state:State = Start

timeTwo(n) { a =>
  lock.synchronized{
    state match {
      case Start =>
        state = WaitForB(a)
      case WaitForA(b) =>
        onFinish(a + b)
      case WaitForB(_) =>
        // Can't be caught b/c async, hopefully it gets reported
        throw new IllegalStateException(state.toString)
    }
  }
}

// ...

这种高级别的锁保护资源(eg. state)不被多线程并行访问。但我个人更倾向于避免这种高级别的锁,因为内核的调度器可以以任何原因冻结任何线程,包括持有锁的线程。冻结一个持有锁的线程意味着如果你想保证持续前进,而其他线程无法再继续前进,这是无阻塞(non-blocking)的逻辑则会更优先。

因此供替代的方式是使用一个AtomicReference,它会更适用这个场景:

// CORRECT VERSION FOR JVM

import scala.annitation.tailrec
import java.util.concurrent.atomic.AtomicReference

def timeFourInParallel(n:Int):Async[Int] = {
  onFinish =>{
    val state = new AtomicReference[State](Start)
    
    @tailrec def onValueA(a:Int):Unit = {
      state.get match{
        case Start => 
          if(!state.compareAndSet(Start, WaitForB(a))) onValue(a) // retry
        case WaitForA(b) => onFinish(a + b)
        case WaitForB(a) => throw new IllegalStateException(state.toString)
      }
    }
    
    timesTwo(n)(onValueA)
    
    @tailrec def onValueB(b:Int):Unit = {
      state.get match {
        case Start =>
          if (!state.compareAndSet(Start, WaitForA(b)))
            onValueB(b) // retry
        case WaitForB(a) =>
          onFinish(a + b)
        case WaitForA(_) =>
          // Can't be caught b/c async, hopefully it gets reported
          throw new IllegalStateException(state.toString)
      }
    }
    
    timesTwo(n)(onValueB)
  }
}

PRO-TIP:如果你想编写  Javascript / Scala.js 的交叉编译代码,基于性能调整和用于操作原子引用的酷炫工具类,可以尝试Monix中的Atomic

3.3. 递归(爆栈的愤怒)

如果我告诉你上面的onFinish调用并非栈安全(stack-unsafe)的,同时当你调用它时也不会强制异步边界(asynchronous boundary),这时你的程序会因为一个StackOverflowError爆炸,又该怎么办呢?

你不应该相信为的话。首先让我们找些乐子,同时以更通用的方式来定义上面的操作:

import scala.annotation.tailrec
import java.util.concurrent.atomic.AtomicReference

type Async[+A] = (A => Unit) => Unit

def mapBoth[A,B,R](fa:Async[A], fb:Async[b])(f:(A,B) => R): Async[R] = {
  // Defines the state machine
  sealed trait State[+A,+B]
  // Initial state
  case object Start extends State[Nothing, Nothing]
  // We got a B, waiting for an A
  final case class WaitForA[+B](b:B) extends State[Nothing, B]
  // We got an A, waiting for a B
  final case class WaitForB[+A](a:A) extends State[A, Nothing]
  
  onFinish =>{
    val state = new AtomicReference[State[A,B]](Start)
    
    @tailrec def onVlueA(a:A):Uint = {
      state.get match {
        case Start => 
          if(!state.compareAndSet(Start, WaitForB(a))) onValue(a) //retry
        case WaitForA(b) => onFinish(f(a,b))
        case WaitForB(a) =>
          throw new IllegalStateException(state.toString)
      }
    }
    
    @tailrec def onValueB(b:B):Unit = {
      state.get match{
        case Start =>
          if (!state.compareAndSet(Start, WaitForA(b)))
            onValueB(b) // retry
        case WaitForB(a) => onFinissh(f(a,b))
        case WaitForA(b) => 
          throw new IllegalStateException(state.toString)
      }
    }
    
    fa(onValueA)
    fb(onValueB)
  }
}

现在可以定义一个类似 Scala 中的Future.sequence操作,因为我们的意志坚强,勇气不可估量…..

def sequence[A](list:List[Async[A]]):Async[List[A]] = {
  @tailrec def loop(list:List[Async[A]], acc:Async[List[A]]): Async[List[A]] = {
    list match {
      case Nil =>
        onFinish => acc(r => onFinish(r.reverse))
      case x :: xs =>
        val update = mapBoth(x, acc)(_ :: _)
        loop(xs, update)
    }
  }
  
  vall empty:Async[List[A]] = _(Nil)
  loop(list, empty)
}

// Invocation
sequence(List(timesTwo(10), timesTwo(20), timesTwo(30))) {r =>
  println(s"Result: $r")
}
// => Result: List(20, 40, 60)

你一定认为我们完成了?

val list = 0.until(10000).map(timesTwo).toList
sequence(list)(r => println(s"Sum: ${r.sum}"))

注意看这个壮丽的内存错误,它会让你的程序在生产环境崩溃,被认为是一个致命错误,因此 Scala 的NonFatal也捕捉不到:

java.lang.StackOverflowError
  at java.util.concurrent.ForkJoinPool.externalPush(ForkJoinPool.java:2414)
  at java.util.concurrent.ForkJoinPool.execute(ForkJoinPool.java:2630)
  at scala.concurrent.impl.ExecutionContextImpl$$anon$3.execute(ExecutionContextImpl.scala:131)
  at scala.concurrent.impl.ExecutionContextImpl.execute(ExecutionContextImpl.scala:20)
  at .$anonfun$timesTwo$1(<pastie>:27)
  at .$anonfun$timesTwo$1$adapted(<pastie>:26)
  at .$anonfun$mapBoth$1(<pastie>:66)
  at .$anonfun$mapBoth$1$adapted(<pastie>:40)
  at .$anonfun$mapBoth$1(<pastie>:67)
  at .$anonfun$mapBoth$1$adapted(<pastie>:40)
  at .$anonfun$mapBoth$1(<pastie>:67)
  at .$anonfun$mapBoth$1$adapted(<pastie>:40)
  at .$anonfun$mapBoth$1(<pastie>:67)

如为所说,onFinish作为一个没有强制异步边界的调用会引起栈溢出错误。在 Javascript 中可以通过调度setTimeout来解决,而 JVM 则需要一个线程池或 Scala 的ExecutionContext

Are you feeling the fire yet? 🔥

4. Future & Promise

scala.concurrent.Future描述了完整的异步求值计算,和我们上面用的Async有点类似。

维基百科:Future 和 Promise 是在一些并发编程语言中用于异步程序执行的结构。它描述了一个对象,该对象看做是最初并不可知的结果的代理,通常因为该结果的值尚未计算完成。

作者的碎碎念:docs.scala-lang.org中关于 Futures and Promises是这样说的,“Future 提供了一个以并行方式执行多个操作的方法 -更加高效、无阻塞的方式。 ”这种说法容易产生误解,一个混淆的源头。

Future描述的是异步化而非并行化。当然,可以以并行的方式来使用,但并不意味者仅用作并行(async != Parallelism),或适用于那些寻找充分利用 CPU 容量的人,使用Future可以证明是昂贵和不明智的,因为在有些场景它会出现性能问题,参考本部分的第四小节。

Future是一个定义了两种主要操作的接口,同时附带一些基于这些主要操作实现的组合子:

import scala.util.Try
import scala.concurrent.ExecutionContext

trait Future[+T] {
  // abstract
  def value:Option[Try[T]]
  
  // abstract
  def onComplete(f:Try[T] => Unit)(implicit ec:ExecutionContext):Unit
  
  // Transforms values
  def map[U](f: T => U)(implicit ec:ExecutionContext):Future[U] = ???
  // Sequencing
  def flatMap[U](f: T => Future[U])(implicit ec:ExecutionContext):Future[U] = ???
}

Future的特性:

  • Eagerly evaluated(立即求值,strict and not lazy),意味着一旦调用者收到一个Future引用,无论异步处理要完成的是什么,它都已经开始了;
  • Memoized(记忆,cached),因为会被立即求值,它的行为更像一个正常值而不是一个函数,同时最终的结果会对所有的监听者(listener)可用。value的目的是用于返回记忆结果或尚未完成时返回None。Goes 并未提到会返回一个不确定的值;
  • 流经(stream)单个结果时它会显示,因为是记忆化起了作用。因此当监听者注册了完成时,他们最多会被调用一次。

ExecutionContext的解释性说明:

  • ExecutionContext管理异步执行,也可以把它视作一个线程池,但它并非必须是一个线程池(因为异步不等于多线程或并发);
  • onComplete和我们上面定义的Async类型一样,然而,它需要一个ExecutionContext,因为所有的完成时回调需要以异步的方式调用;
  • 所有的组合子和工具类都基于onComplete实现,因此所有的组合子和工具类都要提供一个ExecutionContext参数。

如果你不理解为什么这些签名都需要一个ExecutionContext,回到上面的“递归”部分,直到你完全理解了。

4.1. 顺序化

让我们使用Future重新定义“回调地狱”部分的函数:

import scala.concurrent.{Future, ExecutionContext}

def timesTwo(n:Int)(implicit ec:ExecutionContext):Future[Int] = Future(n * 2)

// Usage
{
  import scala.concurrent.ExecutionContext.Implicits.global
  timesTwo(20).onComplete{ result => println(s"Result: $result")}
  // => Result: Success(40)
}

足够简单,Future.apply创建器使用提供的ExecutionContext执行给出的计算。因此在 JVM 上,假设global执行上下文会运行在不同的线程上。

然后实现顺序化:

def timesFour(n:Int)(implicit ec:ExecutionContext):Future[Int] = 
  for{
    a <- timesTwo(n)
    b <- timesTwo(n)
  } yield a + b

// Usage
{
  import scala.concurrent.ExecutionContext.Implicits.global
  timesFour(20).onComplete {result => println(s"Result: $result")}
  // => Result: Success(80)
}

足够简单。这里的for 表达式魔法仅仅是会被转换为flatMapmap,在字面上等同于:

def timesFour(n:Int)(implicit ec:ExecutionContext):Future[Int]={
  timesTwo(n).flatMap{ a=>
    timesTwo(n).map{ b=>
      a + b
    }
  }
}

如果你在项目中导入了scala-async,可以像下面这样实现:

import scala.async.Async.{async, await}

def timesFour(n:Int)(implicit ec:ExecutionContext):Future[Int]={
  async{
    val a = await(timesTwo(n))
    val b = await(timesTwo(n))
    a + b
  }
}

扩展库scala-async由 macros 驱动,并将你的代码转换为flatMapmap调用。因此,await并不会阻塞线程,尽管它带来了这种错觉。

这些看起来确实不错,不幸的是拥有很多限制。当你的await处于匿名函数之内时,库将无法“重写”你的代码,不幸的是 Scala 代码中到处都是这种表达式。这将不会工作:

// BAD SAMPLE
def sum(list:List[Future[Int]])(implicit ec:ExecutionContext):Future[Int] = {
  async{
    var sum = 0
    // Nope, not going to work because "for" is translated to "foreach"
    for(f <- list){
      sum += await(f)
    }
  }
}

这种方式带来了拥有first-class continuations的幻觉,但是这种扩展并非一等类,仅仅是作为由编译器管理的重写代码。使得,这种约束在 C# 和 ECMAScript 中却应用的很好,因为async代码并不严重依赖于函数式

还记得我前面的碎碎念中提到的没有银弹

4.2. 并行化

像先前的例子中展示的,这两个函数互相独立,因此我们可以并行调用他们。使用Future则会更加简单,尽管求值语义对于新手来说会有点迷惑:

def timesFourInParallel(n:Int)(implicit ec:ExecutionContext):Future[Int] = {
  // Future is eagerly evaluated, so this will trigger the
  // execution of both before the composition happens.
  val fa = timesTwo(n)
  val fb = timesTwo(n)
  
  for{
    a <- fa
    b <- fb
  } yield a + b
  // fa.flatMap(a => fb.map(b => a + b))
}

这会有点迷惑,领新手措手不及。因为在这种执行模型中,为了以并行的方式执行,你需要在组合发生之前初始化这些Future引用。

一种可替代的方式是使用Future.sequence,可以用于任意集合:

def timesFourInParallel(n:Int)(implicit ec:ExecutionContext):Future[Int] = 
  Future.sequence(timesTwo(n) :: timesTwo(n) :: Nil).map(_.sum)

这种用法估计也会让新手吃惊,因为这些Future仅会当传入sequence的集合是精确的时候才会以并行的方式执行,不像 Scala 的StreamIterator。显然这个名字是个误称。

4.3. 递归

Future类型对于递归操作是绝对安全的,因为信心在于执行回调的ExecutionContext。因此重试前面的例子:

def mapBoth[A,B,R](fa:Future[A], fb:Future[B])(f:(A,B) => R)(implicit ec:...) = {
  for{
    a <- fa
    b <- fb
  } yield f(a,b)
}

def sequence[A](list:List[Future[A]])(implicit ec:...):Future[List[A]] = {
  val seed = Future.successful(List.empty[A])
  list.foldLeft(seed)((acc,f) => for(1 <- accl; a <- f) yield a :: l).map(_.reverse)
}

// Invocation
{
  import scala.concurrent.ExecutionContext.Implicits.global
  sequence(List(timesTwo(10), timesTwo(20), times(30))).foreach(println)
  // => List(20, 40, 60)
}

这次则不会出现StackOverflowError:

val list = 0.until(10000).map(timesTwo).toList
sequence(list).foreach(r => println(s"Sum: ${r.sum}""))

4.4. 性能代价

Future的麻烦是每次调用onComplete都会使用一个ExecutionContext来执行,通常这意味着一个Runnable被发送到了线程池,像这样分支(fork)一个逻辑线程。如果你拥有 CPU 绑定的任务,这种实现细节对性能来说是一种灾难,因为跳跃的线程意味着 context switches,同时会带来 CPU 的cache locality被摧毁。当然,该实现拥有确定性的优化,比如flatMap的实现中使用一个内部的蹦床形式的(trampolined?)执行上线文,为了避免在链接这些内部回调时进行分支,但是这还不够并且基准测试也不会说谎。

同时基于它的记忆化,在完成之上,实现会强制每个生产者执行一个AtomicReference.compareAndSet,在每个Future完成之前又会为每个注册的监听者加上一个compareAndSet。这些调用是十分昂贵的,所有这些都是为了记忆化以便在多个线程之间能够良好运行。

换句话说,如果你想让你的 CPU 绑定任务能够充分利用 CPU,这时使用FuturePromise不是一个好注意。

如果你想对比 Scala 的FutureTask实现,可以看一下相关benchmark:

[info] Benchmark                   (size)   Mode  Cnt     Score     Error  Units
[info] FlatMap.fs2Apply             10000  thrpt   20   291.459 ±   6.321  ops/s
[info] FlatMap.fs2Delay             10000  thrpt   20  2606.864 ±  26.442  ops/s
[info] FlatMap.fs2Now               10000  thrpt   20  3867.300 ± 541.241  ops/s
[info] FlatMap.futureApply          10000  thrpt   20   212.691 ±   9.508  ops/s
[info] FlatMap.futureSuccessful     10000  thrpt   20   418.736 ±  29.121  ops/s
[info] FlatMap.futureTrampolineEc   10000  thrpt   20   423.647 ±   8.543  ops/s
[info] FlatMap.monixApply           10000  thrpt   20   399.916 ±  15.858  ops/s
[info] FlatMap.monixDelay           10000  thrpt   20  4994.156 ±  40.014  ops/s
[info] FlatMap.monixNow             10000  thrpt   20  6253.182 ±  53.388  ops/s
[info] FlatMap.scalazApply          10000  thrpt   20   188.387 ±   2.989  ops/s
[info] FlatMap.scalazDelay          10000  thrpt   20  1794.680 ±  24.173  ops/s
[info] FlatMap.scalazNow            10000  thrpt   20  2041.300 ± 128.729  ops/s

可以看到 Monix Task在 CPU 绑定的任务上击败了Future

注意:这些基准测试是有局限的,仍然有一些用例中Future会更快(eg. Monix Observer使用Future用做背压)并且性能通常并不相关,比如执行 I/O,即那些吞吐并非 CPU 绑定的场景。

5. Task,Scala 的 IO Monad

Task是一种用于控制可惰性、可异步计算的数据类型,可用于控制副作用、避免非确定性和回调地狱。

Monix 库从 Task in Scalaz 获得灵感,提供了一种非常精致的 Task 实现。相同的概念,但实现不同。

Task类型同样汲取了来自 Haskell’s IO monad 的灵感,而作者认为这是真正的 Scala IO 类型。

该问题存在争论,因为 Scalaz 同样暴漏了一个仅用于处理异步计算的IO类型。Scalaz 的 IO并非异步,这表示它的描述并不完整,因为在 JVM 之上你必须以某种方式表示异步计算。另一方面,在 Haskell 中的你拥有转换成IO类型的Async类型,或许这是由运行时管理的(green-threads and all)。

在 JVM 之上的 Scalaz 实现中,我们无法使用IO在求值过程中以不阻塞线程的方式来表达异步计算,这是要避免的,因为阻塞线程则意味着倾向于错误

总的来说,Task类型:

  • 建模惰性、异步求值
  • 建模一个生产者向一个或多个消费者仅发送一个值
  • 它是惰性求值,因此对比Future它并不会触发执行,或在runAsync之前都不会有任何效果
  • 不会被求值记忆化(memoized),但是 Monix 的Task可以
  • 无需再另一个逻辑线程执行

而 Monix 中的实现拥有更多特别之处:

  • 允许取消一个运行中的计算
  • 在其实现中永远不会阻塞任何线程
  • 没有暴露任何可以阻塞线程的 API 调用
  • 所有异步操作都是栈安全的(stack safe)

Task在设计上的可视化表示:

EagerLazy
SynchronousA() => A
Coeval[A], IO[A]
Asynchronous(A => Unit) => Unit(A => Unit) => Unit
Future[A]Task[A]

5.1. 顺序化

使用Task重新定义第三节中的函数:

import monix.eval.Task

def timesTwo(n:Int):Task[Int] = Task(n * 2)

// Usage
{
  // Our ExecutionContext needed on evaluation
  import scala.concurrent.Scheduler.Implicits.global
  
  timesTwo(20).foreach{ result => println(s"Result: $result")}
  // => Result: 40
}

代码看起来和第四节中Future的版本一样,唯一的区别是新的timesTwo函数不再接受ExecutionContext作为参数。这是因为Task引用是惰性的,和函数类似,因此在调用强制求值发生的foreach之前什么都不会打印。我们需要的是一个 Scheduler,这是 Monix 中增强的ExecutionContext

现在实现 3.1 节中的顺序化:

def timesFour(n:Int):Task[Int] = {
  for{
    a <- timesTwo(n)
    b <- timesTwo(n)
  } yield a + b
}

// Usage
{
  import scala.concurrent.Scheduler.Implicits.global
  
  timesFour(20).foreach{ result => println(s"Result: $result")}
  // => Result: 80
}

同样是和Future类型一样,for 表达式魔法仍然是被 Scala 编译器转换成flatMapmap调用,字面值等同于:

def timesFour(n:Int):Task[Int] = {
  timesTwo(n).flatMap{ a=>
    timesTwo(n).map{ b=>
      a + b
    }
  }
}

5.2. 并行化

Task的并行化比Future要好的多,因为Task在分支 task 时支持细粒度控制,当在当前线程和调用栈执行转换(eg. map/flatMap)时,局域性的缓存保留和避免上下文切换则等同于顺序执行。

但是首先,转换成Future的形式并不能正常工作:

// BAD SAMPLE, 为了达成并行,这实质上会是顺序化
def timesFour(n:Int):Task[Int] = {
  // 并不会触发执行,因为 Task 是惰性的
  val fa = timesTwo(n)
  val fb = timesTwo(n)
  // 因为惰性的缘故求值会是顺序化
  for{
    a <- fa
    b <- fb
  } yield a + b
}

想要达到并行化,必须显示指定:

def timesFour(n:Int):Task[Int] = 
  Task.mapBoth(timesTwo(n), timesTwo(n))(_ + _)

是不是mapBoth看起来很熟悉?如果这两个任务在执行时分支线程,mapBoth会同时启动两者,从而达到并行化。

5.3. 递归

Task支持递归,栈安全且十分有效,这是基于其内部的 trampoline。你可以查看这篇 Rúnar Bjarnason 的论文 Stackless Scala with Free Monads 来了解其为何如此有效。

sequence实现与Future非常相似,只不过你会在sequence的签名中发现其惰性化:

def sequence[A](list:List[Task[A]]): Task[List[A]] = {
  val seed = Task.now(List.empty[A])
  list.foldLeft(seed)((acc, f) => for{
    l <- acc
    a <- f
  } yield a :: l).map(_.reverse)
}

// Invocation
{
  import monix.execution.Scheduler.Implicits.global
  
  sequence(List(timesTwo(10), timesTwo(20), timesTwo(30))).foreach(println)
  // => List(20, 40, 60)
}

6. 函数式编程和 Type-class

当你使用这些众所周知的函数时,比如:mapflatMapmapBoth,我们不再关心这一切的背后是一个(A => Unit) => Unit,因为这些函数会假设为合法、纯净、透明的。这意味着我们可以脱离其上下文来推导它们的结果。

这是 Haskell 中IO的伟大成就。Haskell 不会“伪造(fake)”副作用,因为返回IO函数字面意义上是纯的,副作用会被推迟到其所属程序的边缘。我们可以同样看待Task。不过,对于Future急切的本性(立即计算)来说会更加复杂,但是使用Future也不是一个坏的选择。

那么我们能够基于这些类型,比如TaskFutureCoevalEvalIOIdObservable或者一些其他的类型,来创建接口或抽象吗?

当然我们可以,我们已经见过使用flatMap来描述顺序化,使用mapBoth来描述并行化。但是我们不能使用经典的 OOP 来描述他们,其中一个原因是Functional参数的协变和逆变规则,这会导致我们在flatMap中失去类型信息(除非你使用 F-bounded 泛型类型,这样更适合实现复用或其他 OOP 语言不可用时),同时我们要描述一个数据构造器,他不能是一个方法(比如 OOP 的子类应用到实例而不是整个类)。

幸运的是,Scala 是极少数支持高阶类型且能够编码类型类(type-class)的语言,这意味着我们拥有了从 Haskell 端口概念所需要的一切。

作者的碎碎念:MonadApplicativeFunctor,这些可怕的单词让那些并不忠实的人心生畏惧,导致他们认为关注的是一些与现实世界脱轨的“学术”概念,书籍作者要避免大量使用这些单词,包括 Scala 的 API 文档及官方教程。

但这是给 Scala 和其用户帮倒忙。其他语言中仅有的设计模式主要是难于解释,因为这些不能用类型来表示。你可以用一只手输出拥有这种表达能力的语言。而用户痛苦的是当他们遇到麻烦时不知如何从现有的文献中搜索相关主题,缺失对正确术语的学习。

我也觉得这是一味地反智主义(anti-intellectualism),向往常一样对无知的恐惧。你可以发现这些都来自真正做他们的人,但我们无一幸免。比如 Java 中的Optional类型违反了 Functor 的规则(e.g. opt.map(f).map(g) != opt.map(f andThen g)),Swift 中愚蠢的 5 == Some(5),可以幸运的向人们解释Some(null)实际上与null的意义相同,是AnyRef的有效值,因为不然的话你不能定义一个Applicative[Option]

6.1. Monad(顺序化和递归)

本文并不会解释 Monad。另外有一篇文章来专门解释它。但如果你想建立一个直觉,这里有另外一个:在数据类型的上下文中,比如FutureTask,Monads 用于描述操作的顺序,并且是保证顺序的唯一有效方法。

Observation: programmers doing concurrency with imperative languages are tripped by the unchallenged belief that “;” defines sequencing.” – Aleksey Shipilëv

Scala 中一个简单的编码Monad的例子:

// we shouldn't need to do this
import scala.language.higherKinds

trait Monad[F[_]]{
  /** Constructor (said to lift a value `A` in the `F[A]`
    * monadic context). Also part of `Applicative`, see below.
    */
  def pure[A](a: A): F[A]

  /** FTW */
  def flatMap[A,B](fa: F[A])(f: A => F[B]): F[B]
}

同时提供一个Future实现:

import scala.concurrent._

// Supplying an instance for Future isn't clean, ExecutionContext needed
class FutureMonad(implicit ec: ExecutionContext)
  extends Monad[Future] {

  def pure[A](a: A): Future[A] =
    Future.successful(a)

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

object FutureMonad {
  implicit def instance(implicit ec: ExecutionContext): FutureMonad =
    new FutureMonad
}

这真是一个强力的东西。现在我们可以描述一个用于TaskFutureIO的泛型函数,无论如何,如果flatMap是栈安全的话这将非常伟大:

/** Calculates the N-th number in a Fibonacci series. */
def fib[F[_]](n: Int)(implicit F: Monad[F]): F[BigInt] = {
  def loop(n: Int, a: BigInt, b: BigInt): F[BigInt] =
    F.flatMap(F.pure(n)) { n =>
      if (n <= 1) F.pure(b)
      else loop(n - 1, b, a + b)
    }

  loop(n, BigInt(0), BigInt(1))
}

// Usage:
{
  // Needed in scope
  import FutureMonad.instance
  import scala.concurrent.ExecutionContext.Implicits.global

  // Invocation
  fib[Future](40).foreach(r => println(s"Result: $r"))
  //=> Result: 102334155
}

注意:这只是一个玩具样例,严肃的实现参考 Typelevel’s Cats

6.2. Applicative(并行化)

Monad 定义了操作的顺序化,但是有时我们想组合那些互不依赖的计算的结果,他们可以同时求值,或者并行化。还有一个例子可以证明 Applicative 比 Monad 更加可组合。

现在扩展我们的Typeclassopedia:

trait Functor[F[_]] {
  /** I hope we are all familiar with this one. */
  def map[A,B](fa: F[A])(f: A => B): F[B]
}

trait Applicative[F[_]] extends Functor[F] {
  /** Constructor (lifts a value `A` in the `F[A]` applicative context). */
  def pure[A](a: A): F[A]

  /** Maps over two references at the same time.
    *
    * In other implementations the applicative operation is `ap`,
    * but `map2` is easier to understand.
    */
  def map2[A,B,R](fa: F[A], fb: F[B])(f: (A,B) => R): F[R]
}

trait Monad[F[_]] extends Applicative[F] {
  def flatMap[A,B](fa: F[A])(f: A => F[B]): F[B]
}

然后扩展我们的Future实现:

// Supplying an instance for Future isn't clean, ExecutionContext needed
class FutureMonad(implicit ec: ExecutionContext)
  extends Monad[Future] {

  def pure[A](a: A): Future[A] =
    Future.successful(a)

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

  def map2[A,B,R](fa: Future[A], fb: Future[B])(f: (A,B) => R): Future[R] =
    // For Future there's no point in supplying an implementation that's
    // not based on flatMap, but that's not the case for Task ;-)
    for (a <- fa; b <- fb) yield f(a,b)
}

object FutureMonad {
  implicit def instance(implicit ec: ExecutionContext): FutureMonad =
    new FutureMonad
}

现在可以基于Applicative定义泛型函数并用于Future

def sequence[F[_], A](list: List[F[A]])
  (implicit F: Applicative[F]): F[List[A]] = {

  val seed = F.pure(List.empty[A])
  val r = list.foldLeft(seed)((acc,e) => F.map2(acc,e)((l,a) => a :: l))
  F.map(r)(_.reverse)
}

注意:同样是一个玩具样例,参考Typelevel’s Cats

6.3. 为异步求值定义类型类?

上面的部分缺少的是真正触发计算并获得结果值。思考 Scala 的Future,我们想要一种方式来抽象onComplete。想象 Monix 中我们想要抽象runAsync。想象 Haskell 和 Scalaz 的IO,我们想要抽象unsafePerformIO

FS2 库中定义了一个名为 Effect 类型类来这样做:

trait Effect[F[_]] extends Monad[F] {
  def unsafeRunAsync[A](fa: F[A])(cb: Try[A] => Unit): Unit
}

这看起来向我们初始的Async类型,跟Future.onCompleteTask.runAsyncIO.unsafePerformIO很相似。

然而,它并非真正的类型类:

  • 它是非法的,然而也不足以取消他的资格(after all, useful lawless type-classes like Show exist),但最大的问题是….
  • 如 3.3 中所示,为了避免StackOverflowError,我们需要某种执行上下文或线程池来执行异步任务且不会导致栈溢出。

但是这样的执行上下文根据实现不同而不同。Java 使用 Executor,Scala 的Future时候使用ExecutionContext,Monix 使用增强自ExecutionContextScheduler。FS2 和 Scalaz 使用 包装自ExecutorStrategy来 fork 线程,但是调用unsafePerformIOrunAsync时并不会注入上下文(这也是为什么很多 Scalaz 组合子实际上并不安全)。

我们可以采取与Future同样的策略,从作用域中获取一个implicit whatever: Context来创建实例。但这有点尴尬且效率低下。这也意味这不使用上下文的情况下我们仅仅不能为Effect.unsafePerformIO定义flatMap。如果我们不能这样做,这时也不会继承自Monad,因为它没有必要是一个Monad

因此为个人也不是很确定 - 如果你对 Cats 有什么好的建议,我愿洗耳恭听。

我希望你喜欢这个思想试验,设计东西是很有趣的。

7. 选择正确的工具

一些抽象较其他会更为通用,为个人认为"为工作选择正确的工具"这样的口头禅是过度保护可怜人的选择。

为此,Rúnar Bjarnason 有一个非常有意思的表述,名为 Constraints Liberate, ,而 Liberties Constrain 最终真正道出了并发抽象的本质。

如前所述,并没有银弹能够通用的解决并发问题。抽象的层次越高,能解决的问题视野也就越少。但是更少的视野和其强大的能力,模型也会更加简单更加可组合。比如 Scala 社区中很多开发者滥用 Akka Actor,这个库在不被误用时是很伟大的。就像能够使用FutureTask时不用 Actor。同样在其他的抽象中,比如 Monix 或 ReactiveX 中的Observable抽象。

同样用心学习下面两条规则:

  • 避免使用回调、线程和锁,它们易错且不可组合;
  • 避免像瘟疫一样并发(avoid concurrency like the plague it is)

最后让我告诉你,并发专家首先是避免并发的专家….

1.46 - 战略Scala风格

Strategic Scala Style: Practical Type Safety一文的中文翻译,点击查看原文。

这篇文章探索了如何利用Scala的类型安全特性来避免在使用Scala编写程序时出错。

虽然Scala有一个编译器帮助你捕捉错误,或者称它为类型安全,实际上有一个全方位的方式让你能够编写更多或更少安全性的Scala。我们将会讨论多种方式让你把你的代码转变为更加安全的系列。我们将有意识地忽略那些绝对的证明和逻辑事物的理论方面,而更专注于实践的方式来使编译器帮你不做更多隐藏的BUG。

Type Safety涉及面很广。你需要投入整个生涯来学习Haskell的类型系统或Scala变成语言,并且需要花费另一个周期来学习Haskell运行时的的类型实现或Java虚拟机。这里将会忽略这两个内容。

取而代之,本文将会以实践的方式来介绍如何以“类型安全”的方式来使用Scala,编译器的知识可以让你减轻错误的后果,并且能够把这些错误编程简单的、能够在开发期间完成修改的错误,以此来提高你代码的安全性。一个有经验的Scala开发者会发现本文的“基础“和”显而易见“,但是任何新手将会期望将这些技术添加到工具箱来解决Scala中遇到的问题。

这里的每一种技术描述都会做一些权衡:冗长、复杂、额外的类文件、低劣的运行时性能。本文我们会忽略这些问题而把他们当做是相当完美的。本文只会列举可能的情况,而不会去深入讨论有些取舍是否值得。而且,本文仅作用域纯粹的Scala,不会涉及类似Scalaz、Cats、Shapeless这样的第三方扩展库。如果属性该类技术的人愿意去写的话,这些库应该有他们自己的风格或技术并在他们自己的文章中展示。

原理

在我们讨论具体的技术和围绕类型安全的取舍之前,停下来思考一下问题本质是有意义的。什么是一个类型?”安全“一词有意味这什么?

什么是类型(Type)

在一个程序的运行时,你对一个值的了解就是Type。

基本上所有的编程语言都有一个不同的类型系统。一些有泛型,一些有具体化的泛型。例如Java,有具体化的泛型,一个值的类型总是与一个类护着接口符合并能在运行时检查。其他的,比如C就不能。Python这样的动态语言没有静态类型,因此类型仅存在于运行时。

本文所讲的Scala语言,拥有它自己的相对复杂的特定的类型系统。也有一些尝试将其正式化,比如Dotty项目。本文将会忽略这些。

本文中将会依照上面的释义。在一个程序的运行时,你对一个值的了解就是Type。比如:

  1. 一个Int定义为包含了从-21474836482147483647的32位整数;
  2. 一个Option[T]定义为要么是一个Some[T],要么是一个None
  3. 一个CharSequence定义为包含了一些Char并支持我们调用.length、.chatAt、.subsequence等方法,但是我并不知道它是一个StringString或别的什么。你并不知道它是否可变(mutable/immutable),它如何保存它的内容,或者性能规格如何;
  4. 一个String同样拥有一些字符,但你知道它是不可变的,使用一个内部的字符数组来保存内容,通过索引来查找Char的时间复杂度我O(1)

一个值的类似告诉你某些东西是什么和它不能是什么。Option[String]可能是SomeNone,但它绝不会是一个32位的整数!在Scala中,这些是你不需要检查的:这些是你可以依赖的正确事物,编译器会在编译器为你检查。

这个知识点确切的指出了一个值的类型包含了什么。

类型不是什么

A Class

这个定义中,类型不是一个类(Class)。对,在基于JVM之上的Java和Scala,所有的类型被描述为类(class)或接口(interface),这在Scala.js中并不有效,你可以定义一个假的类型(trait继承自js.Any),在编译之后不会留下任何残留,或在其他编程语言中。

虽然类型被类所支持是一个事实,这只是一个实现细节并且与本位无关。

Scala类型系统

这里讨论的类型概念是含糊的、宽泛的,基于所有语言而不仅仅是Scala。Scala自身的类型系统是复杂的,有类,抽象类型,细化类型,特质,类型界限,上下文界限,和一些其他更加晦涩的东西。

纵观本文,这些细节都是为了服务一个目的:在你程序中将你对值的了解描述给编译器,然后让他检查你现在做的,与你说的和想要做的是否一致。

什么是安全

类型安全意味着一点你出错,后果的影响比较小。

相比类型,可能有其他更多对”安全“的定义。上面的定义比类型有宽泛:它作用于安全实践、隔离、分布式系统中的健壮性和恢复性,还有其他一些事情。

人们会犯各种错误:代码排版、可怜的负荷估算、复制粘贴错误的命令。当你出错时发生了什么?

  • 你会看到编辑器中红色表示然后5s内修复了它;
  • 你想完整的编译,花费了10s,然后修复了它;
  • 你运行测试用例,花费了10s,然后修复了它;
  • 你部署了这个错误,然后几个小时以后才发现它,然后修复后再部署;
  • 你部署了这个错误,这个错误几周内都没有被提醒,但是在提醒后修复它需要花费几周的时间来清理它遗留的错误数据;
  • 你部署了错误,然后发现你的公司45分钟后破产了。你的工作、团队、你的组织和计划,都没了。

忽略类型和运行时的概念,很明显不同的环境有不同的安全级别。只要捕捉够早或异步debug,甚至运行时错误都会造成更小的影响,Python中的习惯,当不匹配时在运行时抛出TypeError,似乎必Php中当不匹配时进行强制执行要安全的多。

什么是类型安全

类型安全是利用我们在运行时对一个值的了解来尽量降低大部分错误的后果。

比如,一个”较小的后果“可以被看做是开发期间就能够发现的易于理解的编译错误,然后花费30s完成修复。

这个定义直接准照上面我们对”类型“和”安全“的定义。这比很多类型安全的定义都要宽泛,特别是:

  • 类型安全不是编写Haskell,这个概念要更为宽泛;
  • 类型安全不是避免可变状态,除非它有助于我们的目标;
  • 类型安全不是一个绝对的目标,是一个尽量和优化的属性;
  • 类型安全对每个人都不同;如果不同的人犯不同的错,这些错都有不同的危害级别,他们需要完善不同的事情来尽力优化这些错误的危害;
  • 如果错误消息不可理解并难于解决,编译器甚至也会有严重的影响。一个能在10s内修复的优雅的编译器消息和一个需要半小时才能理解的巨大的编译器错误消息是完全不同的。

类型安全的定义多种多样;如果你问一个C++开发者、一个Python的网站开发者或者一个研究编程语言的教授,他们会给出各自截然不同的定义。本文中,我们会使用上面宽泛的定义。

Scalazzi Scala

很多人思考了很多关于若何以类型安全的方式编写Scala。所谓的Scala语言的“Scalazzi Subset”是其中一个哲学:

NAME

当然这些指导方针有很多地方需要讨论,我们会花一些时间浏览其中一部分,同时我发现了一些有意思的地方:

  • 避免空值
  • 避免异常
  • 避免副作用

避免空值

使用null来描述一些空的、未初始化或不可用的值会很吸引人,比如,一个未初始化的值:

class Foo{
	val name: String = null // override this with something useful if you want
}

或者是传入到函数的一个”没有值“的参数:

def listFiles(path: String) = ...

listFiles("/usr/local/bin/")
listFiles(null) // no argument, default to current working directory

“Scalazzi Scala”告诉我们要避免这样做,并给了一个很好的理由:

  • null会出现在程序的各个角落,任何一个变量或值,没有办法控制那些变量是null而哪些不是;
  • null在你的程序中到处传播:可以将null传入函数,赋给其他变量,或存入集合。

最终,这意味着null值会在原理他们初始化的地方一起错误,然后就很难被追踪。当有些地方被NullPointerException 终止,你需要首先找到那些可疑的变量(每行代码或许会有很多变量),然后进行追踪,比如函数的传入和传出,集合的存储和检索,直到你找到null的来源。

在Python这样的动态语言中,这种类型的错误值传播很普通,寻常不会贯穿整个程序来进行追踪,然后到处添加print语句,尝试去找到初始值的来源。通常有人简单的将参数混入到一个函数,传入一个user_id而不是user_email或其他不重要的值,但是会照成很大的后果来追踪和调试。

在一个带有类型检查器的编译型语言,比如Scala,许多这样的错误会在你运行编译器之前就能捕获:在于其为一个String的地方传入IntSeq[Double]会得到一个类型错误。并不是所有的错误都会被捕捉,但是会捕捉大部分严重的错误。预期为不是null的地方传入一个null除外。

这里有一些null的备选方案:

如果想要表达一个可能存在的值,一个函数参数或者一个需要被覆写的类属性:

class Foo{
	val name: String = null // override this with something useful if you want
}

考虑使用Option[T]替换:

class Foo{
	val name: Option[String] = None // override this with something useful if you want
}

"foo"null取而代之为Some("foo")None看起来很相似,但是这样做的话所有人都会知道它可能为None,而不会像如果将一个Some[String]放到预期为String的地方然后跟null得到一个编译错误。

如果使用null作为一个未初始化var值的占位符:

def findHaoyiEmail(items: Seq[(String, String)]) = {
	var email: String = null // override this with something useful if you want

	for((name, value) <- items){
		if (name == "Haoyi") email = value
	}

	if (email == null) email = "Not Found"
	doSomething(email)
}

考虑替换为val并一次完成声明和初始化:

def findHaoyiEmail(items: Seq[(String, String)]) = {
  val email = 
    items.collect{case (name, value) if name == "Haoyi" => value} 
         .headOption
         .getOrElse("Not Found")
  doSomething(email)

如果你不能够在一行代码内初始化email的值,Scala支持你将片段的代码放到柯里化的{}中同时将其赋给一个val, 因此, 大部分你需要稍后初始化为var的代码都可以放到一个{}中然后声明并初始化为一个{}.

def findHaoyiEmail(items: Seq[(String, String)]) = {
  val email = {
    ...
  }
  doSomething(email)
}

这样做的话,我们就能控制email永远不会是一个null.

通过简单的在程序中避免null,你并没有改变理论状况, 理论上有人可以传入一个null,你会在同样的地方追踪那些难于调试的问题.但是你改变了实践环境: 不会花费更少的实践来追踪难于调试的NullPointerException问题.

避免异常

异常基本上是一段代码的额外返回值.任何你写的代码都可以通过return关键字以正常的方式返回,或者简单的返回最后一个代码块的表达式,或者是抛出一个异常. 这些异常会包含任意的数据.

虽然一些其他语言比如Java,用编译器来检查你可以确定的能够抛出的异常,它的"受检异常"也并不是很成功: 它的不便之处在于必须要声明你抛出的需要检查的异常,以至于人们只是给他们的方法都使用一个throws Exception,或者捕获受检异常后重新作为未检查的运行时异常抛出.后期的语言比如C#和Scala完全抛弃了这种受检异常的思想.

为什么你不可以使用异常:

  • 你没有办法静态的知道一段代码都能抛出哪些种类的异常. 即你不知道是否处理了代码所有可能的返回类型.
  • 你抛出的异常的注解是可选的,and trivially fall out of sync with reality as development happens and refactorings occur.
  • 他们是传播的,so even if a library you’re using has gone through the discipline of annotating all its methods with the exceptions they throw, the chances are in your own code you’ll get sloppy and won’t.

与其返回一个异常,在只有一种失败模式的函数中,你可以返回一个Option[T]来表示结果,或者Either[T, V],再或者是你自己定义的密闭trait来表示有多重失败模式的返回结果.

sealed trait Result
case class Success(value: String) extends Result
case class InvalidInput(value: String, msg: String) extends Result
case class SubprocessFailed(returncode: Int, stderr: String) extends Result
case object TimedOut extends Result

def doThing(cmd: String): Result = ???

使用密闭trait方式,你可以更易于与用户沟通存在的准确错误,在每种场景可用的数据,同时当用户对doThing的结果进行match时,如果少了一个场景,编译器则会给出一个警告.

通常,你并不能去除所有异常:

  • 任何非一般的程序都很难去列出它所有可能的失败模式
  • 许多都是非常罕见的,你实际上是想捕获他们的大部分然后通过一些通用的方式处理,比如: 写入日志或上报,或重试逻辑,你甚至不知道是什么引起的
  • 对这些罕见的错误模式,可以吧错误信息写入日志,然后进行详细的手动检查,这也你能做的最好方式了

然而,尽管有堆栈追踪(stack trace),找出这些预期之外异常的真正原因仍然要比使用Option[T]在编译器就发现错误要花费的时间更多.

Scala编程中涉及的异常:

  • NullPointerExceptions
  • MatchError: 来自不健全的模式匹配
  • IOExceptions: 来自文件系统的各种问题或网络错误
  • ArithmeticException: 除0时的错误
  • IndexOutOfBoundsException: 搞砸数组的时候
  • IllegalArgumentException: 滥用第三方代码的时候

仍然还有更多,但是并不需要完全去管,尽量在代码中使用Option[T], Either[T, V], sealed trait来使编译器能有更多的机会帮你进行错误检查.

避免副作用

至少在Scala中,编译器不会提供副作用的追踪.

下面是一个例子:

var result = 0

for (i <- 0 until 10){
  result += 1
}

if (result > 10) result = result + 5 

println(result) // 50
makeUseOfResult(result)

我们将value初始化为一个占位值,然后利用副作用来修改result的值,然后为makeUseOfResult函数使用.

这里有很多地方会出错,你可能会意外的得到有一个突变:

var result = 0

for (i <- 0 until 10){
  results += 1
} 

println(result) // 45
makeUseOfResult(result) // getting invalid input!

这些可以看做是很明显的错误,但如果这个片段有1000行而不是10行,在重构中很容易出错.他以为着makeUseOfResult得到一个无效的输入并处理错误.这里有另一个常见的错误模式:

var result = 0

foo()

for (i <- 0 until 10){
  results += 1
}

if (result > 10) result = result + 5 

println(result) // 50
makeUseOfResult(result)

...

def foo() = {
  ...
  makeUseOfResult(result)
  ...
}

这里甚至在result被初始化之前就开始使用它了.

下面的方式可以避免副作用:

val summed = (0 until 10).sum

val result = if (summed > 10) summed + 5 else summed

println(result) // 50
makeUseOfResult(result)

Scalazzi Scala的局限

下面的代码完全符合上面定义的Scalazzi Scala,但会让人感到很乱:

def fibonacci(n: Double, count: Double = 0, chain: String = "1 1"): Int = {
  if (count >= n - 2) chain.takeWhile(_ != ' ').length
  else{
    val parts = chain.split(" ", 3)
    fibonacci(n, count+1, parts(0) + parts(1) + " " + chain)
  }
}
for(i <- 0 until 10) println(fibonacci(i))
1
1
1
2
3
5
8
13
21
34

这个代码是正确的,完全遵守了"Scalazzi Scala"的指导方针:

  • 没有Null
  • 没有异常
  • 没有isInstanceOfasInstanceOf
  • 没有副作用并且所有值是不可变的
  • 没有classOfgetClass
  • 没有反射

但是人们会认为他是可怕的不安全的代码,原因在于下面的"Structured Data".

结构化数据

并非所有的数据都有相同的"形状",如果一些数据包含(name, phone-number)这样的对,有多重方式可以存储他们:

  • Array[Byte]: 这是文件系统存储他们的方式,如果你把他们存到磁盘,这就是他们的形式.
  • String: 在编辑器中打开,会看到这样的形式.
  • Seq[(String, String)]
  • Set[(String, String)]
  • Map[String, String]

这些都是有效的方式,如何来选择呢?

避免字符串有利于结构化数据

有时候会将数据存为String,然后在使用时在使用切片取出其中的不同数据,这样做会带来意外的问题.

Encode Invariants in Types
自描述数据
避免整数枚举
val UNIT_TYPE_UNKNOWN = 0
val UNIT_TYPE_USERSPACEONUSE = 1
val UNIT_TYPE_OBJECTBOUNDINGBOX = 2

这个代码中有一些好处:

  • Int类型消耗廉价,需要很少的内存来存储和传递
  • 避免各种数字这样的魔术代码到处都是,最终难以分辨

但是这种方式并不安全,更安全的方式会是这样:

sealed trait UnitType  
object UnitType{
  case object Unknown extends UnitType
  case object UserSpaceOnUse extends UnitType
  case object ObjectBoundingBox extends UnitType
}

或者:

// You can also make it take a `name: String` param to give it a nice toString 
case class UnitType private () 
object UnitType{
  val Unknown = new UnitType
  val UserSpaceOnUse = new UnitType
  val ObjectBoundingBox = new UnitType
}

这两种方式都是讲UnitType标记为一个实际的值,而不会想仅仅一个数字一样能够修改.

避免字符串标记
val UNIT_TYPE_UNKNOWN = "unknown"
val UNIT_TYPE_USERSPACEONUSE = "user-space-on-use"
val UNIT_TYPE_OBJECTBOUNDINGBOX = "object-bounding-box"

这样做仍然不安全,可以对UNIT_TYPE调用任何字符串的方法,并且能够使用任何字符串替换,更好的方式是这样:

sealed trait UnitType
object UnitType{
  case object Unknown extends UnitType
  case object UserSpaceOnUse extends UnitType
  case object ObjectBoundingBox extends UnitType
}

// Or perhaps

class UnitType private ()
object UnitType{
  val Unknown = new UnitType
  val UserSpaceOnUse = new UnitType
  val ObjectBoundingBox = new UnitType
}

包装整数ID

自增的ID经常是IntLong,UUID可能是Stringjava.util.UUID,与IntLong不同的是,ID都有一个唯一属性:

  • 所有的算术运算一般都没有意义
  • 不同的ID不能交换:比如一个userId: Int和一个函数def deploy(machineId: Int),deploy(userId)这样的调用是不希望出现的

最好的方式是使用不同的类将这些ID进行包装:

case class UserId(id: Int)
case class MachineId(id: Int)
case class EventId(id: Int)
...

或者自定义类型:

type UID = Int

然后使用:

val userId: UID = 2

2 - Scala 函数式

2.1 - CH00-精要

函数

一个函数是一个从“领域”到“代码域”的映射。函数将领域中的每个元素与代码域中的对应元素进行联结。在 Scala 中,领域和代码域都可以表示为type(类型)。

val square: Int => Int = x => x * x
square(2) 	// 4

高阶函数

高阶函数是 接收一个函数作为参数 或 返回一个函数作为结果 的函数。

trait List[A] {
  def filter(f: A => Boolean): List[A]
}

这个例子中,函数filter接收一个类型为A => Boolean的函数作为参数。

组合子

函数组合子是同时接收、返回函数的高阶函数。

type Conf[A] = ConfigReader => A
def string(name:String):Conf[String] = _.readString(name)
def both(left:Conf[A], right:Conf[B]):Conf[(A, B)] = c => (left(c), right(c))

函数both即为一个接收函数并返回函数的高阶函数。

多态函数

多态函数通常拥有一个或多个类型参数。Scala 本身对多态函数没有支持,但是可以通过特质的多态方法来实现。构造多态函数的特质通常拥有apply方法,因此,可以像普通的函数应用语法一样使用:

case object identity {
  def apply[T](value:T):T = value
}
identity(3)		// 3
indentity("3")	// "3"

这样,通过apply方法的便利,就可以实现多态函数。

类型

一个类型一组值运行时描述。比如Int表示从-21474836482147483647这样的一组整数集。值都拥有类型,或者说,每个值都表示一组值的一个成员。

2: Int

例子中,2Int集合的一个成员,也即,2的类型为Int

代数数据类型(ADT)

一个 ADT 是由产品类型自合而成的类型。

乘积类型(product)

乘积类型是由两个或多个类型的笛卡尔积组合构造而成的。

type Point2D = (Int, Int)

例子中,一个二维点是一个数字与另一个数字的积。即每个该类型的值都有一个 x 轴坐标和 y 轴坐标。

样例类

在 Scala 中,样例类是更符合语言习惯的乘积类型表示。

case class Person(name:String, age:Int)

总和类型(sum)

总和类型通过两个或多个类型的不相交形式来定义。

type RequestResponse = Either[Error, HttpResponse]

例子中,定义的类型RequestResponseErrorHttpResponse的总和构成,一个RequestResponse类型的值要么是一个 error,要么是一个 HTTP 响应。

密闭特质

在 Scala 中,密闭特质是更符合语言习惯的总和类型表示。

sealed trait AddressType
case object Home extends AddressType
case object Business extends AddressType

例子中,一个AddressType要么是一个Home,要么是一个Buiness,而不能同时是两者。

子类型

如果AB的子集,A即为B的子类型。在 Scala 中,A必须继承自B。编译器允许在任何需要的地方使用子类型。

sealed trait Shape{
  def width:Int
  def height:Int
}
case class Rectangle(corner:Point2D, width:Int, height:Int) extends Shape

超类型

如果BA的子集,A即为B的超类型,B必须继承自A。编译器支持无论在何处定义子类型,均可以使用超类型。

同样还是上面的例子,Shape即为Rectangle的超类。

Universals

一个通用(universally)量化类型定义了一个“由一些任意类型参数化的类型”的类别。在 Scala 中,类型构造器(比如特质)和方法必须是通用量化类型,尽管方法并不拥有一个类型,它只是类型的一部分。

类型构造器

一个类型构造器是一个通用量化类型,用于构造类型。

sealed trait List[A]
case class Nil[A]() extends List[A]
case class Cons[A](head: A, List[A]) extends List[A]

例子中,List是一个类型构造器,定义了一组List-类似的类型。因此可以认为,List是针对类型A的通用类型量化。

高阶类型

类型级别函数(Type-Level)

类型构造器可以看做一个类型级别函数,它接收类型并返回类型。这种解释对于理解高阶类型很有用。

比如,List是一个接收类型A并返回类型List[A]的类型级别函数。

类别(Kind)

类别可以认为是“类型的类型”。

  • *:类型的类别,所有类型的集合。
  • * => *:类型级别函数的类别(接收一个类型,返回一个类型)。
  • [*, *] => *:接收两个类型并返回一个类型的类型级别函数 的类别。比如类型构造器Either的类别是[*, *] => *,Scala 语法表示成_[_, _]

可以与函数的类型进行对比:A => BA => B => C

高阶类别(Higher-Order Kinds)

就像函数能够是高阶函数一样,类型构造器也可以是高阶的。Scala 中使用下划线编码(encode)高阶类型构造器。trait CollectionModule[Collection[_]]表示CollectionModule的类型构造器需要提供一个* -> *类别的类型构造器,比如List

  • (* => *) => *:类型构造器的类别,它接收一个* => * 类别的类型构造器。比如:trait Functor[F[_]] {...}trait Monad[F[_]] {...}

Existentials

一个“存在量化类型”定义一个基于一些明确但又未知的类型 的类型。“存在判断的类型”用于隐藏一些并非全局相关的类型信息。

trait ListMap[A]{
  type B
  val list: List[B]
  val mapf: B => A
  def run: List[A] = list.map(mapf)
}

例子中,类型ListMap[A]#B是一些明确类型,但是有不能知道其真正类型,它可能是任何类型。

Skolemization

每个“存在判断的类型”都可以被编码为一个通用(universal)类型。这个过程称为Skolemization

case class ListMap[B, A](list:List[B], mapf: B => A)

trait ListMapInspector[A,Z] {
  def apply[B](value: ListMap[B, A]):Z
}

case class AnyListMap[A] {
  def apply[Z](value:ListMapInspector[A, Z]): Z
}

例子中,除了我们直接使用ListMap,还可以使用AnyListMap,仅当能够为B处理任何类型参数时允许我们对ListMap进行检查。

Type Lambdas

函数可以通过使用下划线操作符编程“偏应用型(partially apply)”。比如,zip(a, _)。一个类型匿名函数是偏应用“高阶类别”类型的一种方式,使用较少的类型参数来生成一个新的类型构造器。

类型匿名函数之于类型构造器,相当于匿名函数之于函数。一个是类型、值的表达式,一个声明:

({type λ[α] = Either[String, α]})#λ

例子中,Either通过偏应用一个String作为第一个类型参数。

较多场景中,会使用**类型别名(type aliase)**替代类型匿名函数,比如:type EitherString[A] = Either[String, A]

类别投影器(Projector)

类别投影器是一个常用的 Scala 编译器插件,提供了更易用的语法来创建类型匿名函数。比如,({type λ[α] = Either[String, α]})#λ可以表示为Either[String, ?]这样的语法。另外有更多语法来创建更复杂的类型匿名函数。

https://github.com/non/kind-projector

类型类

一个类型类是对类型和定义在该类上操作的收集。很多类型类会定义一些规则需要实现来遵守。

trait ShowRead[A] {
  def show(v: A): String
  def read(v: String): Either[String, A]
}

object ShowRead {
  def apply[A](implicit v: ShowRead[A]): ShowRead[A] = v
}

例子中,类型类ShowRead[A]定义了通过渲染字符串来显示类型A,并通过读取字符串来读取它,或是生成一个错误消息。

  • Right Identity

    read(show(v)) == Right(v)
    
  • Left Partial Identity

    read(v).map(show(_)).fold(_ => true, _ == v)
    

实例

一个类型类的实例就是通过提供一组类型来定义一个该类型类的实现。一般这些事例会被标记为imolicit,以便编译器能够自动为需要的函数提供这些实现。

implicit val ShowReadString:ShowRead[String] = new ShowRead[String] {
  def show(v:String):String = v
  def read(v:String): Either[String, String] = Right(v)
}

语法

便利的语法,或称为扩展方法,添加给类型以便这些类型类更加易用:

implicit class ShowOps[A:ShowRead](self:A){
  def show:String = ShowRead[A].show(self)
}
implicit class ReadOps(self:String){
  def read[A:ShowRead]:Either[String, A] = ShowRead[A].read(self)
}

函数式模式

Option、Either、Validation

这些类型均用来表示可选性和偏应用性:

seald trait Maybe[A]
final case class Just[A](value:A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]

sealed trait \/[A, B]
final case class -\/[A, B](value:A) extends \/[A, B]
fianl case class \/-[A, B](value:B) extends \/[A, B]

type Either[A, B] = A \/ B

sealed trait Validation[A, B]
final case class Failure[A, B](value:A) extends Validation[A, B]
final case class Success[A, B](value:B) extends Validation[A, B]

半群、幺半群(Semigroup, Monoid)

半群支持将两个相同类型的东西组合成另一个新的相同类型的东西。比如,加法操作构成一个基于整数的半群。幺半群怎加一个额外的拥有“零”元素的属性,比如添加给一个值而不改变该值。

trait Semigroup[A]

未完。。。

https://wiki.scala-lang.org/display/SW/Parser+Combinators–Getting+Started

2.2 - CH01-定义

介绍

函数式编程,只是用纯函数来构造程序,即函数是没有副作用的。

函数的副作用大致包括:

  • 修改一个变量
  • 直接修改数据结构
  • 设置一个对象的成员
  • 抛出一个异常或以一个错误停止
  • 打印到终端或者接收用户的输入
  • 读取或写入一个文件
  • 在屏幕上绘图

函数式编程的益处

副作用函数的实例

class Cafe{
  def buyCoffee(cc:CreditCard):Coffee = {
  	val cup = new Coffee()
  	cc.charge(cup.price)		// 副作用,对信用卡进行扣费
  	cup
  }
}

扣费过程涉及与整个交易系统的交互,而这个方法的主要用途是为了得到一杯咖啡,因此,扣费动作在获取咖啡的过程中额外完成了,即副作用。

但是如果要测试这段程序,我们并不真的需要与交易系统进行交互,或者将交易信息进行持久。同时信用卡本身也不应该知道交易的细节,我们可以传递一个Payments对象给buyCoffe方法,让CreditCard忽略掉交易的细节,整个交易过程由Payments对象来完成:

class Cafe {
  def buyCoffe(cc:CreditCard, p: Payments) = {
  	val cup = new Coffe()
    p.charge(cc, cup.price)		// 副作用,有交易对象来完成扣费
    cup
  }
}

现在可以使用 mock 来对交易细节进行测试。但是真个程序很难被复用,比如我们要订购 12 杯咖啡。

去除副作用

在上面的例子中,我们可以在购买咖啡的过程中同时返回咖啡和对应的费用,而根据费用进行的交易过程则后续进行处理,这样,这个程序就不再存在副作用:

def buyCoffe(cc:CreditCard):(Coffe, Change) = {
  val cup = new Cup()
  (cup, Charge(cc, cup.price))
}

这样通过将费用的创建与执行分离,购买咖啡的过程就不再有副作用,而真正的扣费可以在后续的事务中进行。

现在我们对相同信用卡的计费进行合并,以便支持同时购买多杯咖啡:

case class Charge(cc:CreditCard, amount: Double){
  def combine(ohter:Charge): Charge = {
    if (cc == other.cc) Charge(cc, amount + other.amount)
    else throw new Exception("Can't combine charges to different cards.")
  }
}

然后实现buyCoffes来购买多杯咖啡,一次返回对应数量的咖啡和一个合并后的计费值:

def buyCoffes(cc:CreditCard, n:Int): (List[Coffe], Charge) = {
  val purchases:List[(Coffe, Charge)] = List.fill(n)(buyCoffe(cc))
  val (coffes, charges) = purchases.unzip
  (coffes, chargs.reduce((c1,c2) => c1.combine(c2)))
}

如果需要将不同信用卡的不同消费记录合并成一个计费列表,即将相同的信用卡计费合并:

def coalesce(charges:List[Charge]):List[Charge] = 
  charges.groupBy(_.cc).values.map(_.reduce(_ combine _)).toList

如何消除副作用?

通过把副作用推到程序的最外层,来转换带有副作用的函数。对于很多必须的副作用都存在对应的函数式实现,如果没有对应的实现,也可以通过找到一种方式来构造代码,让副作用发生但不可见。

什么是纯函数

纯函数是没有副作用的,更易推理

一个函数在执行过程中,除了根据给定的参数给出运算结果之外没有任何其他影响,即为无副作用。

引用透明:对于一个函数,无论何时调用,相同的参数都会返回一致的结果。同样也适用于表达式。

任何程序中符合引用透明的表达式都可以由它的结果替代,而不会改变程序的含义。

纯函数:当调用一个函数时,传入的参数是引用透明的,并且函数调用也是引用透明的,那么他就是一个纯函数。

引用透明与纯粹度

对于程序 p,如果它包含的表达式 e 满足引用透明,所有的 e 都能替换为它的计算结果而不会改变程序 p 的含义。假设存在一个函数 f,若表达式 f(x) 对所有引用透明的表达式 x 也是引用透明的,那么 f 是一个纯函数。

引用透明、纯粹度及替代模型

引用透明要求函数不论进行了何种操作都可以用它的返回值来代替。

比如一开始的咖啡例子:

def buyCoffee(cc:CreditCard):Coffee = {
  	val cup = new Coffee()
  	cc.charge(cup.price)		// 副作用,对信用卡进行扣费
  	cup
  }

它的返回值为cup,实际上就是new Coffe()。对于任何一个函数p(buyCoffee(cc:CreditCard))p(new Coffe())显然不能进行替换,因为buyCoffee除了返回一杯咖啡,还进行了交易过程。

引用透明的限制使得推导一个程序的求值变得简单,称为替代模型。如果表达式是引用透明的,可以想象计算过程就像是在解代数方程。展开表达式的每一部分,使用指示对象代替变量,然后归约到最简单的形式。在这一过程中,每一项都可以被等价的值替代,计算的过程就像是被一个又一个等价的值替代的过程。引用透明使得程序具备了等式推理的能力。

2.3 - CH02-函数

高阶函数-把函数传递给函数

函数也是值,也可以赋值给一个变量、存储在一个数据结构里、像参数一样传递给另一个函数

循环调用

使用循环方式实现阶乘:

def factirial(n:Int): Int = {
  def go(n:Int, acc:Int):Int = {	// 内部函数,跟一个函数内的变量含义相同
    if (n <= 0) acc
    else go(n-1, n * acc)
  }
  go(n, 1)
}

想不通过修改一个循环变量而实现循环功能,可以借助递归函数。这样的递归函数一般没命名为 go 或 loop。

上面的例子中,内部函数go实现循环,接收下一个n和和累积值acc。要退出循环时,返回一个不继续递归的值,即n <= 0

编译器会检测到这种自递归,只要递归调用发生在尾部,即递归调用后面没有其他的调用,编译器会优化成类似while循环的字节码。

尾调用

指调用者在一个递归调用之后不做其他事,只是返回这个调用结果。

比如else go(n-1, n * acc)部分,是直接返回了go的递归调用,没有再做其他计算。如果是1 + go(n-1, n * acc),则不再是尾调用,因为又进行了计算。

如果递归调用在尾部位置,则会自动编译为循环迭代,即不会每次进行栈的操作。可以通过@annotation.tailrec告诉编译器当没有成功编译成循环迭代时发出警告。

高阶函数

def formatResult(name:String, n:Int, f: Int => Int) = {
  val msg = "The %s of %d is %d."
  msg.format(name, n, f(n))
}

这里的参数f,其类型为Int => Int,表示接受一个整数并返回一个整数的函数。

因为高阶函数一般不能通过函数名来准确表示函数的功能,因此使用较短的函数命名,比如 g、h、f 等。

多态函数-基于类型的抽象

这里的多态跟继承中所说的“父类-子类继承”不同,这里是指类型的多态。

之前见到的函数都是单态的,因为函数只操作一种数据类型。适用于多种数据类型的函数,称为多态函数,或泛型函数

多态函数的构建,一般是发现多个单态函数有相似的结构,这时,可以封装为一个多态函数。

实例

比如一个函数,返回数组中第一个匹配到 key 的索引,否则返回 -1:

def findFirst(ss:Array[String], key:String):Int = {
  @annotatin.tailrec
  def loop(n:Int):Int = {
    if (n >= ss.length) -1		// 到达数组尾部仍未匹配到 key,返回 -1
    else if (ss(n) == key) n	// 匹配到 key,返回索引
    else loop(n + 1)			// 递归调用
  }
}

这是从 String 数组中匹配,如果是从 Int 数组查找匹配,也是类似的结构,因此我们就可以将它改写为一个从 A 类型数组中查找对应索引的函数:

def findFirst[A](as:Array[A], p:A => Boolean):Int = {
  @annotatin.tailrec
  def loop(n:Int):Int = {
    if (n >= as.length) -1
    else if p(as(n)) n			// 使用传入的 测试函数 p 对当前元素进行判断
    else loop(n + 1)
  }
}

函数名后跟的是类型参数,由中括号包围,多个参数使用逗号分隔,一般使用单个大写字母表示一个类型参数。这些类型参数作为类型变量,可以在其他类型签名中引用,比如上面的as参数类型。

向高阶函数传入匿名函数

在调用高阶函数时,并没有必要提前定义一个有名函数然后再传入,可以在调用时直接定义一个函数值作为高阶函数的参数,这被称为匿名函数函数字面量。比如:

findFirst(Array(1,2,3), (x:Int) => x == 9)

(x:Int) => x == 9即为一个匿名函数,或称为函数字面量。

匿名函数中,=>左边为该函数的参数列表,右边则会函数体。如果 Scala 可以推断参数的类型,则可以省略掉。

函数也是值

当定义一个函数字面量时,实际上是定义了一个包含一个apply方法的 Scala 对象。而当一个对象拥有apply方法,则可以把该对象当做方法一样调用。

比如我们定义一个函数字面量(a, b) => a < b,它事实上是创建函数对象的语法糖:

val lessThan = new Function2[Int, Int, Boolean] {
  def apply(a:Int, b:Int) = a < b
}

lessThan的类型为Function2[Int, Int, Boolean],通常会写成(a, b) => BooleanFunction2特质拥有一个apply方法,在调用lessThan(10, 20)时实际上是对apply方法调用的语法糖:

lessThan.apply(10, 20) 		// true

这些类似Function2的特质,实际是由 Scala 标准库提供的普通特质,比如Function1Funciton3等等。其中的数字是指接收的参数个数。

因为这些函数在 Scala 中是普通对象,因此他们也是一等值。

通过类型实现多态

在实现多态函数时,各种可能的实现方式明显比普通函数减少了。比如针对类型 A 的多态函数,唯一能够对 A 进行操作的方式是传入一个函数作为参数,通过这个传入的函数来操作 A。

比如一个例子,这个函数签名表示它只有一种实现方式。它是一个执行部分应用的高阶函数。函数partial1接收一个值和一个带有两个参数的函数,并返回一个带有一个参数的函数。

部分应用函数,表示函数被应用的参数并不是它需要的完整参数,即只提供了参数列表中的部分参数。

def partial1[A, B, C](a:A, f: (A, B) => c): B => C

我们该如何实现这个高阶函数呢。根据它的返回值类型,是接收一个类型 B 的参数并返回一个类型 C 的值的函数:

def partial1[A, B, C](a:A, f: (A, B) => c): B => C = {
  (b: B) => ???			// 根据返回值类型 B => C ,定义一个该返回值类型的函数
}

现在如何来实现方法体部分呢,根据声明,这个函数必须返回一个 C 类型的值。而在partial1的参数列表中,正好有一个函数f能够返回一个 C 类型的值。除此之外,我们没有其他方式来实现该函数体。因此:

def partial1[A, B, C](a:A, f: (A, B) => c): B => C = {
  (b: B) => f(a, b)		// 调用参数中的函数,实现符合返回类型的函数体
}

这也就是部分应用函数的实现过程,一个函数,接收两个参数,返回一个值。当我们只提供一个参数值时,在这个例子中,是a,这时会得到一个 “接收一个参数并返回一个值的” 函数。然后在提供原始函数需要的第二个参数,即b,就能得到最终的结果, 即c

2.4 - CH03-数据

定义函数式数据结构

函数式数据结构只能被纯函数操作,纯函数一定不能修改原始数据或产生副作用。因此,函数式数据结构被定义为不可变的

比如单链表的实现:

sealed trait List[+A]
case object Nil extends List[Nothing]
case class Cons[+A](head:A, tail: List[A]) extends List[A]	// 递归引用自身类型

object List{
  def sum(ints: List[Int]):Int = ints match {
    case Nil => 0
    case Cons(x, xs) => x + sum(xs)
  }
  
  def product(ds:List[Double]): Double = ds match {
    case Nil => 1.0
    case Cons(0.0, _) => 0.0
    case Cons(x, xs) => x * product(xs)
  }
  
  def apply(as: A*): List[A] = 
    if (as.isEmpty) Nil
    else Cons(as.head, apply(as.tail:_*))
}

List[+A]表示它是一个泛型数据类型。同时拥有两种实现,或者说是两种构造器,每种都由case关键字定义,表示它的两种可能的形式。如果List为空,则用Nil表示,如果非空,则用构造器Cons表示。一个非空列表由初始元素head和后续紧跟的也是List结构的tail构成,并且,这个tail可能为空,即他可能是一个Nil

这就是一个典型的数据构造器声明,为我们提供了一个函数来构造该类型数据的不同形式:

val ex1:List[Double] = Nil
val ex2:List[Int] = Cons(1, Nil)
val ex3:List[String] = Cons("a", Cons("b", Nil))

模式匹配

List的伴生对象中定义了两个方法,sumproduct,他们都使用了模式匹配。同时他们都是以递归的方式定义,这在编写操作递归数据类型的函数时很常见。

模式匹配类似于switch,他可以侵入到表达式的数据结构内部,对这个结构进行检验和提取子表达式。符号=>左边为模式,右边为结果

如果将模式中的变量分配给目标子表达式,使得它在结构上与目标一致,模式与目标就是一致的。匹配上的话,结果表达式就可以访问这些模式中定义的局部变量。

函数式数据结构的数据共享

当一个新的元素添加到已有的列表时返回一个新的列表。既然列表是不可变的,并不需要真的去复制一份原有列表,而是直接去复用它,比如向原有的列表 xs 添加一个数字 1,返回一个Cons(1, xs)

同样,删除列表的第一个元素,比如myList = Cons(x, xs),这时只需要返回尾部的xs。并没有真的删除任何元素。原始的myList依然可用,不会受到任何影响。

这被称为数据共享

“函数式数据结构是持久的”,是指:已存在的引用不会因数据结构的操作而改变。

数据共享的效率

???

高阶函数的类型推导

当向一个高阶函数传递函数类型的参数时,需要标识该函数的类型。比如高阶函数dropWhile

def dropWhile[A](l: List[A], f: A => Boolean): List[A]

当调用该函数时,参数函数f必须制定它的参数类型:

val xs:List[Int] = List(1,2,3,4,5)
val ex1 = dropWhile(xs, (x:Int) => x < 4)	// (x:Int) 指定参数类型为 Int

因为dropWhile的两个参数都使用类型参数A,前一个参数l的类型为Int,因此第二个参数的类型也必须是Int

当函数定义包含多个参数组时,参数组里的类型信息从左到右进行传递。

因此我们把dropWhile的参数列表分开,让他成为一个柯里化函数:

def dropWhile[A](as:List[A])(f: A => Boolean): List[A] = 
  as match {
    case Cons(h, t) if f(h) => dropWhile(t)(f)
    case _ => as
  }

现在可以这样调用dropWhiledropWhile(xs)(x => x < 4)。当调用dropWhile(xs)时他会返回一个函数,这时已经确定第一个参数的类型为A,因此第二个参数时就不再需要进行类型标注了,它的类型只能为A

通过将函数参数分组排序成多个参数列表(将函数柯里化),来最大化的利用类型推导。

基于 List 的递归并泛化为高阶函数

回顾前面的sumproduct函数:

def sum(ints: List[Int]): Int = ints match{
  case Nil => 0
  case Cons(x, xs) => x + sum(xs)
}

def product(ds:List[Double]): Double = ds match{
  case Nil => 1.0
  case Cons(x, xs) => x * product(xs)
}

这两个函数的结构类似,不同在于他们操作的数据类型(Int/Double)、Nil时返回的值(0/1.0)、以及对结果的组合操作(+/*)。

对于这两个函数的抽象,首先将Int/Double泛化为类型参数A,而Nil的返回值,可以作为一个函数参数由客户端提供。

如果一个子表达式引用了任何局部变量,把子表达式放入一个接收这些局部变量作为参数的函数

比如上面这两个函数中的x + sum(xs)x * product(xs)部分,都是对局部变量的引用,因此可以抽象为函数f: (A, B) => B。最终,把上面两个函数改写成下面的方式:

def foldRight[A, B](as: List[A], z: B)(f: (A, B) => B): B = 
  as match {
    case Nil => z
    case Cons(x, xs) => f(x, foldRight(xs, z)(f))
  }

def sum2(ns: List[Int]) = foldRight(ns, 0)((x, y) => x + y)
def product2(ns: List[Double]) = foldRight(ns, 1.0)(_ * _)

这里需要注意的是,与之前遇到的泛化不同,该函数的最终计算结果类型与传入的列表类型并不相同,如函数签名foldRight[A, B](as: List[A], z: B)(f: (A, B) => B): B所示,它的最终计算结果与传入的参数z类型一致,即类型B

因为整个函数的计算过程就是对列表进行循环,一直遍历到列表末尾,这个过程中不断压栈,直至列表的末尾,即Nil。而遇到Nil时返回的值为传入的参数z,套用传入的函数f,也就是通过传入列表的最后一个元素(Nil 之前的元素)与z来调用函数f,得到栈顶的值。然后依次出栈,完成整个计算过程。

可以函数的调用使用传入的函数f进行替换:

Cons(1, Cons(2, Nil))
f	(1, f   (2, z  ))

foldRight函数为 Scala 标准库中List的内置函数,上面的替换过程也就是它的计算过程。

比如我们调用sum2(List(1,2,3), 0)((x, y) => x + y),跟踪其运算过程:

foldRight(Cons(1, Cons(2, Cons(3, Nil))), 0)((x, y) => x + y)
1 + foldRight(Cons(2, Cons(3, Nil)), 0)((x, y) => x + y)
1 + (2 + foldRight(Cons(3, Nil), 0)((x, y) => x + y))
1 + (2 + (3 + foldRight(Nil, 0)((x, y) => x + y)))) // Nil, 终止递归,替换为(0 + 0)
1 + (2 + (3 + (0))
6

List 是代数数据类型(ADT)的一种,有些场景或称为抽象数据类型

ADT 是由一个或多个数据构造器所定义的数据类型,每个构造器可以包含零个或多个参数。数据构造器通过累加(sum)或联合(union)构成数据类型,而每个数据构造器又是其参数的乘积(product)。因此称为代数数据类型。

ADT 用于构造其他数据结构。定义一个二叉树数据结构:

sealed trait Tree[+A]
case class Leaf[A](value: A) extends Tree[A]
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]

2.5 - CH04-异常

抛出异常会产生副作用,在函数式解决方案中,以值的方式返回错误更安全,符合引用透明,并且可以通过高阶函数保存异常的优点 - 统一错误处理逻辑。核心思想就是使用普票值来表现异常。

异常的优点与劣势

一个抛出异常的例子:

def failingFn(i: Int): Int = {
  val y:Int = throw new Exception("fail!") 	// 抛出异常
  try{
    val x = 42 + 5
    x + y
  } catch {
    case e:Exception => 43
  }
}

调用该函数会抛出预期的异常,为了证明异常会破坏引用透明,我们可以将y进行替换:

def failingFn2(i: Int) = {
  try{
    val x = 42 + 5
    x + (throw new Exception("fail!"): Int)	// 抛出异常的表达式可以声明为任何类型
  } catch {
    case e: Exception => 43
  }
}

运行结果会得到 43 而不是像替换前一样排除预期的异常,因此,异常并不是引用透明的。

引用透明的表达式不依赖上下文,可以本地推导,而那些非引用透明的表达式是依赖上下文的,并且需要全局推导。比如把42 + 5这个表达式嵌入到一个更大的表达式中,他不会受这个更大的表达式的影响,即对更大的表达式产生依赖,它永远等于 47。但是把throw new...嵌入到一个更大的表达式,比如嵌入到一个try/catch结构中,它的值取决于catch部分的处理,因此对更大的表达式产生了依赖。

异常的问题:

  • 异常破坏了引用透明并且引入了上下文依赖。
  • 异常不是类型安全的。函数failingInt => Int并不会告诉我们会发生什么样的异常。因为没有受检异常,直到运行时才会发现异常。

我们希望其他选择能够排除异常的这些缺点,但是又不想失去异常最主要的好处:整合集中的错误处理逻辑,而不是在整个代码库发布这个逻辑。

异常的其他选择

比如一个计算列表平均值的函数:

def mean(xs:Seq[Double]): Double = 
  if (xs.isEmpty) throw new ArithmeticException("mean of empty list!")
  else xs.sum / xs.length

这是一个典型的偏函数,他对一些输入没有做定义。如果一个函数对那些非隐式的输入类型做了一些假设,那它就是一个典型的偏函数。

除了选择抛出异常,还有其他的选择。

第一种是返回某个伪造的 Double 类型的值。比如为空时返回Double.NaN,或者其他报警值,或者null。但是以下理由使我们放弃这种方案:

  • 它允许错误无声的传播,如果忘了对这样的值进行检查也不会得到警告,会使后面的代码出错。
  • 使用显式的if来检查是否得到了正确的结果会导致大量的模板代码,特别是调用多个函数时。
  • 不适用与多态代码。比如一个查找最大值的泛型函数def max[A](xs:List[A]),当传入为空时无法发明一个 A 类型的值。也不能是null,因为null只对非基础类型有效,但是这里的 A 可能是IntDouble
  • 需要一个特定的策略或调用约定 - 告诉调用者如何合理的使用mean函数。这导致它不能传递给高阶函数,因为高阶函数对待所有参数都是一致的。

第二种是强迫调用者提供一个参数告诉我们该如何处理。比如:

def mean(xs:Seq[Double], onEmpty: Double): Double = 
  if (xs.isEmpty) onEmpty
  else xs.sum / xs.length

这使mean函数称为一个完全函数。调用者必须知道如何处理未定义的情况。

Option 数据类型

解决方案是在返回值类型明确表示该函数并不总是有结果。

sealed trait Option[+A]
case class Some[+A](get:A) extends Option[A]
case object None extends Option[A]

Option是一个最多只包含一个元素的List

基础 Option 函数

trait Option[+A] {
  def map[B](f: A => B): Option[B]					// 如果 Option 不为 None,对其应用 f
  def flatMap[B](f: A => Option[B]): Option[B] 		// 如果 Option 不为 None,对其应用 f,可能会失败
  def getOrElse[B >: A](default: B): B				// 默认值类型 B 必须是 A 的父类
  def orElse[B >: A](ob: => Option[B]): Option[B]	// 不对 ob 求值,除非需要
  def filter(f: A => Boolean): Option[A] 			// 如果值不满足,转换 Some 为 None
}

基础函数使用场景

???

Option 组合、提升、面向异常的 API 封装

可能会认为,一旦开始使用Option,整个库都会受影响,因为一些方法必须改变为接收或返回Option。但是,我们可以把一个普通函数**提升(lift)**为一个对Option操作的函数。

比如,map函数支持我们用一个类型为A => B的函数来操作一个Option。从另一个角度看,map可以把一个A => B的函数转换为Option[A] => Option[B]类型的函数:

def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f

现在,可以把普通的abs函数转换为处理Option的函数:

val absOpt:Option[Double] => Option[Double] = lift(math.abs)

Try函数是一个通用目的的函数,用于将一个基于异常的 API 转换成一个面向Option的 API。

Either 数据类型

Option只能用来表示可能不存在的值,并不能表示异常条件下发生了什么错误。如果除了需要获取异常时可能的值,还需要知道异常时的错误信息,可以使用Either

sealed trait Either[+E, +A]
case class Left[+E](value: E) extends Either[E, Nothing]
case class Right[+A](value: A) extends Either[Nothing, A]

Either在两种情况都有值,它的值只能是两种情况中的一种,称他是两个类型的互斥并集。一般使用Left表示失败,而Right表示成功。

使用Either改写前面的mean函数:

def mean(xs:List[Double]): Either[String, Double] = 
  if (xs.isEmpty) Left("mean of empty list!")
  else Right(xs.sum / xs.length)

或者在Left中包含处理的异常以获取详细的调用栈信息:

def safeDiv(x:Int, y:Int):Either[Exception, Int] = 
  try Right(x / y)
  catch {case e:Exception => Left(e)}