函数式编程,纯碎的函数。

近期,在使用 R 做数据分析的时候,总是会遇到单独对列或者对行进行处理、筛选,不可避免地用到 apply 这类函数。记得以前在 Javascript 中有个库叫做 “underscore”,支持纯粹的函数式编程。仔细想想,R 中这类 apply 函数,其实也就是函数式编程。那干脆再看看相关资料,看看具体是如何的。

函数式编程

一类函数 first-class function

讲到函数式编程,应该要了解它最基本的特征,那就是一个函数可以以一个函数作为参数进行运算,然后返回值或者函数。这就是所谓的 “first-class function”,它在支持函数式编程的语言中是被当作一等公民的,所谓 “first-class citizens”,它与内置的基本数据类型是一致的,可以进行赋值之类的。关于 “first-class function”,可以参考 wiki - first-class function。那么所谓的函数式编程的基本形式如何表现的?我们可以看下面这个例子。

我要在数据中在行这一维度上按照某个条件清洗数据,清洗条件是值是 6 的倍数改成 99 吧。在代码中就是这样,(虽然有更简便的方法,但是为了效果就这样),我们对每一行分别进行取值赋值,需要重复 10 次。

1
2
3
4
df <- matrix(1:100, 10)
df[1,][df[1,] %% 6 == 0] <- 99
df[2,][df[2,] %% 6 == 0] <- 99
# ...

改造成函数式的话,那么就是要先创建一个函数进行更改数据,然后对每一行应用它。

1
2
3
4
5
fix_6_with_99 <- function(x) {
x[x %% 6 == 0] <- 99
x
}
df <- apply(df, 1, fix_6_with_99)

这就是所谓的函数式编程。基于此,函数式编程又有了另外两个特征:

  • 匿名函数,anonymous function
  • 闭包,closures

匿名函数 anonymous function

匿名函数,顾名思义就是这个函数它没有名字。就一般来讲,函数它本身就是对象,一个对象怎么会没有名字呢。但一类函数由于可以把函数作为参数来传递到函数内部,被传递的这个函数是否有名字就不太重要了,因为它在函数内部是以对应的参数名的形式存在。例如 apply 我们可以自己这样构造,上述的 “fix_6_with_99” 就是作为下面的 “func” 传进去了。

1
2
3
4
5
6
7
8
9
10
11
12
apply <- function(dataframe, dim, func) {
df <- Vector("dataframe", nrow(dataframe))
if (dim == 1) {
for (i in 1:nrow(dataframe)) {
df[i,] <- func(dataframe[i,])
}
} else {
for (i in 1:ncol(dataframe)) {
df[,i] <- func(dataframe[,i])
}
}
}

所以我们可以如下改造上面的哪个调用:

1
2
3
4
df <- apply(df, 1, function(x) {
x[x %% 6 == 0] <- 99
x
})

但是匿名函数只能用一次,你只能在你定义并调用的时候使用一次。对于常用的函数,我们是不推荐把它作为匿名函数使用的,因为我们要 “do not repeat yourself” 的复用嘛。

闭包 closures

在说闭包的时候,有句话说的好,我认为可以加深认识:

“An object is data with functions. A closure is a function with data.” — John D. Cook

闭包就是一个函数在内部有了数据,这和我们平常的函数内部定义的数据不同,需要结合函数式编程的另一大特征来理解:返回函数。一般来说,函数应该要返回的是值。在函数式编程中,一个函数可以返回值,也可以返回函数,但兜兜转转,函数返回的函数返回的函数…返回的还是值。由于函数正常情况下,执行完毕后,解析器就会收回这个函数对应的内存空间,函数内部的变量也随之清除。但是,如果“在另外一个地方有引用这些待清除的变量”时,因为解析器无法得知这些变量何时不再使用或者说使用完毕,这些变量就暂时无法被清除,这个情况就是闭包。由于函数 parent 可以返回函数 child,如果 child 中有引用到 parent 的某个变量 variant,就实现在“另外一个地方引用这些变量”,保证了 variant 不能被清除,出现了闭包。简单地闭包例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
greet_to_name <- function(name) {
count <- 0
function(greet) {
cat(paste(greet, name, count, "\n", sep=", "))
count <- count + 1
}
}
greet <- greet_to_name("xizhihui")
greet("hello")
greet("hello")
#> hello, xizhihui, 0,
#> hello, xizhihui, 0,

我们在 “greet” 函数的返回值里面看到了 “greet_to_name” 里面的 name 和 count 变量的值,这就是一个简单地闭包。但是,虽然我们在函数里面添加了 count <- count + 1 这个语句,似乎对于 greet 调用多次并有没影响。这又涉及到另外一个问题:作用域。

作用域 scope

每个变量或函数都有它的有效作用范围,当出现变量名冲突时,根据作用域由近及远来获取对应的变量。在上面的例子中,由于内部的 function(greet) 没有定义 count 变量,在当前的函数作用域没有,那就往远的地方(外层作用域)寻找,于是就找到了 greet_to_name 的 count 变量,然后就引用它,这也就说明了形成闭包的引用是怎么来的。但是,这里有个但是哈,R 中的作用域还有另外一个特征,那就是如果在内层对外层变量进行赋值操作,是不会成功的,它只是在内层生成了一个与外层被引用变量相同名字的新变量,此时,比如 count 在 count <- count + 1 之后,就不是原来的 count (外层的)了。要避免这个情况,需要使用 <<- 超赋值语句,它只会对外层变量进行赋值。换句话说,即便内层有 count 变量,count <<- count + 1 还是会赋值给外层。

  • 内层有 count 变量,普通赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 内层有 count 变量
greet_to_name <- function(name) {
count <- 0
function(greet) {
cat(paste(greet, name, count, "\n", sep=", "))
count <- 100
count <- count + 1
}
}
greet <- greet_to_name("xizhihui")
greet("hello")
greet("hello")
#> hello, xizhihui, 0,
#> hello, xizhihui, 0,
  • 内层有 count 变量,进行超赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 内层有 count 变量
greet_to_name <- function(name) {
count <- 0
function(greet) {
cat(paste(greet, name, count, "\n", sep=", "))
count <- 100
count <<- count + 1
}
}
greet <- greet_to_name("xizhihui")
greet("hello")
greet("hello")
#> hello, xizhihui, 0,
#> hello, xizhihui, 101,

在使用超赋值后,第二次 greet(“hello”) 输出的是 “hello, xizhihui, 101,”。

那些支持函数式编程的常用函数

由于 first-class function 可以输出函数,这类输出的函数叫做 higher-order function。

apply 族

  • lapply: any collection -> Func -> list
  • sapply: any collection -> Func -> matrix/vector
  • apply: matrix/dataframe + margin -> Func -> matrix/vector
  • tapply: vector + factor_list -> Func -> list/vector (like aggregte)
  • mapply: 类似 sapply(df, function(x) sapply(x, Func)), Func + params -> list

common higher-order function

  • Reduce(fun, x, init, right=False, accumulate=False): 归并函数, init 设定起始值
  • Filter(fun, x): 按照 fun 返回的逻辑值对 x 进行过滤
  • Find(fun, x, right=False, nomatch=False):找到首个使 fun 为真的元素
  • Position(fun, x, right=False, nomatch=NA_integer_):找到所有使 fun 为真的元素的 index
  • Map(fun, …):执行 func(…)
  • Negate(fun): 生成 fun 的反函数,比如 Negate(Find) 就是等于 Find(!fun)

参考

Comments