2017-08-02 20:40:27

目录

函数式编程(Functional Programming)

简介

  • 函数式编程(functional programming),与指令性编程(imperative programming)相对,是以函数为核心的编程范式(paradigm)

  • 在FP范式中,函数是一等公民 (像变量一样,可以被创建,被作为参数,被作为返回结果)

  • FP思想起源于Princeton Univ的 Alonzo Church 提出的\(\lambda\)演算

  • FP最早应用在Lisp语言,如今Python、JavaScript、Haskell、Erlang、Clojure、R、Mathematica等均支持FP

  • FP是学术界为数值计算而设计出来的,尤其适合数值计算

  • R在本质上是一种FP语言 (回忆一下apply家族)

一个例子

一个调查问卷,采集了5个人对a-f六个问题的答案。部分答案为-9,表示漏填
要求: 将df数据集里的-9都改为NA

set.seed(1234)
df <- as.data.frame(matrix(sample(c(1:5, -9), 30, replace=TRUE), ncol=6))
names(df) <- letters[1:6]; print(df)
##    a b  c  d e  f
## 1  1 4  5 -9 2  5
## 2  4 1  4  2 2  4
## 3  4 2  2  2 1 -9
## 4  4 4 -9  2 1  5
## 5 -9 4  2  2 2  1

  • 传统指令性编程思路
df$a[df$a == -9] <- NA
df$b[df$b == -9] <- NA
df$c[df$c == -9] <- NA
df$d[df$d == -9] <- NA
df$e[df$e == -9] <- NA
df$f[df$f == -9] <- NA

问题来了

  • 如果有一万列怎么办?
  • 如果拷贝错了怎么办?
  • 来个循环?
for (col in letters[1:6]){
    df[df[, col] == -9, col] <- NA
}

for (col in letters[1:6]){
    df[, col][df[, col] == -9] <- NA
}
  • 代码表达太繁琐
  • 执行效率不高(显式循环)

  • 写个函数?
symb2na <- function(x) {
    x[x == -9] <- NA; x
}
df$a <- symb2na(df$a)
df$b <- symb2na(df$b)
df$c <- symb2na(df$c)
df$d <- symb2na(df$d)
df$e <- symb2na(df$e)
df$f <- symb2na(df$f)
  • 看起来还是那么繁琐
  • 正宗的FP
symb2na <- function(x) {
    x[x == -9] <- NA
    x
}
df[] <- lapply(df, symb2na)

或用匿名函数,连函数名都省了

df[] <- lapply(df, function(x) {
    x[x == -9] <- NA
    x
})

进一步扩展

  • 假如-9、-99都表示NA,如何扩展symb2na函数,使它适应性更广?
symb2na <- function(x, symb){
    x[x %in% symb] <- NA
    x
}
df[] <- lapply(df, symb2na, 
    symb=c(-9, -99))
  • 假如不光-9转NA,允许自定义转换方式(如-9、-99转9),怎样扩展?
replaceVal <- function(x, v1, v2){
    x[x %in% v1] <- v2
    x
}
df[] <- lapply(df, replaceVal, 
    v1=c(-9, -99), v2=9)

FP的特点

  • 不修改状态(status)、数据不可变(immutable data)
    • 调用环境内对象的状态和变量不会变化: 没有副作用(side effect)
    • 不需要维护状态、修改变量,减少了bug
  • 函数可以当对象用 (一等公民)
    • 可以用函数来产生函数 (工厂函数)
    • 可以把函数组成列表投入运算
  • 用表达式(expression),而不是语句(statement)
    • 关注求值,减少读写(I/O)
  • 引用透明(referential transparency)
    • 函数输出只依赖于输入参数: 结果确定性更高
    • R并不是纯函数式编程语言,不能做到完全的引用透明
  • 惰性求值(lazy evaluation)

FP的优缺点

优点

  • 代码简洁优雅
  • 便于模块化设计
    • 函数封装
    • 可复用性增强
  • 接近自然语言,更加易读
  • 利于并行开发和部署
  • 便于调试(debug)
  • 便于单元测试(unit test)

缺点

  • 相对难学
  • 纯函数式编程有时并不合理
    • 有时候就是需要I/O,副作用难以避免
    • R的函数无法避免外环境影响,做不到纯函数

