Scala 3 — 书籍

高阶函数

语言

高阶函数 (HOF) 通常定义为一个函数,它 (a) 将其他函数作为输入参数,或 (b) 返回一个函数作为结果。在 Scala 中,HOF 是可能的,因为函数是一等值。

作为一个重要的说明,虽然我们在本文档中使用了常见的行业术语“高阶函数”,但在 Scala 中,此短语同时适用于方法函数。由于 Scala 的 Eta 扩展技术,它们通常可以在相同的地方使用。

从使用者到创建者

在本书迄今为止的示例中,您已经了解到如何成为将其他函数作为输入参数的方法的使用者,例如使用 mapfilter 等 HOF。在接下来的几节中,您将看到如何成为 HOF 的创建者,包括

  • 如何编写将函数作为输入参数的方法
  • 如何从方法返回函数

在此过程中,您将看到

  • 用于定义函数输入参数的语法
  • 如何调用函数引用

作为此讨论的有益副作用,一旦你熟悉此语法,你将使用它来定义函数参数、匿名函数和函数变量,并且阅读高阶函数的 Scaladoc 也变得更加容易。

理解 filter 的 Scaladoc

要理解高阶函数的工作原理,深入了解一个示例会有帮助。例如,你可以通过查看 Scaladoc 来了解函数 filter 接受的类型。以下是 List[A] 类中的 filter 定义

def filter(p: A => Boolean): List[A]

这说明 filter 是一个方法,它接受一个名为 p 的函数参数。根据惯例,p 代表一个谓词,它只是一个返回 Boolean 值的函数。因此,filter 将谓词 p 作为输入参数,并返回一个 List[A],其中 A 是列表中保存的类型;如果你对 List[Int] 调用 filterA 就是类型 Int

此时,如果你不知道 filter 方法的目的,你所知道的只是它的算法以某种方式使用谓词 p 来创建和返回 List[A]

具体查看函数参数 pfilter 描述的这一部分

p: A => Boolean

这意味着你传入的任何函数都必须将类型 A 作为输入参数,并返回 Boolean。因此,如果你的列表是 List[Int],你可以用 Int 替换泛型类型 A,并像这样读取该签名

p: Int => Boolean

因为 isEven 具有此类型——它将输入 Int 转换为结果 Boolean——所以它可以与 filter 一起使用。

编写接受函数参数的方法

鉴于此背景,让我们开始编写将函数作为输入参数的方法。

注意:为了使以下讨论清晰,我们将你编写的代码称为方法,将你作为输入参数接受的代码称为函数

第一个示例

要创建接受函数参数的方法,您只需

  1. 在方法的参数列表中,定义您想要接受的函数的签名
  2. 在您的方法中使用该函数

为了演示这一点,这里有一个方法,它接受一个名为 f 的输入参数,其中 f 是一个函数

def sayHello(f: () => Unit): Unit = f()

代码的这一部分——类型签名——指出 f 是一个函数,并定义 sayHello 方法将接受的函数类型

f: () => Unit

以下是它的工作原理

  • f 是函数输入参数的名称。它就像给 String 参数命名为 s 或给 Int 参数命名为 i
  • f 的类型签名指定了此方法将接受的函数的类型
  • f 签名中 () 部分(在 => 符号的左侧)指出 f 不接受任何输入参数。
  • 签名中 Unit 部分(在 => 符号的右侧)表示 f 不应返回有意义的结果。
  • 回顾 sayHello 方法的主体(在 = 符号的右侧),那里的 f() 语句调用传入的函数。

现在我们已经定义了 sayHello,让我们创建一个函数来匹配 f 的签名,以便我们可以对其进行测试。以下函数不接受任何输入参数,也不返回任何内容,因此它与 f 的类型签名匹配

def helloJoe(): Unit = println("Hello, Joe")

因为类型签名匹配,所以您可以将 helloJoe 传递到 sayHello

sayHello(helloJoe)   // prints "Hello, Joe"

如果您以前从未这样做过,那么恭喜:您刚刚定义了一个名为 sayHello 的方法,它接受一个函数作为输入参数,然后在其方法主体中调用该函数。

sayHello 可以使用许多函数

