R语言教程-R编程7

函数进阶

函数式编程

纯函数:

  • 没有副作用,全局变量在函数之间传递是不允许的,结果也是去确定的,不会产生不同的结果(如R的rnorm是随机产生正态分布的一组数据,每次运行产生不同的结果)
  • 不受外部影响,函数返回值只依赖于其自变量及函数的定义。不依赖于任何外部信息(也就不能依赖于全局变量与系统设置值)。
  • 不受赋值影响。函数定义不需要反复对内部对象(所谓“状态变量”)赋值或修改。

绘图的函数中经常需要用par()修改绘图参数, 这会使得后续程序出错。 为此,可以在函数开头保存原始的绘图参数, 函数结束时恢复到原始的绘图参数。
mfcol, mfrow A vector of the form c(nr, nc). Subsequent figures will be drawn in an nr-by-nc array on the device by columns (mfcol), or rows (mfrow), respectively.

R语言中的on.exit() 函数用来保证当函数退出时,函数把全局工作区恢复到原来的状态。

f <- function(){
# 设定opar参数,一行放2张图
opar <- par(mfrow=c(1,2))
# 函数运行结束后退出opar设定,恢复到原始值
on.exit(par(opar))
plot((-10):10)
plot(-10:10)
plot(10:10)
# y = x^2
plot((-10):10, ((-10):10)^2)
}
f()

如果函数中需要多次调用on.exit()指定多个恢复动作, 除第一个调用的on.exit()以外都应该加上add=TRUE选项。
如果需要指定越晚添加的恢复动作越先执行, 在on.exit()中还要加上after=FALSE选项。

泛函(functionals):支持内嵌函数, 并可以输入函数作为函数的自变量, 称这样的函数为泛函(functionals),如lapply类函数;

函数工厂:可以输出函数作为函数结果,称这样的函数为函数工厂;

函数算子(function operators):可以输入函数, 进行一定修改后输出函数的函数。

利用R的purrr扩展包,可以用统一的风格使用函数式编程, 比基本R的lapply类函数、Map、Reduce等更容易使用。

泛函

许多函数需要用函数作为参数,称这样的函数为泛函(functionals)。典型的泛函是lapply类函数。这样的函数具有很好的通用性,因为需要进行的操作可以输入一个函数来规定, 用输入的函数规定要进行什么样的操作。

purrr::map函数

设我们要对列表或向量x的每个元素x[[i]]调用函数f(), 将结果保存成一个列表。
其中的输入x是任意的,函数f是任意的。purrr包的map()函数可以用一条命令完成上述任务:

y <- map(x, f)

示例:

d.class <- readr::read_csv('class.csv')

typeof()函数求变量的存储类型

d.class
typeof(d.class[["sex"]])

d.class是一个tibble数据框, tibble也是一个列表, 每个列表元素是数据框的一列。

如下程序使用purrr::map()求每一列的存储类型, map的结果总是列表,每个列表元素对应于输入的一个元素, 如:

library(purrr)
class_ty <- map(d.class, typeof)
class_ty
# > class(class_ty)
# [1] "list"
# > mode(class_ty)
# [1] "list"

当结果比较简单时, 保存为列表不够方便, 函数unlist()可以将比较简单的列表转换为基本类型的向量

ve <- unlist(class_ty)
ve
mode(ve)

关于一个数据框的结构, 用str()函数可以得到更为详细的信息:
str即structure的缩写,得到输入参数的结构信息

str(d.class)
str(class_ty)
str(ve)

purrr::map()总是返回列表。 如果确知其调用的函数总是返回某种类型的标量值, 可以用map的变种:

  • map_lgl():返回逻辑向量;
  • map_int():返回整型向量;
  • map_dbl(): 返回双精度浮点型向量(double类型);
  • map_chr(): 返回字符型向量。

比如, 求d.class各列类型, 因为确知typeof()函数对每列返回一个标量字符串,所以可以写成:

map_chr(d.class, typeof)
# is.numeric 判断是否为数值型,结果为逻辑向量
map_lgl(d.class, is.numeric)

