Partial Function in Scala

模式匹配和匿名函数

Scala 的模式匹配可能是最常用到的代码片段,matchcase 配合使用,应该是 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 => truecase 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 呢?我们可以看下 mapcollect 这两个方法在定义上的区别:

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 != 042 / 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, MapSet 在 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 中 ListMap 都是偏函数(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-