【译】Scala的偏函数和部分应用函数

原文:Partial & Partially Applied Functions in Scala
PDF:下载
来源:PlayScala中文社区

Partial Functions

在Scala中,模式匹配(Pattern Matching)可以用于创建偏函数(Partial Function),偏函数是一种特殊的一元函数,它并不会接受符合参数类型的所有可能值,而是只接受特定的值。例如,一个计算平方根的函数只接受正数而不接受负数。

val squareRoot: PartialFunction[Double, Double] = {
 case x if x >= 0 => Math.sqrt(x)
}

上面的 squareRoot 只接受正数,并且返回其平方根。如果传入的是负数,则会抛出运行时异常scala.MatchError。

偏函数提供了一个 isDefinedAt 方法,可以检测一个特定的参数值是否被接受。

squareRoot.isDefinedAt(2) shouldEqual true
squareRoot.isDefinedAt(-2) shouldEqual false

多个偏函数可以使用 orElseandThen 方法串接起来,

val positive: PartialFunction[Int, Int] = {
 case x if x >= 0 => x
}

val odd: PartialFunction[Int, Boolean] = {
 case x if x % 2 == 1 => true
}

val even: PartialFunction[Int, Boolean] = {
 case x if x % 2 == 0 => true
}

val evenCheck: PartialFunction[Int, Boolean] = positive andThen even

val oddCheck: PartialFunction[Int, Boolean] = positive andThen odd

由于返回结果也是偏函数,所以仍然可以使用 isDefined 方法检测特定参数值是否被接受,

evenCheck.isDefinedAt(-2) shouldEqual false
evenCheck.isDefinedAt(2) shouldEqual true

当我们想要实现一个验证系统的时候,偏函数的这个特性将会变得非常有用。我们可以实现一系列的检查用于检测输入数据是否满足特定的规则。

val finalCheck = check1 andThen check2 andThen check3 ...

这个实现方案很容易扩展,我们可以随意地从 finalCheck 上增加或减少检查项。

在集合类上也可以使用偏函数,

val greaterThan20: PartialFunction[Any, Int] = {
 case i: Int if i > 20 => i
}

List(1, 45, 10, "blah", true, 25) collect greaterThan20 
shouldEqual List(45, 25)

collect 方法接受一个类型为 PartialFunction 的偏函数,并且自动跳过未在该偏函数上定义的元素。

Partially Applied Functions

在函数式编程语言中,调用函数的过程也叫做将函数应用(applying)到参数。当调用时传入了所有的参数,就叫做将函数完全应用(fully applied)到了所有参数。如果在调用时只传入了部分参数,返回的结果就是一个部分应用函数(Partially Applied Function)。当只传入部分参数时,Scala并不会报错,而是简单地应用(apply)了这些参数,并返回一个接受剩余参数的新函数。

val divide = (num: Double, den: Double) => {
 num / den
}

val halfOf: (Double) => Double = divide(_, 2)

halfOf 20 shouldEqual 10

我们知道,取某个数的一半是一个很简单的除法,分母固定为2,所以 halfOf 可以由部分应用 divide 函数得到。_ 是剩余参数的占位符。

在Scala中,部分应用函数(Partially Applied Function)经常会误认为是柯里化函数(Currying Function)。不管是柯里化函数,还是部分应用函数,它们都用于对函数进行降元(译注:减少参数个数),只是柯里化函数实现降元的方式有些不同。将一个接受多个参数的函数分解成一系列的函数,每个函数只接受一个参数,这个过程叫做柯里化。

val curriedDivide: (Double) => (Double) => Double = divide.curried

curriedDivide 函数的类型是 (Double) => (Double) => (Double),这表明 divide 函数被分解成了两个函数,每个函数接受一个参数,divide 函数上定义的两个参数按顺序分配在这两个函数上。部分应用 curriedDivide 函数将会得到相同 halfOf 函数。

val halfOf: (Double) => Double = curriedDivide(_)(2)

halfOf(20) shouldEqual 10

利用柯里化函数(Currying Function)和部分应用函数(Partially Applied Function),我们不需要编写额外的代码,便可以基于普通函数创建一些特殊用途的函数,从而保持我们的代码随处可复用。

译注:

柯里化函数(Currying Function)和部分应用函数(Partially Applied Function)从本质上来说都是用于对函数进行降元,所以它们通常被用于完成相似的任务。但是在使用时要注意以下几点区别:

  • 柯里化函数的参数必须按照定义的顺序逐次调用,而部分应用函数可以传入任意位置的参数
  • 部分应用函数的未传入参数部分需要使用占位符,由于编译器无法自动推断出占位符类型,所以需要显式标注占位符类型,编写时较为繁琐
  • 利用柯里化函数(Currying Function)可以实现类似语言级别提供的内置语法,详见:Scala基础 - 柯里化(Currying)及其应用
编辑于 2018-01-22