R是多范式的,应当灵活综合OOP和FP的优点

FP的常用技术

常用技术包括

  • 匿名函数 (anonymous function)
    • map和reduce: apply家族
  • 函数闭包 (closure): 函数写出的函数
    • 高阶函数 (high order function): 以函数为形参
  • 函数列表 (lists of functions)
  • 管道化 (pipeline): %>%
  • 递归 (recursing): 简化代码,描述问题本身

匿名函数

匿名函数同样有头、体、环境

formals(function(x) mean(x)+c(1,-1)*sd(x))
## $x
body(function(x) mean(x)+c(1,-1)*sd(x))
## mean(x) + c(1, -1) * sd(x)
environment(function(x) 
    mean(x)+c(1, -1)*sd(x))
## <environment: R_GlobalEnv>

匿名函数最常用于apply家族函数

lapply(iris[,1:4], function(x) 
    mean(x)+c(1, -1)*sd(x))
## $Sepal.Length
## [1] 6.671399 5.015267
## 
## $Sepal.Width
## [1] 3.493200 2.621467
## 
## $Petal.Length
## [1] 5.523298 1.992702
## 
## $Petal.Width
## [1] 1.9615710 0.4370957

函数闭包

squarecube都是函数闭包

power <- function(exp){
    function(x) x ^ exp
}
square <- power(2)
cube <- power(3)

power用来生成函数闭包,叫工厂函数(function factory)

c(square(3), cube(3))
## [1]  9 27
c(power(2)(3), power(3)(3))
## [1]  9 27

squarecube函数定义是一样的

all.equal(square, cube)
## [1] TRUE

unenclose后则不同

library(pryr)
c(unenclose(square), unenclose(cube))
## [[1]]
## function (x) 
## x^2
## 
## [[2]]
## function (x) 
## x^3

FP不改变状态,但闭包可以

函数闭包会保留数据

new_counter <- function() {
  i <- 0
  function() {
    i <<- i + 1
    i
  }
}
counter1 <- new_counter()
counter2 <- new_counter()
  • 通过函数闭包修改父环境数据,可以实现状态改变
  • 复杂的情形,建议通过设置refClass来实现

测一下

counter1()
## [1] 1
counter1()
## [1] 2
counter1()
## [1] 3
counter2()
## [1] 1

高阶函数 - Reduce & Map

  • 有4个列表,各包含mtcars的一列参数和型号名
library(magrittr)
a <- lapply(1:4, function(i) data.frame(
    I(rownames(mtcars)[1:5]), mtcars[1:5,i]) %>% 
        `names<-`(c("Car", names(mtcars)[i])))
str(a, vec.len=1)
## List of 4
##  $ :'data.frame':    5 obs. of  2 variables:
##   ..$ Car:Class 'AsIs'  chr [1:5] "Mazda RX4" ...
##   ..$ mpg: num [1:5] 21 21 ...
##  $ :'data.frame':    5 obs. of  2 variables:
##   ..$ Car:Class 'AsIs'  chr [1:5] "Mazda RX4" ...
##   ..$ cyl: num [1:5] 6 6 ...
##  $ :'data.frame':    5 obs. of  2 variables:
##   ..$ Car :Class 'AsIs'  chr [1:5] "Mazda RX4" ...
##   ..$ disp: num [1:5] 160 160 ...
##  $ :'data.frame':    5 obs. of  2 variables:
##   ..$ Car:Class 'AsIs'  chr [1:5] "Mazda RX4" ...
##   ..$ hp : num [1:5] 110 110 ...
  • 进行merge合并,普通用法需要三次merge
merge(merge(merge(a[[1]], a[[2]]), 
    a[[3]]), a[[4]])
  • 直接用Reduce
Reduce(merge, a)
                Car  mpg cyl disp  hp
1        Datsun 710 22.8   4  108  93
2    Hornet 4 Drive 21.4   6  258 110
3 Hornet Sportabout 18.7   8  360 175
4         Mazda RX4 21.0   6  160 110
5     Mazda RX4 Wag 21.0   6  160 110
  • Map 就等于 mapply

高阶函数 - Filter & Find/Position

  • a的四个列表中,第二列最小值超过100的是
