R语言教程-R编程10

函数进阶

函数式编程

基本R的函数式编程支持

使用purrr包的泛函的好处是用法风格一致, 有许多方便功能。
基本Rlapply函数用输入的函数对数据的每个元素进行变换,格式为

lapply(X, FUN, ...)

其中X是一个列表或向量, FUN是一个函数(可以是有名或无名函数), 结果也总是一个列表, 结果列表的第个元素是将X的第个元素输入到FUN中的返回结果。 ...参数会输入到FUN中。 这与purrr::map()功能类似。

sapply与lapply函数类似, 但是sapply试图简化输出结果为向量或矩阵, 在不可行时才和lapply返回列表结果。
sapply()结果是一个矩阵, 矩阵的第列保存FUN(X[i])的结果。 因为sapply()的结果类型的不确定性, 在自定义函数中应慎用。

vapply(X, FUN, FUN.VALUE, …)
其中FUN.VALUE是每个FUN(X[i])的返回值的例子, 要求所有FUN(X[i])结果类型和长度相同。

例如,求d.class每一列类型的问题,用lapply,写成:

lapply(d.class, typeof)

lapply的结果总是列表。 sapply会尽可能将结果简化为向量或矩阵,如:

sapply(d.class, typeof)

或使用vapply():

vapply(d.class, typeof, "")

vapply可以处理遍历时每次调用的函数返回值不是标量的情形, 结果为矩阵, purrr包的map_dbl等只能处理调用的函数返回值是标量的情形。

Map()与purrr::map、purrr::pmap功能类似, 以一个函数作为参数, 可以对其它参数的每一对应元素进行变换, 结果为列表。

例如, 对数据框d, 如下的程序可以计算每列的平方和:

d <- data.frame(
x = c(1, 7, 2),
y = c(3, 5, 9)
)
Map(function(x) sum(x^2), d)

purrr包map的写法:

map(d, ~ sum(.^2))

实际上,这个例子也可以用lapply()改写成

lapply(d, function(x) sum(x^2))

Map()比lapply()增强的地方在于它允许对多个列表的对应元素逐一处理。 例如,为了求出d中每一行的最大值,可以用

Map(max, d$x, d$y)

mapply()函数与Map()类似, 但是可以自动简化结果类型, 可以看成是sapply()推广到了可以对多个输入的对应元素逐项处理。 mapply()可以用参数MoreArgs指定逐项处理时一些共同的参数。 如

mapply(max, d$x, d$y)

当d数据框有多列时为了求每行的最大值, 可以用Reduce函数将两两求最大值的运算推广到多个之间的运算。

Reduce函数功能与purrr::reduce类似, 把输入列表(或向量)的元素逐次地用给定的函数进行合并计算。

set.seed(98)
x <- replicate(3, sample(
1:5, size=5, replace=TRUE), simplify=FALSE); x
Reduce(intersect, x)

Reduce函数对多个输入默认从左向右计算, 可以用right参数选择是否从右向左合并。 参数init给出合并初值, 参数accumulate要求保留每一步合并的结果(累计)。 这个函数可以把很多仅适用于两个运算元的运算推广到多个参数的情形。
Filter(f, x)与purrr::keep作用类似, 用一个示性函数f作为筛选规则, 从列表或向量x中筛选出用f作用后为真值的元素子集。 例如

f <- function(x) x > 0 & x < 1
Filter(f, c(-0.5, 0.5, 0.9, 2))

改写:

x <- c(-0.5, 0.5, 0.9, 2)
x[x>0 & x<1]

当x是列表且其元素本身也是复合类型的时候, 就需要把判断写成一个函数, 然后可以用Filter比较简单地表达按照判断规则取子集的操作。
Find()功能与purrr::detect类似, 返回满足条件的第一个元素, 也可以用参数right=TRUE要求返回满足条件的最后一个。
Position()功能与purrr::detect_index类似, 返回第一个满足条件的元素所在的下标位置。

自定义泛函

用户也可以自定义泛函。 比如,希望对一个数据框中所有的数值型变量计算某些统计量, 要计算的统计量由用户决定而不是由此自定义函数决定, 输入的函数的结果总是数值型向量, 编写自定义的泛函为:

df.numeric <- function(df, FUN, ...){
sapply(Filter(is.numeric, df), FUN, ...)
}

这里参数FUN是用来计算统计量的函数。
例如对d.class中每个数值型变量计算最小值:

# na.rm为去掉缺失值
df.numeric(d.class, summary, na.rm=T)

函数工厂

函数的返回值可以是函数, 为此只要在函数内部定义嵌套函数并以嵌套函数为返回值。 返回函数的函数称为函数工厂, 函数工厂的输出结果称为一个闭包(closer)。 因为函数由形参表、函数体和定义环境三个部分组成, 函数工厂输出的闭包的定义环境是函数工厂的内部环境, 即函数工厂运行时产生的运行环境, 所以闭包包含了生产它的函数工厂的运行环境, 可以将闭包的一些状态信息保存在该环境中, 实现带有状态的函数。

闭包例子

符号测试:

fe1 <- function(x){
t <- 0
for (i in seq(x)){
t <- t + i
print(t)
}
}
fe1(3)

tr=runtimes

f.gen <- function(){
tr <- 0

function(){
tr <<- tr + 1
print(tr)
}
}
f <- f.gen()
f()
f()
f()

下面的这种写法不能使每次运行的结果递增。

f.gen <- function(){
tr <- 0
tr <- tr + 1
print(tr)
}
f.gen()
f.gen()

函数f.gen中定义了内嵌函数并以内嵌函数为输出, f.gen是一个函数工厂, 其返回值是一个闭包, 闭包也是一个R函数, 这个返回值“绑定”(bind)到变量名f上, 所以f是一个函数。

函数f.gen中定义了内嵌函数并以内嵌函数为输出, f.gen是一个函数工厂, 其返回值是一个闭包, 闭包也是一个R函数, 这个返回值“绑定”(bind)到变量名f上, 所以f是一个函数。

An object is data with functions. A closure is a function with data.
对象就是带着函数的数据,闭包就是带着数据的函数。闭包就是在原有函数的基础上去生产新的函数,或者简称为函数的函数,也就是函数本身可以作为参数代入新的函数里。

调用函数f时用到变量tr, 用了<<-这种格式给这个变量赋值, 这样赋值的含义是在定义时的环境中逐层向上(向外,向父环境方向)查找变量是否存在, 在哪一层找到变量就给那里的变量赋值。 这样查找的结果是变量tr在f.gen的运行环境中。 调用f的时候f.gen已经结束运行了, 一般说来f.gen的运行环境应该已经不存在了; 但是, 函数的定义环境是随函数本身一同保存的, 因为函数工厂f.gen输出了函数f, f的定义环境是f.gen的运行环境, 所以起到了把f.gen的运行环境保存在f中的效果, 而f.gen运行环境中的变量值tr也就保存在了函数f中, 可以持续被f访问, 不像f的局部变量那样每次运行结束就会被清除掉。

下面是一个类似的函数工厂例子, 产生的闭包可以显示从上次调用到下次调用之间经过的时间:

proc.time()
# 获取流逝时间
proc.time()[3]

make_stop_watch <- function(){
saved.time <- proc.time()[3]

function(){
t1 <- proc.time()[3]
td <- t1 - saved.time
saved.time <<- t1
cat("流逝时间(秒):", td, "\n")
invisible(td)
}
}
ticker <- make_stop_watch()
ticker()
## 流逝时间(秒): 0
for(i in 1:1000) sort(runif(10000))
ticker()
## 流逝时间(秒): 1.53

其中proc.time()返回当前的R会话已运行的时间, 结果在MS Windows系统中有三个值,分别是用户时间、系统时间、流逝时间, 其中流逝时间比较客观。

动态查找和懒惰求值引起的问题

make.pf <- function(power){
function(x) x^power
}
squ <- make.pf(2)

squ(4)
make.pf <- function(power){
function(x) x^power
}
p <- 2
# 现在相当于square函数是求输入值x的平方
square <- make.pf(p)
# 给p重新赋值后,make.pf的power值变为3,现在square函数是求输入值的立方。
p <- 3
#
square(4)
square(3)

