2.8 函数

2.8.1 R中的函数

不像很多其他语言的函数(和方法)有value.func()func value等格式,R中所有函数的通用格式是这样的:

function(argument1 = value1, argument2 = value2, ...)

比如

sample <- c(5.1, 5.2, 4.5, 5.3, 4.3, 5.5, 5.7)
# 根据传统,赋值变量时用`<-`号,赋值函数参数时才用`=`
t.test(x = sample, mu = 4.5)
#> 
#>  One Sample t-test
#> 
#> data:  sample
#> t = 3, df = 6, p-value = 0.02
#> alternative hypothesis: true mean is not equal to 4.5
#> 95 percent confidence interval:
#>  4.61 5.56
#> sample estimates:
#> mean of x 
#>      5.09

二元运算符和[(取子集符号)看起来一点都不像函数,而实际上它们也是函数,因此也可以用通用的格式使用他们,只是需要加上引号或转义符号:

"+"(2, 3)
`+`(2, 3)
## 5
"["(c("四川担担面", "武汉热干面", "兰州牛肉面", "北京炸酱面"), 2)
#> [1] "武汉热干面"

可自定义的二元运算符形式为%x%, 其中x为任何字符。(见第2.8.3.3节)

英语中,“parameter”或“formal argument”二词用于函数定义,“argument”或“actual argument”二词用于调用函数(Kernighan and Ritchie 1988),中文里分别是“形式参数”和“实际参数”,但是多数场合简称“参数”。

2.8.2 调用函数

根据通用格式(function(argument1 = value1, argument2 = value2, ...))调用函数。对于二元运算符,a %x% b等价于"x"(a, b).

从“function(”开始到此函数结尾的“)”中间为(实际)参数,各参数用逗号隔开,空格和换行会被忽略,“#”符号出现之处,那一行之后的内容都会被忽略。这意味着你可以(丧心病狂地)像这样调用一个函数。

sum        (
 # 4   
        4 # 我怕不是
,              #疯了
            6
  
  )
#> [1] 10

它实际的好处是,当参数很长或是有嵌套的函数时,可以通过换行和空格使代码更易读,就像其它的编程语言一样。(后面会有很多例子)

函数的参数以seq函数为例,通过查看帮助文档(在console执行?seq),通常在Usage一栏,可以查看它的所有的(形式)参数及其排序:

## Default S3 method:
seq(from = 1, to = 1, by = ((to - from)/(length.out - 1)),
    length.out = NULL, along.with = NULL, ...)

可以看到第一个参数是from,第二个是to,第三个是by,以此类推。我们执行seq(0, 50, 10)的时候,R会理解成seq(from = 0, to = 50, by = 10)。而想用指定长度的方法就必须要写清楚是length.out等于几。

length.out本身也可以简写:

seq(0, 25, l = 11)
#>  [1]  0.0  2.5  5.0  7.5 10.0 12.5 15.0 17.5 20.0 22.5 25.0

因为参数中只有length.out是以l开头的,l会被理解为length.out. 但是这个习惯并不好;自己用用就算了,与别人分享自己的工作时请尽量使用参数名的全称。

对于seq(0, 50, 10),亦可写成seq(by = 10, 0, 50). 这是因为by参数先赋值,050是未命名的参数,所以按照剩余的参数的排列顺序来,即from = 0, to = 50. 同理,seq(to = 50, 0, 10)也是等价的。

2.8.3 创建函数

2.8.3.1 普通函数

函数名 <- function(参数1, 参数2, ...){
  对参数1和参数2
  进行
  一系列
  一行或者多行
  计算
  return(计算结果)
}

在R中,函数是作为对象保存的,因此定义函数不需要一套另外的符号/语句,还是用赋值符号<-,和function()函数。

R自带了计算样本标准差 (standard deviation, \(s\))的函数, sd(),我们可以根据它写一个计算均值标准差(即“标准误”, standard error)(\(SE=s_{\bar{x}}=\frac{s}{\sqrt{n}}\)

SE <- function(x) {
  s <- sd(x)
  n <- length(x)
  result <- s/sqrt(n)
  return(result)
}
# 随后,你就可以使用自定义的函数了
SE(c(5,6,5,5,4,5,6,6,5,4,5,3,8)) 
#> [1] 0.337

这里其实可以做一些省略。很多时候,最后一“句”的计算结果(不是赋值计算)就是我们想return的结果。因此,这时return可以省略:

SE <- function(x) {
  s <- sd(x)
  n <- length(x)
  s/sqrt(n) # 注意不是`result <- s/sqrt(n)`
}
SE(c(5,6,5,5,4,5,6,6,5,4,5,3,8)) 
#> [1] 0.337

很多时候,函数内部有复杂流程控制,这时使用return()可以很大地增强易读性:

# 这是随手写的一个没有意义的函数
myfunc <- function(i){
  k <- 8
  if (i>3) {
    j <- -i
    while(j < 20){
      k <- k + i + j
      j <- j+5
    }
    return(k)
  } else {
    if (i %% 2 == 0) {
      return(5)
    } else return(k*i)
  }
}
myfunc(6)
#> [1] 83

本章剩余的内容,都是比较进阶的了。可以酌情从这里跳转至本章第2.9节。

2.8.3.2 匿名函数

函数不需要名字也可以执行。一般,会与apply族函数联用(见第2.7.4节):

sapply(1:5, function(x) x^2)
#> [1]  1  4  9 16 25

或者用于

2.8.3.3 二元运算符

定义二元运算符的方式和定义普通函数的方法极其类似,只是参数必须要有且仅有两个(否则作为“二元”运算符就无意义了),且运算符名称需要用引号包围。

比如我们可以定义一个计算椭圆面积的函数

'%el%' <- function(x, y) pi*x*y

2 %el% 5
#> [1] 31.4

原则上,可自定义的二元运算符不一定要用%包围;+, -, :等符号的功能都可以被自定义,但是它们是R自带的,非常常用的函数,重定义它们只会带来麻烦。

2.8.3.4 闭包 (Closure)

函数里可以包含着另一个函数,这就形成了一个闭包:

myfunc <- function(){
  a = 5
  function(){
    b = 10
    return(a*b)
  }
}
# 执行myfunc()的时候,默认结果为最后一句/一行,在这里应为内函数:
myfunc()
#> function(){
#>     b = 10
#>     return(a*b)
#>   }
#> <environment: 0x7fae44fb9110>
# 既然`myfunc()`的结果是一个函数,那么在后面再加上一个括号就是执行内函数了;内函数可以使用外函数中所定义的变量(比如这里使用了外函数的`a = 5`)
myfunc()()
#> [1] 50
speak <- function(x){
  x()$speak
}

speak(cat)
#> NULL

利用闭包,可以使用R中的简易的函数实现伪·OOP22,这是本章末的挑战题

2.8.4 关于...

有时候,你想写的函数可能有数量不定的参数,或是有需要传递给另一个函数的“其他参数”(即本函数不需要的参数),这时候可以在函数定义时加入一个名为...的参数,然后用list()来读取它们。list是进阶内容,在第2.4节有说明。

比如我写一个很无聊的函数:

my_func <- function(arg1, arg2 = 100, ...){
  other_args <- list(...)
  print(arg1)
  print(arg2)
  print(other_args)
}

my_func("foo", cities = c("崇阳", "Αθήνα", "つがる"), nums = c(3,4,6))
#> [1] "foo"
#> [1] 100
#> $cities
#> [1] "崇阳"   "Αθήνα"  "つがる"
#> 
#> $nums
#> [1] 3 4 6

arg1指定了是"foo"(通过简写),因此第一行印出"foo"; arg2未指定,因此使用默认值100,印在第二行。citiesnums在形式参数中没有匹配,因此归为“…”,作为list印在第三行及之后。

下面是一个(没有意义的)利用...做一个对于向量和列表通用的函数calc(),使calc(data, pow = a, times = b, add = c)返回与原数据data的结构相同,但各元素\(x\)变为\(bx^a+c\)的向量/列表(这和OOP有相似之处):

calc_v <- function(v, pow = 1, times = 1, add = 0) {
  v ^ pow * times + add
}

calc_l <- function(L, pow = 1, times = 1, add = 0) {
  rapply(L, function(l) l ^ pow * times + add, how = "list")
}

calc <- function(data, ...) {
  if(is.list(data)) {
    calc_l(data, ...) # 即 calc_l(L = data, ...)
  } else if(is.vector(data)) {
    calc_v(data, ...) # 即 calc_v(v = data, ...)
  }
}
calc(c(1, 2, 3), pow = 2, add = 1)
#> [1]  2  5 10
calc(list(1, 2, list(10, 20)), pow = 2, times = 2)
#> [[1]]
#> [1] 2
#> 
#> [[2]]
#> [1] 8
#> 
#> [[3]]
#> [[3]][[1]]
#> [1] 200
#> 
#> [[3]][[2]]
#> [1] 800

pow, timesadd不是calc的参数,它们以...的形式被传递给calc_l()calc_v().

在第2.7.4.2节讲到,sapply()的功能本质上和lapply()一致,只是会化简结果。我们看一下sapply()函数的结构:

sapply
#> function (X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE) 
#> {
#>     FUN <- match.fun(FUN)
#>     answer <- lapply(X = X, FUN = FUN, ...)
#>     if (USE.NAMES && is.character(X) && is.null(names(answer))) 
#>         names(answer) <- X
#>     if (!isFALSE(simplify) && length(answer)) 
#>         simplify2array(answer, higher = (simplify == "array"))
#>     else answer
#> }
#> <bytecode: 0x7fae42df7d10>
#> <environment: namespace:base>

可以看到,answer <- lapply(X = X, FUN = FUN, ...)这一行把sapply()...中的参数传递到了lapply()中,使用lapply()得到未化解的结果answer, 随后仅需要写用来化简结果的代码,而不需要把与lapply()里的代码重写一遍。

2.8.5 赋值函数外的对象

函数内的赋值一般只在函数内有效,比如:

x <- 5
fun1 <- function() {
  x <- 100
}
fun1()
x
#> [1] 5

使用assign()函数可以在函数内赋值任意environment中的对象,其中最常见的是Global environment里的(即等价于在console中直接赋值)。

x <- 5
fun1 <- function() {
  assign("x", 100, envir = .GlobalEnv)
}
fun1()
x
#> [1] 100

<<-23可用于赋值“上一层”里的对象。当在“第一层”的函数里使用<<-时, .GlobalEnv里对应的对象就会受到影响,即和assign("x", value, envir = .GlobalEnv)等效。

x <- 5
fun1 <- function() {
  x <<- 100
}
fun1()
x
#> [1] 100

在下面的例子中,fun2()赋值了fun1()里的n, 但.GlobalEnv里的n不受影响。

n <- 1 # `GlobalEnv`里的`n` = 1

fun1 <- function() {
  n <- 10 # `fun1()`里的`n` = 10
  fun2 <- function() {
    n <- 50 # 赋值`fun2()`里的`n`
    n <<- 100 # 重赋值`fun1()`里的`n`为100
  }
  fun2() # 运行`fun2()`
  return(n) # 返回`fun1()`里的`n`
}

fun1() # 10是否变为100?
#> [1] 100
n # 是否仍然是1?
#> [1] 1

利用这个性质,我们可以使apply()族函数进行递归计算,比如求累加和:

cum = 0
sapply(1:10, FUN = function(x){
  cum <<- cum + x
  cum
})
#>  [1]  1  3  6 10 15 21 28 36 45 55

原则上,这已经不是一个向量化计算了,但是在这个例子中sapply()仍然比for循环(见下)速度更快。

cum = 1
  for (i in 2:10000) {
    cum[i] <- cum[i-1] + i
  }
cum

2.8.6 测速

当你开始处理复杂,大量的数据时,或是向别人分享自己的代码时,代码执行的速度变得重要。

一段代码/一个函数经常有很多种写法,哪种效率更高呢?实践是检验真理的唯一标准,R提供了一个测速函数:system.time()函数。

x <- vector('numeric')
system.time(
  for (i in 1:50){
    for (j in 1:100) {
      x <- append(x, i*j)
    }
  }
)
#>    user  system elapsed 
#>   0.046   0.022   0.068

其中第三个数字 (elapsed)是执行system.time()括号内的语句实际消耗的时间。可以使用索引 ([3])抓取。

如果括号内的语句大于一句,像这样:

system.time(
  1 + 1
  2 + 1
)

R会报错。就像流程控制里学到的那样,需要用大括号包围多行/多句的语句,就像这样:

system.time({
  1 + 1
  2 + 1
})

References

Kernighan, Brain W., and Ednnis M. Ritchie. 1988. The C Programming Language. 2nd ed. Prentice Hall.


  1. R中的真·OOP是有三种,S3,R6和S4。

  2. <<-符号的名字叫做“super assignment”(超级赋值)