...形参
在R函数的形参中, 允许有一个特殊的…形参(三个小数点), 这在调用泛函类型的函数时起到重要作用。 在调用泛函时, 所有没有形参与之匹配的实参, 不论是带有名字还是不带有名字的, 都自动归入这个参数, 将会由泛函传递给作为其自变量的函数。 …参数的类型相当于一个列表, 列表元素可以部分有名部分无名, 用list(…)可以将其转换成列表再访问。

例如,函数mean()可以计算去掉部分最低、最高值之后的平均值, 用选项trim=指定一个两边分别舍弃的值的个数比例。 为了将d.class的三列数值型列计算上下各自扣除10%的平均值, 需要利用map_dbl()函数的…参数输入trim选项值,如:

library(purrr)
map_dbl(d.class[,3:5], mean, trim=0.10)

上面的数值型列是直接在程序中固定列号选出的, 也可以用map_lgl()选出:

dsub <- d.class[,map_lgl(d.class, is.numeric)]
map_dbl(dsub, mean, trim=0.10)

purrr包提供了一个keep函数, 可以专门用来选择数据框各列或列表元素中满足某种条件的子集, 这个条件用一个返回逻辑值的函数来给出。如:

library(purrr)
dsub <- keep(d.class, is.numeric)
map_dbl(dsub, mean, trim=0.10)

利用magrittr包的管道运算符%>%可以将对一个数据框的删选、计算过程更清晰地表达出来, 不需要dsub这样存储中间结果的变量

利用%>%管道传递数值:

d.class %>%
# 将d.class作为keep函数的第一个参数
keep(is.numeric) %>%
# 将keep函数的计算结果传送到map_dbl函数的第一个参数上。
map_dbl(mean, trim=0.1)

# 相当于:map_dbl(keep(d.class, is.numeric), mean, trim=0.1)

在map类泛函中...仅用来将额外的选项传递给要调用的函数, 不支持向量化, 如果需要对两个或多个自变量的对应元素作变换, 需用用purrr包的map2等函数。 如果泛函中调用的是无名函数, 则...参数会造成变量作用域理解困难。

用map处理strsplit函数结果示例

# 假设4个学生3次测验成绩
s <- c('10, 8, 7',
'5, 2, 2',
'3, 7, 8',
'8, 8, 9')
# 可以使用strsplit()函数把三组成绩拆开

t1 <- strsplit(s, ',', fixed = T)

names(t1) <- c("James", 'Jack', 'Steven', 'Marry')
t1
mode(t1)
## list
# 求三次x小测验的总分
as.numeric(t1[[1]]) %>%
sum() %>%
# 其中.就代表上一个参数传入的值
cat(.,'-', names(t1[1]))

以上表达式等于:

cat(sum(as.numeric(t1[[1]])),'-', names(t1[1]))

或者写成:

# 求三次x小测验的总分
as.numeric(t1[[1]]) %>%
sum() %>%
# 其中.就代表上一个参数传入的值
cat(names(t1[1]),'的总分:', .)

分别计算三个人的总分:

  1. 写成一个循环:
# 分别打印三个人的总分
for (i in 1:3){
as.numeric(t1[[i]]) %>%
sum() %>%
# 其中.就代表上一个参数传入的值
cat(names(t1[i]),'的总分:', ., "\n")
}

## James 的总分: 25
## Jack 的总分: 9
## Steven 的总分: 18

用strsplit()处理有4个字符串的字符型向量s, 结果是长度为4的列表:

tmpr <- strsplit(s, ',', fixed=T); tmpr
  1. 用map()和as.numeric()可以把列表中所有字符型转为数值型, 输出为一个列表, 然后再对各个列表元素中的向量求和。 使用管道运算符表达逐步的操作:
# 一句话的表达式
map_dbl(map(strsplit(s,split = ",", fixed = T),as.numeric),sum)

等同于:

s %>%
strsplit(split=",", fixed=TRUE) %>%
map(as.numeric) %>%
map_dbl(sum)

受上面程序写法的启发,还可以写成:

t2 <- map(strsplit(s, split = ',',fixed = T),as.numeric)
names(t2) <- c('李明','小黄','小吕','小爱')
for (i in 1:3){
# 其中sep的默认值为空格,可以指定为空,这样字符之间连接就不会出现空格。
cat(names(t2[i]), "的总分:",sum(t2[[i]]),"\n", sep="")
}