在生产出square函数时, 选项p的值是2, 所以函数square应该是做平方变换的函数, 虽然在调用之前p的值被改成了3, 但是按理说不应该修改已经生产出来的square定义。 程序结果说明调用square时用的是p=3的值, 这是怎么回事?

R函数有懒惰求值规则, 在生产出square的那一步, 因为并不需要实际计算x^power, 所以实参p的值并没有被计算, 而是将square的定义环境中的power指向了全局空间的变量p, 调用square(4)的时候才实际需要power的值, 这时power才求值, 其值为p的当前值。

避免这样的问题的办法是在函数工厂内用force()函数命令输入的参数当场求值而不是懒惰求值。 如:

make.pf <- function(power){
force(power)
function(x) x^power
}
# 给定一个P值
p <- 3
square <- make.pf(p)
# 再给p赋值时,p值也不会发生改变
p <- 2
square(4)

因为函数工厂生产出的闭包函数保存了函数工厂的运行环境, 如果这个运行环境很大, 就会造成较大的不必要的内存占用。 所以, 函数工厂内应尽量不要有占用大量内存的变量。 可以在函数工厂内用rm()删除不再使用的变量。

函数算子

函数算子输入函数,输出函数, 通常用来对输入函数的行为进行改进或做细微的修改。 基本R的Vectorize函数输入一个函数, 将其改造成支持向量化的版本。

下面的dot_every函数输入一个函数, 将其改造为被循环调用时可以每调用一定次数就显示一个小数点, 这样可以用来显示循环的进度

dot_every <- function(f, n) {
# 输入一次f和n之后,这个值会固定,再次赋值时不发生改变
force(f)
force(n)

i <- 0
function(...) {
i <<- i + 1
if (i %% n == 0) cat(".")
f(...)
}
}
sim <- function(i){
x <- runif(1E6)
invisible(sort(x))
}
library(purrr)
walk(1:100, dot_every(sim, 10))

环境

基本认识
环境作为一个数据结构与有名的列表相似, 但是其中的名字必须都互不相同, 且没有次序(类似集合), 环境都有一个父环境, 修改环境内容时都不制作副本。

rlang扩展包可以比较方便地操作R的语法内容。 可以用rlang::env()生成新的环境, 这类似于list()函数的用法, 如:

e1 <- rlang::env(
a = FALSE,
b = "a",
c = 2.3,
d = 1:3)

环境的作用是将一系列的名字(变量名、函数名等)与R对象绑定起来, 即建立从名字到对象的对应关系, 不计次序。 对环境的修改是直接进行而不制作副本的。 如:

e1$e <- list(x=1, y="abcd")
# 显示环境, 只会显示一个地址信息,
e1

rlang包的env_print()函数可以给出较多的信息:

rlang::env_print(e1)

rlang::env_names()可以获得环境中绑定的名字组成的字符型向量:

rlang::env_names(e1)

父环境

用rlang::env_parent()获得父环境, 用rlang::env_parents()获得各层父环境,如:

rlang::env_print(e1)
cat('-----\n')
rlang::env_parent(e1)
cat('-----\n')
rlang::env_parents(e1)

在上层环境中赋值

用“<<-”在各级父环境中赋值, 最先在那一层父环境中找到变量就在那一层中赋值, 如果直到全局环境都没有找到变量, 就在全局环境中新建一个变量。 全局变量应谨慎使用, 它使得程序之间的数据输入输出变得不明晰。 在使用闭包时常常需要使用这种赋值方式保存并修改一个闭包的状态。

如:

f0 <- function(){
x <- 0
f1 <- function(){
f2 <- function(){
x <<- x+1
x
}
f2()
}
f1
}
f01 <- f0()
f01()
f01()

在上面的例子中, <<-首先在f1环境内查找x, 没有找到就继续向上在f0的环境内查找x。

# 输入数字x,求0~x所有整数相加的和
f2 <- function(x){
t <- 0
for (i in seq(x)){
t <- t + i
}
t
}
f2(4)

library()调用的包会形成一个全局环境的父环境。使用search()函数可以查看当前启用包的环境。