Filter(function(df) min(df[,2]>100), a)
## [[1]]
##                 Car disp
## 1         Mazda RX4  160
## 2     Mazda RX4 Wag  160
## 3        Datsun 710  108
## 4    Hornet 4 Drive  258
## 5 Hornet Sportabout  360
  • Find返回第一个满足条件的元素
  • Position返回第一个满足条件的元素的索引号
  • a的四个列表中,第二列最大值不超过10的列表是
Filter(Negate(function(df) max(df[,2]>10)), 
       a)
## [[1]]
##                 Car cyl
## 1         Mazda RX4   6
## 2     Mazda RX4 Wag   6
## 3        Datsun 710   4
## 4    Hornet 4 Drive   6
## 5 Hornet Sportabout   8

自定义高阶函数

set.seed(12345)
f <- function(fun, ...){
    stopifnot(is.function(fun))
    fun(...)
}
f(mean, 1:10)
## [1] 5.5
matrix(f(rnorm, 6, 0, 1), nrow=2)
##           [,1]       [,2]       [,3]
## [1,] 0.5855288 -0.1093033  0.6058875
## [2,] 0.7094660 -0.4534972 -1.8179560
set.seed(12345)
hiOrdFun <- function(fun, ...){
    stopifnot(is.function(fun))
    function(...) fun(...)
}
hiOrdFun(mean)(1:10)
## [1] 5.5
matrix(hiOrdFun(rnorm)(6, 0, 1), nrow=2)
##           [,1]       [,2]       [,3]
## [1,] 0.5855288 -0.1093033  0.6058875
## [2,] 0.7094660 -0.4534972 -1.8179560

函数列表 List of functions

将函数组成列表,结合高阶函数,可以套用apply家族函数

求一组数据的mean、sd、fivenum:

x <- c(sample(1:100, 10), NA)
list(mean(x, na.rm=TRUE), 
     sd(x, na.rm=TRUE), fivenum(x))
## [[1]]
## [1] 47.3
## 
## [[2]]
## [1] 30.86188
## 
## [[3]]
## [1]  1.0 30.0 40.5 74.0 98.0

函数列表解决方案

f.lst <- list(mean, sd, fivenum)
lapply(f.lst, f, x, na.rm=TRUE)
## [[1]]
## [1] 47.3
## 
## [[2]]
## [1] 30.86188
## 
## [[3]]
## [1]  1.0 30.0 40.5 74.0 98.0

进一步应用

工厂函数 + 函数列表 + 高阶函数 + 匿名函数

lst.fun <- lapply(f.lst, hiOrdFun)
lapply(lst.fun, function(f) f(x, na.rm=TRUE))
## [[1]]
## [1] 47.3
## 
## [[2]]
## [1] 30.86188
## 
## [[3]]
## [1]  1.0 30.0 40.5 74.0 98.0

函数列表全局化

power.fun <- function(exp, ...)
    function(...) list(...)[[1]] ^ exp
f.lst <- c(square=2, cube=3, powfour=4)
pow <- lapply(f.lst, power.fun)
c(pow$square(4), pow$cube(4), 
    pow$powfour(4))

sapply(pow, function(f) f(4))
##  square    cube powfour 
##      16      64     256

list2env将函数列表pow提取到环境中

list2env(pow, environment())
## <environment: R_GlobalEnv>
c(square(3), cube(3), powfour(3))
## [1]  9 27 81

即实现了函数的批量生成

递归: 一个函数调用其自身

Reduce的例子中,合并四个列表也可以用递归法

f <- function(dfs, ...) {
    if (length(dfs) == 2) merge(dfs[[1]], dfs[[2]], ...)
    else  merge(dfs[[1]], f(dfs[-1]), ...)}
print(f(a))
##                 Car  mpg cyl disp  hp
## 1        Datsun 710 22.8   4  108  93
## 2    Hornet 4 Drive 21.4   6  258 110
## 3 Hornet Sportabout 18.7   8  360 175
## 4         Mazda RX4 21.0   6  160 110
## 5     Mazda RX4 Wag 21.0   6  160 110
  • 递归代码中,可以用Recall代替函数名f
  • R不支持尾递归(tail recursion),故递归不能过深