回顾:Case 类
Case 类是 Scala 处理复杂数据的理想方式。
比如表示 JSON 类型的数据,可以这样:
1 2 3 4 5 6 7
| abstract class JSON case class JSeq(elems: List[JSON]) extends JSON case class JObj(bindings: Map[String, JSON]) extends JSON case class JNum(num: Double) extends JSON case class JStr(str: String) extends JSON case class JBool(b: Boolean) extends JSON case object JNull extends JSON
|
模式匹配
这是将 JSON 数据转换成字符串的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| def show(json: JSON): String = json match { case JSeq(elems) => "[" + (elems map show mkString ", ") + "]" case JObj(bindings) => val assocs = bindings map { case (key, value) => "\"" + key + "\": " + show(value) } "{" + (assocs mkString ", ") + "}" case JNum(num) => num.toString case JStr(str) => "\"" + str + "\"" case JBool(b) => b.toString case JNull => "null" }
|
函数是对象
问题:下面表达式的类型是什么?
1
| { case (key, value) => key + ": " + value }
|
当它单独出现时,它是没有类型的,需要给它规定一个期望的类型。
上面代码中 map
方法期望的类型是 JBinding => String
,其中 JBinding
为:
1
| type JBinding = (String, JSON)
|
在 Scala 中,所有实体类型都是某个类或 trait,函数也不例外。
其实是
1
| scala.Function1[JBinding, String]
|
的缩写形式,scala.Function1
是一个 trait,JBinding
和 String
则是它的类型参数。
Function1 Trait
这是trait Function1
的大概样子:
1 2 3
| trait Function1[-A, +R] { def apply(x: A): R }
|
之前的模式匹配块会被扩展为Function1
的一个实例:
1 2 3 4 5
| new Function1[JBinding, String] { def apply(x: Binding) = x match { case (key, value) => key + ": " + show(value) } }
|
Function 的子类
函数是 trait 的一个好处是可以用子类来扩展它。
比如 map 是从键到值的函数:
1
| trait Map[Key, Value] extends (Key => Value)
|
序列是从 Int
到值的函数:
1
| trait Seq[Elem] extends Int => Elem
|
这就是 elems(i)
可以这样写的原因。
不完全函数 (Partial Function)
有如下定义的函数:
1
| val f: String => String = { case "ping" => "pong" }
|
它的类型为 String => String
,但是这个函数没有在整个域(所有字符串)上定义。
有在运行一个函数之前就能测试它能接受哪些参数的方法吗?确实有:
1 2 3
| val f: PartialFunction[String, String] = { case "ping" => "pong" } f.isDefinedAt("ping") f.isDefinedAt("pong")
|
不完全函数是这样定义的:
1 2 3 4
| trait PartialFunction[-A, +R] extends Function1[-A, +R] { def apply(x: A): R def isDefinedAt(x: A): Boolean }
|
如果期望类型是 PartialFunction
,Scala 编译器会将
1
| { case "ping" => "pong" }
|
扩展为:
1 2 3 4 5 6 7 8 9
| new PartialFunction[String, String] { def apply(x: String) = x match { case "ping" => "pong" } def isDefinedAt(x: String) = x match { case "ping" => true case _ => false } }
|
练习
这个函数:
1 2 3 4 5 6 7
| val f: PartialFunction[List[Int], String] = { case Nil => "one" case x :: rest => rest match { case Nil => "two" } }
|
g.isDefinedAt(List(1, 2, 3))
的返回值是?
答案为 true
。但应注意尽管测试为 true
,但当以 List(1, 2, 3)
为参数调用 f
时会发生错误。也就是说 isDefinedAt
只保证最外层的模式匹配正确。
回顾:集合
Scala 包含丰富的集合类层次。所有集合类型共享一些通用方法。
核心方法:
另外还有:
各方法在列表 (List) 上的理想化实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| abstract class List[+T] { def map[U](f: T => U): List[U] = this match { case x :: xs => f(x) :: xs.map(f) case Nil => Nil } def flatMap[U](f: T => List[U]): List[U] = this match { case x :: xs => f(x) ++ xs.flatMap(f) case Nil => Nil } def filter(p: T => Boolean): List[T] = this match { x :: xs => if (p(x)) x :: xs.filter(p) else xs.filter(p) case Nil => Nil } }
|
在实践中,这些方法的具体实现和类型并不和上面相同,这样做是想:
- 让它们可以应用在不同的集合上面,不仅是列表
- 让它们在列表上是尾递归的
For 表达式
For 表达式可以简化 map
、flatMap
、filter
的组合。
1 2 3
| (1 until n) flatMap (i => (1 until i) filter (j => isPrime(i + j)) map (j => (i, j)))
|
可以写成:
1 2 3 4 5
| for { i <- 1 until n j <- 1 until i if isPrime(i + j) } yield (i, j)
|
For 的翻译
Scala 编译器会把 for 表达式翻译成 map
、flatMap
和 filter
的一种懒求值变体的组合。
会被翻译为:
1
| for (x <- e1 if f; s) yield e2
|
其中 f
是过滤器,s
是(可能为空)一系列的生成器和过滤器。它会被翻译为:
1
| for (x <- e1.withFilter(x => f); s) yield e2
|
可以认为 withFilter
是 filter
的一种变体,它不产生临时列表,而是直接过滤 map
或 flatMap
中剩余的元素。
1
| for (x <- e1; y <- e2; s) yield e3
|
会被翻译为:
1
| e1.flatMap(x => for (y <- e2; s) yield e3)
|
然后继续递归翻译。
For 表达式和模式匹配
生成器的左侧也可以是模式。
比如:
1 2 3 4 5 6 7 8
| val data: List[JSON] = ... for { JObj(bindings) <- data JSeq(phones) = bindings("phoneNumbers") JObj(phone) <- phones JStr(digits) = phone("number") if (digits) startsWith "212" } yield (bindings("firstName"), bindings("lastName"))
|
如果 pat
是有一个变量 x
的模式,将会把
翻译为:
1 2 3 4 5 6
| x <- expr withFilter { case pat => true caes _ => false } map { case pat => x }
|
练习
1 2 3 4 5
| for { x <- 2 to N y <- 2 to x if (x % y == 0) } yield (x, y)
|
会被翻译为?
答案:
1 2 3
| (2 to N) flatMap (x => (2 to x) withFilter (y => x % y == 0) map (y => (x, y)))
|
For 表达式的其他用法:函数式随机生成器
For 可以应用在任何实现了 map
、flatMap
、withFilter
的对象上。
现在以一个能生成任意类型的随机生成器为例。
首先定义 trait:
1 2 3
| trait Generator[+T] { def generate: T }
|
一些实例:
1 2 3 4 5 6 7 8 9 10
| val integers = new Generator[Int] { val rand = new java.util.Random def generate = rand.nextInt() } val booleans = new Generator[Boolean] { def generate = integers.generate > 0 } val pairs = new Generator[(Int, Int)] { def generate = (integers.generate, integers.generate) }
|
会发现有许多 new Generator
重复,能避免吗?理想中的写法是:
1 2 3 4 5
| val booleans = for (x <- integers) yield x > 0 def pairs[T, U](t: Generator[T], u: Generator[U]) = for { x <- t y <- u } yield (x, y)
|
这其实会生成:
1 2 3
| val booleans = integers map (x => x > 0) def pairs[T, U](t: Generator[T], u: Generator[U]) = t flatMap (x => u map (y => (x, y)))
|
所以说要定义 map
和 flatMap
。下面是一个更便捷的 Generator
版本:
1 2 3 4 5 6 7 8 9 10
| trait Generator[+T] { self => def generate: T def map[S](f: T => S): Generator[S] = new Generator[S] { def generate = f(self.generate) } def flatMap[S](f: T => Generator[S]): Generator[S] = new Generaotr[S] { def generate = f(self.generate).generate } }
|
那么上面的 booleans
和 pairs
展开过程为:
1 2 3 4 5 6
| val booleans = new Generator[Boolean] { def generate = (x: Int => x > 0)(integers.generate) } val booleans = new Generator[Boolean] { def generate = integers.generate > 0 }
|
1 2 3 4 5 6 7 8 9 10 11
| def pairs[T, U](t: Generator[T], u: Generator[U]) = t flatMap { x => new Generator[(T, U)] { def generate = (x, u.generate) } } def pairs[T, U](t: Generator[T], u: Generator[U]) = new Generator[(T, U)] { def generate = (new Generator[(T, U)] { def generate = (t.generate, u.generate) }).generate } def pairs[T, U](t: Generator[T], u: Generator[U]) = new Generator[(T, U)] { def generate = (t.generate, u.generate) }
|
然后可以定义一些实用方法:
1 2 3 4 5 6 7
| def single[T](x: T): Generator[T] = new Generator[T] { def generate = x } def choose(lo: Int, hi: Int): Generator[Int] = for (x <- integers) yield lo + x % (hi - lo) def oneOf[T](xs: T*): Generator[T] = for (idx <- choose(0, xs.length)) yield xs(idx)
|
现在就可以定义一个生成列表的随机生成器了:
1 2 3 4 5 6 7 8 9
| def lists: Generator[List[Int]] = for { isEmpty <- booleans list <- if (isEmpty) emptyLists else nonEmptyLists } yield list def emptyLists = single(Nil) def nonEmptyLists = for { head <- integers tail <- lists } yield head :: tail
|
也可以定义一个生成二叉树的随机生成器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| trait Tree case class Inner(left: Tree, right: Tree) extends Tree case class Leaf(x: Int) extends Tree def leafs: Generator[Leaf] = for { x <- integers } yield Leaf(x) def inners: Generator[Inner] = for { l <- trees r <- trees } yield Inner(l, r) def trees: Generator[Tree] = for { isLeaf <- booleans tree <- if (isLeaf) leafs else inners } yield tree
|
随机生成器的一个应用:随机测试 (Random Testing)
关于单元测试:
- 包含一些测试输入和一个后置条件 (postcondition)
- 后置条件是期望结果的一个属性
- 验证此程序是否满足后置条件
若将测试输入替换为随机生成的输入,就是随机测试。
用随机生成器可以写一个测试函数:
1 2 3 4 5 6 7
| def test[T](g: Generator[T], numTimes: Int = 100)(test: T => Boolean): Unit = { for (i <- 0 until numTimes) { val value = g.generate assert(test(value), "test failed for " + value) } println("passed " + numTimes + " tests") }
|
如果有下面的一个使用例子:
1 2 3
| test(pairs(lists, lists)) { case (xs, ys) => (xs ++ ys).length > xs.length }
|
它会始终通过测试吗?
答案是不会。因为 lists
随机生成器会生成空列表。
关于 ScalaCheck:一个测试工具,使用它写测试时只用写待测属性而不用写具体测试过程。比如这样:
1 2 3
| forAll { (l1: List[Int], l2: List[Int]) => l1.size + l2.size == (l1 ++ l2).size }
|
单子 (Monad)
可以看出包含 map
和 flatMap
的数据结构很普遍。实际上有个专门的名称来表述这些满足一定代数条件的数据结构类别,这就是单子。
一个单子 M
是一个拥有 flatMap
和 unit
这两种操作的参数化类型 M[T]
,它必须满足一些条件。
1 2 3 4
| trait M[T] { def flatMap[U](f: T => M[U]): M[U] } def unit[T](x: T): M[T]
|
在其他文献中,flatMap
也常被称为 bind
。
List
是单子,其中 unit(x) = List(x)
Set
是单子,其中 unit(x) = Set(x)
Option
是单子,其中 unit(x) = Some(x)
Generator
是单子,其中 unit(x) = single(x)
在 Scala 中,flatMap
是这些类型上的一个操作,而 unit
对于每个单子都不相同。
对每个单子, map
可以被定义为 flatMap
和 unit
的组合:
1 2
| m map f == m flatMap (x => unit(f(x))) == m flatMap (f andThen unit)
|
单子的条件
一种类型若为合格的单子需要满足三个条件:
1
| m flatMap f flatMap g == m flatMap (x => f(x) flatMap g)
|
1
| unit(x) flatMap f == f(x)
|
例子
现在以 Option
为例来检验这三个条件。Option
的 flatMap
为:
1 2 3 4 5 6
| abstract class Option[+T] { def flatMap[U](f: T => Option[U]): Option[U] = this match { case Some(x) => f(x) case None => None } }
|
下面来验证 Some(x) flatMap f == f(x)
(左单位元):
1 2 3 4 5 6
| Some(x) flatMap f == Some(x) match { case Some(x) => f(x) case None => None } == f(x)
|
再来验证 opt flatMap Some == opt
(右单位元):
1 2 3 4 5 6
| opt flatMap Some == opt match { case Some(x) => Some(x) case None => None } == opt
|
最后验证 opt flatMap f flatMap g == opt flatMap (x => f(x) flatMap g)
(结合律):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| opt flatMap f flatMap g == opt match { case Some(x) => f(x) case None => None } match { case Some(y) => g(y) case None => None } == opt match { case Some(x) => f(x) match { case Some(y) => g(y) case None => None } case None => None match { case Some(y) => g(y) case None => None } } == opt match { case Some(x) => f(x) match { case Some(y) => g(y) case None => None } case None => None } == opt match { case Some(x) => f(x) flatMap g case None => None } == opt flatMap (x => f(x) flatMap g)
|
三条件对 for 表达式的意义
可以看到单子类型的表达式一贯被写为 for 表达式,那么对它来说这三个条件的意义是什么呢?
- 结合律本质上是说可以“内联 (inline)”嵌套 for 表达式:
1 2 3 4 5
| for (y <- for (x <- m; y <- f(x)) yield y z <- g(y)) yield z == for (x <- m; y <- f(x) z <- g(y)) yield z
|
1 2
| for (x <- m) yield x == m
|
另一个类型:Try
Try
类似于 Option
,不同之处是将 Some
/ None
替换成了包含一个值的 Success
和包含一个异常的Failure
:
1 2 3
| abstract class Try[+T] case class Success[T](x: T) extends Try[T] case class Failure(ex: Exception) extends Try[Nothing]
|
Try
被用来在线程或计算机间传递计算结果,这些计算可能失败并包含异常。
Try
可以包裹任意的计算:
下面是 Try
的一个实现:
1 2 3 4 5 6 7
| object Try { def apply[T](expr: => T): Try[T] = try Success(expr) catch { case NonFatal(ex) => Failure(ex) } }
|
像 Option
一样,Try
类型的计算可以由 for 表达式构成:
1 2 3 4
| for { x <- computeX y <- computeY } yield f(x, y)
|
如果 computeX
和 computeY
均成功且结果是 Success(x)
和 Success(y)
,那么会返回 Success(f(x, y))
。如果其中一个失败并有一个异常 ex
,则会返回 Failure(ex)
。
下面是 Try
中 flatMap
和 map
的定义:
1 2 3 4 5 6 7 8 9 10
| abstract class Try[T] { def flatMap[U](f: T => Try[U]): Try[U] = this match { case Success(x) => try f(x) catch { case NonFatal(ex) => Failure(ex) } case fail: Failure => fail } def map[U](f: T => U): Try[U] = this match { case Success(x) => Try(f(x)) case fail: Failure => fail } }
|
所以对一个 Try
类型值 t
有:
1 2
| t map f == t flatMap (x => Try(f(x))) == t flatMap (f andThen Try)
|
练习
看起来 Try
像是一个单子,其中 unit = Try
,对吗?
其实它不满足左单位元:
1
| Try(expr) flatMap f != f(expr)
|
实际上左侧永远不会产生非致命异常,但右侧会产生由 expr
或 f
抛出的任何异常。
因此,Try
以一个单子条件的代价换来了另外一个特性,这在下面的情景中更有用:
由 Try
、map
、flatMap
构成的表达式永远不会抛出非致命异常
称这个为“防弹”原则 (“bullet-proof” principle)。
小结
从上面可以看出 for 表达式不仅仅对于集合有用。很多其他类型也定义了 map
、flatMap
、withFilter
操作和由此而来的 for 表达式。
很多定义了 flatMap
的类型都是单子。如果它们也定义了 withFilter
,则可以称为“包含0的单子” (monads with zero)。
单子的三个条件给库的API设计提供了有用的指导。