部分函数是任何函数,它只接受一个参数,仅针对其参数值的某个范围定义(即有效)。例如,Math.asin
仅针对范围内的参数值定义,对于范围[-1.0, 1.0]
外的值未定义 - 所以它是一个偏函数。例如,如果我们调用Math.asin(5.0)
,我们会得到NaN
返回,这意味着该函数没有为该参数定义。
请注意,部分函数不一定要抛出异常;它只需要做一些事情而不是返回一个有效值。
函数式编程的一个关键原则是引用透明性( RT ),这意味着我们应该能够用表达式的值替换表达式(例如函数调用),而不会改变程序的含义。(有关此主题的更多信息,我强烈建议您阅读Chiusano 和 Bjarnason的 Scala 中的函数式编程。)显然,如果抛出异常或返回无效值,则会崩溃。为了使对部分函数的调用具有引用透明性,我们只能使用定义了它们的参数值来调用它们,或者我们需要优雅地处理未定义的值。那么我们如何判断是否为某个任意参数值定义了偏函数呢?
在Scala中,我们可以将部分函数表示为scala.PartialFunction
允许我们回答这个问题的子类。
让我们看看您在Scala REPL会话中的示例...
$ scala
Welcome to Scala 2.12.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_171).
Type in expressions for evaluation. Or try :help.
scala> val second: List[Int] => Int = {
| case x :: y :: _ => y
| }
<console>:11: warning: match may not be exhaustive.
It would fail on the following inputs: List(_), Nil
val second: List[Int] => Int = {
^
second: List[Int] => Int = $$Lambda$3181/1894473818@27492c62
那么我们刚刚做了什么?我们定义second
为一个函数的引用,该函数接受一个List[Int]
参数并返回一个Int
(列表中的第二个值)。
您会注意到Scala 编译器认识到这不会匹配所有情况并警告您这一事实。这是一个偏函数,因为它会因某些参数而失败,但它不是 的实例scala.PartialFunction
,我们可以验证如下:
scala> second.isInstanceOf[PartialFunction[List[Int], Int]]
res0: Boolean = false
顺便说一句,typeList[Int] => Int
是一个简写形式scala.Function1[List[Int], Int]
,因此second
s type 是该类型的一个实例:
scala> second.isInstanceOf[Function1[List[Int], Int]]
res1: Boolean = true
调用此版本的函数会产生您指示的结果:
scala> second(List(1, 2, 3))
res1: Int = 2
scala> second(Nil)
scala.MatchError: List() (of class scala.collection.immutable.Nil$)
at .$anonfun$second$1(<console>:11)
at .$anonfun$second$1$adapted(<console>:11)
... 36 elided
问题是,如果我们只有一些列表值,l
并且不知道该列表中有什么,我们不知道如果我们将它传递给引用的函数是否会得到异常second
。现在,我们可以将调用放在一个try
块和catch
任何异常中,但这很冗长,而且不是很好的函数式编程风格。理想情况下,我们想知道是否可以先调用该函数以避免异常。不幸的是,没有办法从一个Function1
实例中看出:
scala> second.isDefinedAt(Nil)
<console>:13: error: value isDefinedAt is not a member of List[Int] => Int
second.isDefinedAt(Nil)
^
我们需要声明second
的类型PartialFunction[List[Int], Int]
如下:
scala> val second: PartialFunction[List[Int], Int] = {
| case x :: y :: _ => y
| }
second: PartialFunction[List[Int],Int] = <function1>
(顺便说一句,请注意,您的问题中有此代码的拼写错误 - 以上是应该如何定义的。)
现在我们没有任何警告!我们已经告诉编译器这是一个PartialFunction
实例,所以编译器知道它对于某些参数是未定义的,所以警告是多余的。我们现在可以验证这个事实:
scala> second.isInstanceOf[PartialFunction[List[Int], Int]]
res6: Boolean = true
我们现在还可以验证它是否是为特定值定义的:
scala> second.isDefinedAt(Nil)
res7: Boolean = false
scala> second.isDefinedAt(List(1, 2))
res9: Boolean = true
等等。(书中描述的Scala编译器能够isDefinedAt
为我们实现这个神奇的功能。)
那么,这是否意味着我们现在应该编写如下代码:
def getSecondValue(l: List[Int]): Option[Int] = {
// Check if second is defined for this argument. If so, call it and wrap in Some.
if(second.isDefinedAt(l)) Some(second(l))
// Otherwise, we do not have a second value.
else None
}
嗯,这也有点冗长。幸运的是,一旦second
是一个PartialFunction
实例,我们可以将上面的内容重写为:
def getSecondValue(l: List[Int]): Option[Int] = second.lift(l)
该lift
方法将一个部分函数转换为一个完整函数,该函数为每个参数返回一个定义的值:如果second
定义了参数 to,那么我们得到一个Some(value)
; 否则,我们得到None
.
您会发现偏函数的概念,并且随着您对函数式编程PartialFunction
越来越熟悉,它会变得更加有用。如果您现在不明白,请不要担心;一切都会变得清晰。