上一节讨论了闭包,即定义在父函数中的函数。接下来要讨论的是高阶函数,也就是
将另外一个函数做为参数的函数。
在进入这个专题以前,咱们须要先了解一下,当一个函数做为变量或参数传递给另外一
个函数时的具体运行方式。
1.为函数建立别名
首先要考虑的问题是:若是将一个现有的函数赋给一个变量,会影响函数的封闭环境
吗?若是会影响的话,那么非局部定义的符号的搜索路径就会不一样的。
如下代码演示了为何一个函数被赋给另外一个符号时,其封闭环境不会改变。咱们定
义一个简单函数 f1( ),调用该函数会输出其执行环境、封闭环境和调用环境。而后定义
另外一个输出 3 个环境的函数 f2( ),不一样的是,f2( ) 先将 f1( ) 赋值给一个局部变量 p,
而后在 f2( ) 的内部调用它。
若是 p<- f1 局部定义了一个函数,那么 p 的封闭环境就是 f2( ) 的执行环境。否
则,其封闭环境还是定义 f1( ) 的环境 —— 全局环境:
f1 <- function() {
cat("[f1] executing in ")
print(environment())
cat("[f1] enclosed by ")
print(parent.env(environment()))
cat("[f1] calling from ")
print(parent.frame())
}
f2 <- function() {
cat("[f2] executing in ")
print(environment())
cat("[f2] enclosed by ")
print(parent.env(environment()))
cat("[f2] calling from ")
print(parent.frame())
p <- f1
p()
}
f1()
## [f1] executing in <environment: 0x000000001435d700>
## [f1] enclosed by <environment: R_GlobalEnv>
## [f1] calling from <environment: R_GlobalEnv>
f2()
## [f2] executing in <environment: 0x0000000014eb2200>
## [f2] enclosed by <environment: R_GlobalEnv>
## [f2] calling from <environment: R_GlobalEnv>
## [f1] executing in <environment: 0x0000000014eaedf0>
## [f1] enclosed by <environment: R_GlobalEnv>
## [f1] calling from <environment: 0x0000000014eb2200>
咱们依次调用这两个函数,发现 p 是从 f2( ) 的执行环境中调用,但它的封闭环境没
有变。换句话说,p 和 f1( ) 的搜索路径彻底相同。事实上,p<-f1 这个语句就是用 p 表
示函数 f1( ),它们指向彻底相同的函数。
2.将函数看成变量使用
R 中的函数不像其余编程语言的函数那样特殊,一切事物都是对象。函数也不例外,
而且能够经过变量来引用。
假设咱们有这样一个函数:
f1 <- function(x, y) {
if (x > y) {
x + y
} else {
x - y
}
}
在上面这个函数中,两个条件分支分别转向不一样的表达式,所以结果也可能不一样。为
了达成相同的目标,咱们也能够将条件分支转向不一样的函数,将结果存储在一个变量中,
最后调用该变量所表明的函数得到最终结果:
f2 <- function(x, y) {
op <- if (x > y) `+` else `-`
op(x, y)
}
须要注意到的是,在 R 中,咱们所作的一切都是由函数完成的。最基本的运算符 + 和 −
也是函数。它们也能够赋给变量 op,若是 op 确实是一个函数的话,即可以调用它。
3.将函数看成参数传递
前面的例子代表,咱们能够像传递其余对象同样轻松地传递函数,包括在参数中传递
函数。
在下面的例子中,咱们定义两个函数 add( )和 product( ):
add <- function(x, y, z) {
x + y + z
}
product <- function(x, y, z) {
x * y * z
}
252 第 9 章 元编程
而后再定义一个函数 combine( ),以参数 f 指定的某种方式将 x、y 和 z 组合起来。
这里,咱们假设 f 是一个函数,而且在调用时须要 3 个参数。这样 combine( ) 函数就会
更灵活。它没有限定必须以某种特定的方式来组合输入,而是容许用户指定组合方式:
combine <- function(f, x, y, z) {
f(x, y, z)
}
咱们能够将刚才定义的函数 add( )和 product( ) 传递给 combine( ),看看是否
能够运行:
combine(add, 3, 4, 5)
## [1] 12
combine(product, 3, 4, 5)
## [1] 60
当咱们调用 combine(add, 3, 4, 5) 时,函数体包含 f = add 和 f(x, y, z),
即 add(x, y, z)。一样的逻辑也适用于将 product 传递给 combine( ) 函数。因为
第 1 个参数接收了一个函数,所以 combine( ) 是一个高阶函数。
咱们须要高阶函数的另外一个缘由是,它使代码在更高的抽象化层次下读写起来更容易。
在许多状况下,使用高阶函数会使代码更短,表达力更强。例如,for 循环就是一个沿着
向量或列表迭代的普通的控制流工具。
假设咱们须要将一个名为 f 的函数应用在向量 x 的每一个元素上。若是函数自己是向量
化的,就能够直接调用 f(x)。然而并非每一个函数都支持向量化操做,也不是全部函数
都须要向量化。若是想完成上述操做,运用下面的 for 循环就能够了:
result <- list()
for (i in seq_ _along(x)) {
result[[i]] <- f(x[[i]])
}
result
在上面这个循环中,seq_along(x) 生成从 1 开始,与 x 等长的整数序列,效果等价
于 1:length(x)。代码看起来很简单且容易实现,但若是一直使用它,缺点就很明显了。
假设每次迭代中的运算都很复杂,那么 for 循环就会变得难以理解。仔细想一想,咱们
就会发现这段代码只是告诉 R 怎样完成任务,而不是这个任务是什么。当看到一段很长,
有时还包含嵌套的循环时,就很难搞明白它到底在干什么。
9.1 函数式编程 253
相反,咱们能够经过调用 lapply( ) 将一个函数(f)做用于向量或列表(x)的每
个元素上,前面的章节也介绍过 lapply( ) 函数:
lapply(x, f)
实际上,lapply( ) 函数和下列代码是等价的,尽管下列代码是在 C 语言中实现的:
lapply <- function(x, f, ...) {
result <- list()
for (i in seq_ _along(x)) {
result[[i]] <- f(x[i], ...)
}
}
这个函数就是一个高阶函数,由于它在更高的抽象层级上工做。尽管在函数内部仍然
使用了一个 for 循环,它却将工做分红了两个抽象层级,这样每一个层级看起来都很简单。
事实上,lapply( ) 一样支持含有额外参数的 f。例如, + 有两个参数,如如下代码所示:
lapply(1:3, `+`, 3)
## [[1]]
## [1] 4
##
## [[2]]
## [1] 5
##
## [[3]]
## [1] 6
上面的代码等价于:
list(1 +3, 2 +3, 3 +3)
也等价于使用闭包生成 x+3 函数的状况:
lapply(1:3, addn(3))
## [[1]]
## [1] 4
##
## [[2]]
## [1] 5
##
## [[3]]
## [1] 6
正如咱们在前面章节中提到的,lapply( ) 函数返回一个列表。若是想要以向量形
式返回,可使用 sapply( ) 函数:
sapply(1:3, addn(3))
## [1] 4 5 6
或者,使用 vapply( ) 函数并加上类型检验:
vapply(1:3, addn(3), numeric(1))
## [1] 4 5 6
除了这些函数, R 还提供了一些其余 apply 函数族,以及 Filter( )、Map( )、
Reduce( )、Find( )、Position( ) 和 Negate( ) 函数。详情请参阅帮助文档: ?Filter 。
此外,使用高阶函数不只使代码可读性更高、表达力更强,还将每一个抽象层级的实现
分离,使它们彼此独立。相比于改进整合在一块儿的逻辑束,改进其简单的部分要容易得多。
例如,给定一个函数,咱们能够用 apply 函数族来执行向量映射。若是每次迭代都相
互独立,就能够用多核 CPU 进行并行计算,同时执行更多任务。可是,若是一开始没有使
用高阶函数,而是用 for 循环,则须要花费一段时间才能将其转换为并行代码。
例如,假设咱们使用 for 循环获取结果,每次迭代都执行计算繁重的任务。即便发现
每次迭代都相互独立,也不是总能直接将其转换为并行代码:
result <- list()
for (i in seq_ _along(x)) {
# heavy computing task
result[[i]] <- f(x[[i]])
}
result
可是,若是咱们使用高阶函数 lapply( ),事情就简单多了:
result <- lapply(x, f)
只须要一个很小的改动,就能将原代码转换为并行代码。使用 parallel::mclapply( ),
就能够利用多核计算将 f 做用到 x 的每一个元素上:
result <- parallel::mclapply(x, f)
不幸的是,mclapply( ) 不支持 Windows 操做系统。在 Windows 操做系统下须要更
多代码来执行并行应用函数。编程