第 8 章 函数式编程

很多教材都是讲函数和循环,都是从for, while, ifelse讲起 ,如果我也这样讲,又回到了Base R的老路上去了。考虑到大家都没有编程背景,也不会立志当程序员,所以我直接讲purrr包,留坑以后填吧。

8.1 简单回顾

大家知道R常用的数据结构是向量、矩阵、列表和数据框,如下图

他们构造起来,很多相似性。

8.2 多说说列表

我们构造一个列表

## $num
## [1] 8 9
## 
## $log
## [1] TRUE
## 
## $cha
## [1] "a" "b" "c"

要想访问某个元素,可以这样

## $num
## [1] 8 9

注意返回结果,第一行是$num,说明返回的结果仍然是列表, 相比a_list来说,a_list["num"]是只包含一个元素的列表。

想将num元素里面的向量提取出来,就得用两个[[

## [1] 8 9

大家知道程序员都是偷懒的,为了节省体力,用一个美元符号$代替[[" "]]六个字符

## [1] 8 9

在tidyverse里,还可以用

## [1] 8 9

或者

## [1] 8 9

8.3 列表 vs 向量

假定一向量

## [1] -2 -1  0  1  2

我们对元素分别取绝对值

## [1] 2 1 0 1 2

如果是列表形式,abs函数应用到列表中就会报错

## Error in abs(lst): 数学函数中用了非数值参数

报错了。用在向量的函数用在list上,往往行不通。

再来一个例子:我们模拟了5个学生的10次考试的成绩

## $student1
##  [1] 75 75 66 75 61 97 83 63 62 66
## 
## $student2
##  [1]  88  80  85  72  90  78  67 100  86  77
## 
## $student3
##  [1] 64 99 51 73 87 95 90 58 75 74
## 
## $student4
##  [1] 82 75 54 76 55 81 78 89 77 63
## 
## $student5
##  [1] 71 85 88 80 96 78 52 98 55 98

很显然,exams是一个列表。那么,每个学生的平均成绩是多呢?

我们可能会想到用mean函数,但是

## Warning in mean.default(exams): argument is not numeric
## or logical: returning NA
## [1] NA

发现报错了,可以看看帮助文档看看问题出在什么地方

帮助文档告诉我们,mean()要求第一个参数是数值型或者逻辑型的向量。 而我们这里的exams是列表,因此无法运行。

那好,我们就用笨办法吧

## $student1
## [1] 72.3
## 
## $student2
## [1] 82.3
## 
## $student3
## [1] 76.6
## 
## $student4
## [1] 73
## 
## $student5
## [1] 80.1

成功了。但发现我们写了好多代码,如果有100个学生,那就得写更多的代码,如果是这样,程序员就不高兴了,这太累了啊。于是purrr包的map函数来解救我们,下面主角出场了。

8.4 purrr

介绍之前,先试试

## $student1
## [1] 72.3
## 
## $student2
## [1] 82.3
## 
## $student3
## [1] 76.6
## 
## $student4
## [1] 73
## 
## $student5
## [1] 80.1

哇,短短几句话,得出了相同的结果。如果希望返回的是数值型的向量,可以这样写

## student1 student2 student3 student4 student5 
##     72.3     82.3     76.6     73.0     80.1

如果希望返回的结果是数据框

## # A tibble: 1 x 5
##   student1 student2 student3 student4 student5
##      <dbl>    <dbl>    <dbl>    <dbl>    <dbl>
## 1     72.3     82.3     76.6       73     80.1

是不是很酷?

事实上,map函数

  • 第一个参数是列表(数据框是列表的一种特殊形式,因此数据框也是可以的)
  • 第二个参数是函数,这个函数会应用到列表的每一个元素

map函数执行过程如下 :

具体为,exams有5个元素,一个元素装着一个学生的10次考试成绩, 运行map(exams, mean)函数后, 首先取出exams第一个元素exams$student1(它是向量),然后执行 mean(exams$student1), 然后将计算结果存放在列表result中的第一个位置result1上;

做完第一个学生的,紧接着取出exams第二个元素exams$student2,执行 mean(exams$student2), 然后将计算结果存放在列表result中的第一个位置result2上;

如此这般,直到所有学生都处理完毕。我们得到了最终结果—一个新的列表result

当然,我们也可以根据需要,让map返回我们需要的数据格式, purrr也提供了方便的函数,具体如下

我们将mean函数换成求方差var函数试试,

## # A tibble: 1 x 5
##   student1 student2 student3 student4 student5
##      <dbl>    <dbl>    <dbl>    <dbl>    <dbl>
## 1     127.     90.9     257.     138.     276.

8.5 自定义函数

刚才我们是让学生成绩执行求平均mean,求方差var等函数。我们也可以自定义函数。 比如我们这里定义了将向量中心化的函数(先求出10次考试的平均值,然后每次考试成绩去减这个平均值)

## # A tibble: 10 x 5
##    student1 student2 student3 student4 student5
##       <dbl>    <dbl>    <dbl>    <dbl>    <dbl>
##  1     2.7      5.7    -12.6         9  -9.10  
##  2     2.7     -2.30    22.4         2   4.9   
##  3    -6.30     2.7    -25.6       -19   7.9   
##  4     2.7    -10.3     -3.60        3  -0.1000
##  5   -11.3      7.7     10.4       -18  15.9   
##  6    24.7     -4.30    18.4         8  -2.10  
##  7    10.7    -15.3     13.4         5 -28.1   
##  8    -9.30    17.7    -18.6        16  17.9   
##  9   -10.3      3.7     -1.60        4 -25.1   
## 10    -6.30    -5.30    -2.60      -10  17.9

当然可以偷懒将函数直接写在map()里,用~代替my_fun, 但代价是参数必须是规定的写法,比如.x

## # A tibble: 10 x 5
##    student1 student2 student3 student4 student5
##       <dbl>    <dbl>    <dbl>    <dbl>    <dbl>
##  1     2.7      5.7    -12.6         9  -9.10  
##  2     2.7     -2.30    22.4         2   4.9   
##  3    -6.30     2.7    -25.6       -19   7.9   
##  4     2.7    -10.3     -3.60        3  -0.1000
##  5   -11.3      7.7     10.4       -18  15.9   
##  6    24.7     -4.30    18.4         8  -2.10  
##  7    10.7    -15.3     13.4         5 -28.1   
##  8    -9.30    17.7    -18.6        16  17.9   
##  9   -10.3      3.7     -1.60        4 -25.1   
## 10    -6.30    -5.30    -2.60      -10  17.9

有时候,程序员觉得x还是有点多余,于是更够懒一点,只用., 也是可以的

## # A tibble: 10 x 5
##    student1 student2 student3 student4 student5
##       <dbl>    <dbl>    <dbl>    <dbl>    <dbl>
##  1     2.7      5.7    -12.6         9  -9.10  
##  2     2.7     -2.30    22.4         2   4.9   
##  3    -6.30     2.7    -25.6       -19   7.9   
##  4     2.7    -10.3     -3.60        3  -0.1000
##  5   -11.3      7.7     10.4       -18  15.9   
##  6    24.7     -4.30    18.4         8  -2.10  
##  7    10.7    -15.3     13.4         5 -28.1   
##  8    -9.30    17.7    -18.6        16  17.9   
##  9   -10.3      3.7     -1.60        4 -25.1   
## 10    -6.30    -5.30    -2.60      -10  17.9

8.6 延伸阅读

1、看手册?purrr::modify(), 思考下它与map()的区别

2、他们的区别哪里?函数能否互换?