模式匹配和匿名函数
Scala 的模式匹配可能是最常用到的代码片段,match
和 case
配合使用,应该是 Scala 程序员最常写的代码片段:
1
2
3
4
|
v match {
case Some(str) => ...
case None => ...
}
|
但是 case
有时也会出现在没有 match
关键词的场合:
1
|
map foreach {case(k,v) => println(s"$k -> $v")}
|
这种情况看上去有点费解。实际上,根据 scala 的语言规范,这是一种定义匿名函数的方式。
不使用 match
语句的一串 case
语句可以构造一个匿名函数,如下:
1
|
{ case p1 => b1 … case pn => bn }
|
上述表达式的类型可以是 scala.Functionk[S1,…,Sk, R]
或是 scala.PartialFunction[S1, R]
,后一种类型就是我们要讨论的偏函数。
如果期待的类型是 scala.Functionk[S1,…,Sk, R]
,则上述表达式等价于:
1
2
3
|
(x1:S1, …, xk:Sk) => (x1, …, xk) match {
case p1 => b1 … case pn => bn
}
|
同样等价于:
1
2
3
4
5
|
new scala.Functionk[S1,…,Sk, T] {
def apply(x1:S1,…,xk:Sk): T = (x1,…,xk) match {
case p1 => b1 … case pn => bn
}
}
|
如果期待的类型是 scala.PartialFunction[S1, R]
,则等价于:
1
2
3
4
5
6
7
8
9
|
new scala.PartialFunction[S, T] {
def apply(x: S): T = x match {
case p1 => b1 … case pn => bn
}
def isDefinedAt(x: S): Boolean = {
case p1 => true … case pn => true
case _ => false
}
}
|
使用 case
语句的方式构造匿名函数有更高的灵活性,可以充分使用模式匹配的优势,比如安全地进行类型转换,“解构”参数等。
偏函数
先来看一个简单的例子:
1
2
3
4
5
|
List(41, "cat") map { case i: Int ⇒ i + 1 }
//scala.MatchError: cat (of class java.lang.String)
List(41, "cat") collect { case i: Int ⇒ i + 1 }
//res1: List[Int] = List(42)
|
为什么后一条语句可以成功执行,没有抛出 MatchError
呢?我们可以看下 map
和 collect
这两个方法在定义上的区别:
1
2
3
4
5
|
//参数是一个普通函数
def map[B](f: (A) ⇒ B): List[B]
//参数是一个偏函数
def collect[B](pf: PartialFunction[A, B]): List[B]
|
很明显,区别正是在于偏函数。所以,到底什么是偏函数呢?偏函数(partial function)是数学上的概念,partial 是相对于 total 而言的。我们知道,函数是定义域到值域的一种映射关系,而偏函数只是在定义域的一个子集上面存在映射关系。
例如:
1
|
def fraction(d: Int) = 42 / d
|
这个函数对于 d == 0
是没有意义的,fraction(0)
会抛出异常。有了偏函数,我们可以这样来实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
val fraction = new PartialFunction[Int, Int] {
def apply(d: Int) = 42 / d
def isDefinedAt(d: Int) = d != 0
}
fraction.isDefinedAt(42)
//res2: Boolean = true
fraction.isDefinedAt(0)
//res3: Boolean = false
fraction(42)
//res4: Int = 1
fraction(0)
//java.lang.ArithmeticException: / by zero
|
通过 isDefinedAt
方法,我们可以判断一个偏函数对于给定的参数是否是有定义的。
我们也可以用 case
语句这样来写:
1
2
3
4
5
6
|
val fraction: PartialFunction[Int, Int] = {
case d: Int if d != 0 ⇒ 42 / d
}
fraction.isDefinedAt(0)
//false
|
目前 Scala 不能推断 case
语句构成的匿名函数的类型,必须明确指定。对于本节开始时的例子,也可以这样定义:
1
2
3
4
5
6
7
8
|
val incAny: PartialFunction[Any, Int] = {
case i: Int ⇒ i + 1
}
incAny.isDefinedAt(41)
//true
incAny.isDefinedAt("hello")
//false
|
由于 case
语句中只匹配了参数为 Int
的情况,因而该偏函数对于 string
类型的参数是没有定义的。
你可能不知道的偏函数
Seq
, Map
和 Set
在 Scala 中都是函数,因而我们可以这样使用
1
2
3
4
5
|
val pets = List("cat", "dog", "frog")
pets(0)
//cat
pets(3)
//java.lang.IndexOutOfBoundsException: 3
|
可以把 pets
这个函数看作在 0,1,2 上有定义,但是在 3 上没有定义。是的,Scala 中 List
和 Map
都是偏函数(Set
并不是):
1
2
3
4
5
6
|
pets.isDefinedAt(0)
//true
pets.isDefinedAt(3)
//false
pets.isInstanceOf[PartialFunction[_,_]]
//true
|
你甚至可以这样写:
1
2
|
Seq(1, 2, 42) collect pets
//Seq[java.lang.String] = List(dog, frog)
|
如果每次调用偏函数前都用 isDefinedAt
判断参数的合理性,这有点类似于 Java 中检查 null
值,在 Scala 中并不提倡这样。好在 PartialFunction
提供了一个 lift
方法,在没有定义时返回 None
,这样就避免了恼人的 null
值检测:
1
2
3
4
5
6
7
8
|
pets.lift(0)
//Option[java.lang.String] = Some(innocent)
pets.lift(42)
//Option[java.lang.String] = None
pets.lift(0) map ("I love my " + _) getOrElse ""
//java.lang.String = I love my cat
pets.lift(42) map ("I love my " + _) getOrElse ""
//java.lang.String = ""
|
参考
-EOF-