重要的是要知道这种方法的优点不在于 sayHello 可以将 一个 函数作为输入参数;优点在于它可以使用与 f 的签名匹配的 任何 函数。例如,因为此下一个函数不使用任何输入参数且不返回任何内容,所以它也可以与 sayHello 一起使用

def bonjourJulien(): Unit = println("Bonjour, Julien")

它在 REPL 中

scala> sayHello(bonjourJulien)
Bonjour, Julien

这是一个好的开始。现在唯一要做的事情是查看更多有关如何为函数参数定义不同类型签名的示例。

定义函数输入参数的一般语法

在此方法中

def sayHello(f: () => Unit): Unit

我们注意到 f 的类型签名为

() => Unit

我们知道这意味着“一个不使用任何输入参数且不返回任何有意义内容(由 Unit 给出)的函数”。

为了演示更多类型签名示例,这是一个使用 String 参数并返回 Int 的函数

f: String => Int

哪种函数使用字符串并返回整数?“字符串长度”和校验和函数是两个示例。

类似地,此函数使用两个 Int 参数并返回 Int

f: (Int, Int) => Int

你能想象哪种函数与该签名匹配?

答案是任何使用两个 Int 输入参数并返回 Int 的函数都与该签名匹配,因此所有这些“函数”(实际上是方法)都匹配

def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
def multiply(a: Int, b: Int): Int = a * b

正如你可以从这些示例中推断的那样,定义函数参数类型签名的通用语法是

variableName: (parameterTypes ...) => returnType

由于函数式编程就像创建和组合一系列代数方程,因此在设计函数和应用程序时经常会考虑类型。你可能会说你“思考类型”。

将函数参数与其他参数一起使用

对于 HOF 来说,它们真正有用,它们还需要一些数据来处理。对于像 List 这样的类,它的 map 方法已经有了要处理的数据:List 中的数据。但是对于没有自己数据的独立 HOF,它还应该接受数据作为其他输入参数。

例如,这里有一个名为 executeNTimes 的方法,它有两个输入参数:一个函数和一个 Int

def executeNTimes(f: () => Unit, n: Int): Unit =
  for (i <- 1 to n) f()
def executeNTimes(f: () => Unit, n: Int): Unit =
  for i <- 1 to n do f()

正如代码所示,executeNTimes 执行 f 函数 n 次。因为像这样的简单 for 循环没有返回值,所以 executeNTimes 返回 Unit

要测试 executeNTimes,请定义一个与 f 的签名匹配的方法

// a method of type `() => Unit`
def helloWorld(): Unit = println("Hello, world")

然后将该方法传递给 executeNTimes 以及一个 Int

scala> executeNTimes(helloWorld, 3)
Hello, world
Hello, world
Hello, world

很好。executeNTimes 方法执行 helloWorld 函数三次。

任意数量的参数

您的方法可以继续变得复杂,以满足需要。例如,此方法采用类型为 (Int, Int) => Int 的函数,以及两个输入参数

def executeAndPrint(f: (Int, Int) => Int, i: Int, j: Int): Unit =
  println(f(i, j))

由于这些 summultiply 方法与该类型签名匹配,因此它们可以与两个 Int 值一起传递到 executeAndPrint

def sum(x: Int, y: Int) = x + y
def multiply(x: Int, y: Int) = x * y

executeAndPrint(sum, 3, 11)       // prints 14
executeAndPrint(multiply, 3, 9)   // prints 27

函数类型签名一致性

了解 Scala 函数类型签名的一个好处是,您用于定义函数输入参数的语法与用于编写函数字面的语法相同。

例如,如果您要编写一个计算两个整数之和的函数,您将像这样编写它

val f: (Int, Int) => Int = (a, b) => a + b

该代码由类型签名组成

val f: (Int, Int) => Int = (a, b) => a + b
       -----------------

输入参数

val f: (Int, Int) => Int = (a, b) => a + b
                           ------

和函数体

val f: (Int, Int) => Int = (a, b) => a + b
                                     -----

Scala 的一致性在此处显示,其中此函数类型

val f: (Int, Int) => Int = (a, b) => a + b
       -----------------

与您用于定义函数输入参数的类型签名相同

def executeAndPrint(f: (Int, Int) => Int, ...
                       -----------------

一旦您熟悉此语法,您将使用它来定义函数参数、匿名函数和函数变量,并且更容易阅读高阶函数的 Scaladoc。

此页面的贡献者