高阶函数 (HOF) 通常定义为这类函数,它 (a) 将其他函数作为输入参数或 (b) 返回函数作为结果。 在 Scala 中,HOF 是可能的,因为函数是一等值。
需要注意的是,虽然我们在本文档中使用了常见的行业术语“高阶函数”,但在 Scala 中,该短语同时适用于 方法 和 函数。 得益于 Scala 的 Eta 扩展技术,它们通常可以在相同的地方使用。
从消费者到创造者
在本书到目前为止的示例中,您已经了解了如何成为方法的消费者,该方法将其他函数作为输入参数,例如使用诸如 map
和 filter
之类的 HOF。
在接下来的几节中,您将了解如何成为 HOF 的创造者,包括:
- 如何编写将函数作为输入参数的方法
- 如何从方法中返回函数
在这个过程中你会看到:
- 用于定义函数输入参数的语法
- 引用函数后如何调用它
作为本次讨论的一个有益的副作用,一旦您对这种语法感到满意,您将使用它来定义函数参数、匿名函数和函数变量,并且也更容易阅读有关高阶函数的 Scaladoc。
理解过滤器的 Scaladoc
要了解高阶函数的工作原理,深入研究一个示例会有所帮助。
例如,您可以通过查看 Scaladoc 来了解 filter
接受的函数类型。
这是 List[A]
类中的 filter
定义:
def filter(p: (A) => Boolean): List[A]
这表明 filter
是一个接受名为 p
的函数参数的方法。
按照惯例,p
代表 谓词(predicate),它只是一个返回 Boolean
值的函数。
所以 filter
将谓词 p
作为输入参数,并返回一个 List[A]
,其中 A
是列表中保存的类型;如果你在 List[Int]
上调用 filter
,A
是 Int
类型。
在这一点上,如果你不知道 filter
方法的用途,你只知道它的算法以某种方式使用谓词 p
创建并返回 List[A]
。
具体看函数参数 p
,这部分 filter
的描述:
p: (A) => Boolean
意味着您传入的任何函数都必须将类型 A
作为输入参数并返回一个 Boolean
。
因此,如果您的列表是 List[Int]
,则可以将通用类型 A
替换为 Int
,并像这样读取该签名:
p: (Int) => Boolean
因为 isEven
具有这种类型——它将输入 Int
转换为结果 Boolean
——它可以与 filter
一起使用。
编写接受函数参数的方法
鉴于此背景,让我们开始编写将函数作为输入参数的方法。
注意:为了使下面的讨论更清楚,我们将您编写的代码称为方法,将您作为输入参数接受的代码称为函数。
第一个例子
要创建一个接受函数参数的方法,您所要做的就是:
- 在方法的参数列表中,定义要接受的函数的签名
- 在你的方法中使用那个函数
为了证明这一点,这里有一个方法,它接受一个名为 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")
然后将该方法与 Int
一起传递给executeNTimes
:
scala> executeNTimes(helloWorld, 3)
Hello, world
Hello, world
Hello, world
优秀。
executeNTimes
方法执行 helloWorld
函数 3 次。
需要多少参数
您的方法可以继续变得尽可能复杂。
例如,此方法采用类型为 (Int, Int) => Int
的函数,以及两个输入参数:
def executeAndPrint(f: (Int, Int) => Int, i: Int, j: Int): Unit =
println(f(i, j))
因为这些 sum
和 multiply
方法与该类型签名匹配,所以它们可以与两个 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 中有关高阶函数的内容时,这些内容变得更容易了。
Contributors to this page:
Contents
- 导言
- Scala 3 特性
- 为什么是 Scala 3 ?
- Scala 的味道
- Hello, World!
- The REPL
- 变量和数据类型
- 控制结构
- 领域建模
- 方法
- 头等函数
- 单例对象
- 集合
- 上下文抽象
- 顶层定义
- 总结
- 类型初探
- 字符串插值
- 控制结构
- 领域建模
- 工具
- OOP 领域建模
- 函数式领域建模
- 方法
- 方法特性
- main 方法
- 总结
- 函数
- 匿名函数
- 函数变量
- Eta 扩展
- 高阶函数
- 自定义 map 函数
- 创建可以返回函数的方法
- 总结
- 打包和导入
- Scala 集合
- 集合类型
- 集合方法
- 总结
- 函数式编程
- 什么是函数式编程?
- 不可变值
- 纯函数
- 函数是值
- 函数式错误处理
- 总结
- 类型和类型系统
- 类型推断
- 泛型
- 相交类型
- 联合类型
- 代数数据类型
- 型变
- 不透明类型
- 结构化类型
- 依赖函数类型
- 其他类型
- 上下文抽象
- 扩展方法
- Given 实例和 Using 语句
- 上下文绑定
- Given 导入
- 实现类型类
- 多元相等性
- 隐式转换
- 总结
- 并发
- Scala 工具
- 使用 sbt 构建和测试 Scala 项目
- worksheet
- 与 Java 交互
- 向 Java 开发者介绍Scala
- Scala for JavaScript Developers
- Scala for Python Developers
- 下一步去哪