R 语言结合了面向对象编程语言和函数式编程语言的特性,由于拥有函数式编程的特性,R 的每一个运算符,实际上也是函数,同样,面向对象的特性决定了你接触到的 R 中所有东西(从数字到字符串到矩阵等)都是对象。这些综合的特质决定了 R 这门语言的特殊性,最大的特点就是开源。
之前简单了解了 R 语言的 S3 对象以及泛型函数,下面介绍 R 语言的函数式编程,主要包含三个模块:匿名函数,闭包(由函数编写的函数)和函数列表。其主要目的还是为了减少代码量或提高效率。
匿名函数
在 R 语言中,函数本身就是一个对象,使用<-符号只是将这个对象绑定到一个符号上以供重复使用,但若是只使用一次或者是传入参数类型必须是函数类型时,匿名函数就可以派上用场了。R 函数不会自动绑定到名称,与许多语言(例如 C , C ++,Python 和 Ruby )不同,R 没有用于创建命名函数的特殊语法:创建函数时,可以使用常规赋值运算符为其命名。不命名的话你得到的就是一个匿名函数。
# 比如这些场景
lapply(mtcars, function(x) length(unique(x)))
Filter(function(x) !is.numeric(x), mtcars)
integrate(function(x) sin(x) ^ 2, 0, pi)
# 与R中的所有函数一样,匿名函数具有formals(),abody()和父级environment()
formals(function(x = 4) g(x) + h(x)) # 函数形参
`## $x
## [1] 4`
body(function(x = 4) g(x) + h(x))
`## [1] g(x) + h(x)`
environment(function(x = 4) g(x) + h(x))
`## [1] <environment: R_GlobalEnv>`
在 map 或者 lapply 这种可以并行处理或者批处理函数中的 f 参数中使用匿名函数将会特别有用,当然,匿名函数的最大功能还是创建闭包。
闭包
“对象是具有功能的数据。闭包是具有数据的功能。” —约翰·库克(John D. Cook) 闭包之所以得名,是因为它们封闭了父函数的环境并且可以访问其所有变量。这非常有用,因为它允许我们拥有两个级别的参数:控制操作的父级别和完成工作的子级别。
# 一个小例子
func <- function(x) {
function(y) {
paste(x, y)
}
}
func1 <- func('吃饭')
func2 <- func('睡觉')
# 调用匿名函数
func1('小明')
`## [1] 小明 吃饭`
func2('小明')
`## [1] 小明 睡觉`
以上就是闭包的一般应用场景了,我们可以在父函数中定义一系列操作,创建闭包后子函数可以访问到父函数的所有变量,再执行子函数的操作。那么闭包究竟是怎样工作的呢?
# 首先我们打印闭包,得到的只有子函数的函数体
func1
## [1] function(y) {
paste(x, y)
}
<environment: 0xe3f6028>
# 闭包的父环境是创建它的函数的执行环境
func <- function() {
print(environment())
function() {
0
}
}
func1 <- func()
## [1] <environment: 0xce6b3e8>
environment(func1)
## [1] <environment: 0xce6b3e8>
以上可以看出来闭包函数实际上还是那个子函数,只不过它所在的是创建它的父函数的执行环节。函数返回值后,执行环境通常会消失。但是,闭包捕获了父函数的执行环境。这意味着当函数 func 返回函数 func1 时,函数 func1 捕获并存储函数 func 的执行环境,并且它不会消失。
下面介绍函数父环境和子环境(创建环境和执行环境)在函数调用之时保持联系的状态。在不同环境级别上管理变量的关键是双箭头分配运算符(<<-
)。与通常<-
在当前环境中分配对象的单箭头不同,双箭头运算符将继续查找父级环境链,直到找到匹配的名称。
# 首先是在闭包中应用双箭头分配运算符
func <- function() {
i <- 0
function() {
i <<- i + 1
i
}
}
func1 <- func()
func1()
## [1] 1
func1()
## [1] 2
func1()
## [1] 3
# 再创建一个闭包
func2 <- func()
func2()
## [1] 1
func2()
## [1] 2
func2()
## [1] 3
func1 和 func2 函数通过不修改其本地环境中的变量来规避“改变同一个变量”限制。由于更改是在不变的父(或封闭)环境中进行的,因此可以在函数调用之间保留更改。那么 func1 和 func2 之间为什么改变各自的封闭环境变量不受影响呢,因为func1 和 func2 在创建闭包的时候使用的已经不是同一个 func 函数了,而是复制出来一个父环境以供当前闭包使用,如需测试在 func 函数中加上打印 environment 就可以了。
还有一个例子比较直观,以下的两个函数,分别调用 func1 创建闭包 func2 哪个会对本地环境的i做出改变呢?答案是 func1 ,因为 func2 中闭包的父环境是 func2 的运行环境,一旦在父环境中找到了 i 变量就不会再去父环境的父环境中查找了,若是删掉 func2 中的 i <- 0 这行代码则会改变本地环境中的i。
i <- 0
func1 <- function() {
i <<- i + 1
i
}
func2 <- function() {
i <- 0
function() {
i <<- i + 1
i
}
}
函数列表
关于函数列表理解起来就比较简单了,也有适用的环境。在 R 中,函数可以存储在列表中。这使得与相关函数组一起使用更容易,就像数据框使与相关向量组一起使用一样。
# 假设要计算一组数据的聚合数据
arg <- c(1,2,3,4,5)
mean(arg)
## [1] 3
max(arg)
## [1] 5
min(arg)
## [1] 1
# 如果适用函数列表的方法
func <- list(
me <- function(x) mean(x),
ma <- function(x) max(x),
mi <- function(x) min(x)
)
lapply(func, function(f) f(arg))
map(func, function(f) f(arg))
## [[1]]
## [1] 3
## [[2]]
## [1] 5
## [[3]]
## [1] 1