Likan Zhan

R 语言中的函数

战立侃 · 2018-07-09

1. 函数的组成部分

R语言中,所有函数都包含三个成分:

f <- function(x) x^2
f
body(f)
formals(f)
environment(f)
## function(x) x^2
## x^2
## $x
## 
## 
## <environment: R_GlobalEnv>

我们也可以通过给 body()formals()environment() 附值的方法改变这个函数。

函数与R语言中所有其他对象一样可以有无数多个特征 attributes()。R语言基础包使用的一个特征是 srcrefsource reference 的简写),用于指向创造该函数的源代码。该特征与body()不同,因为它包含了代码的注视和其他格式。你也可以给一个函数添加特征。例如,你可以通过设定class()的方式给某个函数增加输出方法 print()

R语言中有些函数是通过接口.Primitive()直接调用的C语言代码,而不是用R语言代码编写的。这些函数被称元函数(Primitive functions)。这些函数的上述三个成分是空的 NULL。例如

sum
body(sum)
formals(sum)
environment(sum)
## function (..., na.rm = FALSE)  .Primitive("sum")
## NULL
## NULL
## NULL

元函数仅在R基础包base中出现。因为元函数用底层语言写成,所以他们通常计算效率更高。但是也因为他们用C语言而不是用R语言写成。他们的行为方式也可能与R语言的其他函数不一样。

2. 词法作用域

词法作用域(lexical scoping)在给符号赋值时,是基于函数在被定义时的嵌套状态,而不是基于函数在使用时的嵌套状态。词汇作用域的应用有四个基本原则:

x <- 1
h <- function() {
  y <- 2
  i <- function() {
    z <- 3
    c(x, y, z)
  }
  i()
}
h()
rm(x, h)
## [1] 1 2 3

又例如

j <- function(x) {
  y <- 2
  function() {
    c(x, y)
  }
}
k <- j(1)
k()
rm(j ,k)
## [1] 1 2
l <- function(x) x + 1
m <- function() {
  l <- function(x) x * 2
  l(10)
}
m()
rm(l, m)
## [1] 20
j <- function() {
  if (!exists("a")) {
    a <- 1
  } else {
    a <- a + 1
  }
  print(a)
}
j()
rm(j)
## [1] 1
f <- function() x
x <- 15
f()

x <- 20
f()
## [1] 15
## [1] 20

我们也可以对函数进行重新定义。例如:

`(` <- function(e1) {
  if (is.numeric(e1) && runif(1) < 0.2) {
    e1 + 1 
  } else {
    e1
  }
}
replicate (30, (1 + 2))
rm("(")
##  [1] 4 4 3 4 3 3 3 3 4 4 3 3 4 3 3 3 3 3 4 3 3 4 3 3 4 3 3 4 3 3

3. 函数调用

例如,R语言中 x + y 等价于 `+` (x, y) , 因为 + 是一个函数调用:

x <- 10; y <- 5
x + y
`+`(x, y)
## [1] 15
## [1] 15

下面是几个类似的例子:

for (i in 1:2) print(i)
`for`(i, 1:2, print(i))

if (i == 1) print("yes!") else print("no.")
`if`(i == 1, print("yes!"), print("no.") )

x[3]
`[`(x, 3)

{ print(1); print(2); print(3) }
`{`(print(1), print(2), print(3))
## [1] 1
## [1] 2
## [1] 1
## [1] 2
## [1] "no."
## [1] "no."
## [1] NA
## [1] NA
## [1] 1
## [1] 2
## [1] 3
## [1] 1
## [1] 2
## [1] 3

我们可以像使用普通函数一样使用上述特殊函数。例如加入我们想用 sapply() 把列表中的每个值都加 3,那么我们可以有下面三种方式来实现:

add <- function(x, y) x + y
sapply(1: 10, add, 3)

sapply(1:10, `+`, 3)

sapply(1:10, "+", 3)
##  [1]  4  5  6  7  8  9 10 11 12 13
##  [1]  4  5  6  7  8  9 10 11 12 13
##  [1]  4  5  6  7  8  9 10 11 12 13

下面是另外一个例子:

x <- list(1:3, 4:9, 10:12)
sapply(x, `[`, 2)
sapply(x, function(x) x[2])
## [1]  2  5 11
## [1]  2  5 11

4. 函数的参数

首先需要区分两个参数:形式参数(formal arguments / parameter)指在函数内期望被提供值的名字;而实际参数(actual argument / parameter)或调用参数(calling argument)则指在调用函数时提供给每个形式参数的值。

R语言中实际参数匹配到形式参数的方式有三种:位置、完整论元名称、部分论元名称。参数匹配的优先级为:完整论元名称 > 部分论元名称 > 位置。下面是几个例子:

f <- function(abcdef, bcde1, bcde2) {
  df <- data.frame(a = abcdef, b1 = bcde1, b2 = bcde2)
  print(df, row.names = "")
}

f(1, 2, 3)
f(2, 3, abcdef = 1)
f(2, 3, a = 1)
# f(1, 3, b = 1)
##  a b1 b2
##  1  2  3
##  a b1 b2
##  1  2  3
##  a b1 b2
##  1  2  3

完整论元名称的匹配方式最不容易出问题。位置匹配仅用于匹配函数的前两个论元,否则容易引起混乱。部分论元名称的匹配应当仅在不引起歧义的情况下使用。使用名称匹配的参数应该始终放在不使用名称匹配的参数之后。当函数需要匹配的参数位于省略的范围内...时,只能用完成论元名称的方式匹配。以平均值函数 mean() 为例:

args(mean.default)
## function (x, trim = 0, na.rm = FALSE, ...) 
## NULL

下面是几个较好的例子

mean(1:10)
mean(1:10, trim = 0.05)
## [1] 5.5
## [1] 5.5

下面这个例子没有歧义,但有点过于复杂了:

mean(x = 1:10)
## [1] 5.5

下面这些例子是有歧义(难以理解的)的:

x <- c(1:10, NA)
mean(x)
mean(x, , TRUE)      # mean(x, na.rm = TRUE)
mean(x, n = TRUE)  # mean(x, na.rm = TRUE)
mean(, TRUE, x = x) # mean(x, na.rm = TRUE)
## [1] NA
## [1] 5.5
## [1] 5.5
## [1] 5.5

函数 do.call() 可被用来同时匹配多个参数:

args(do.call)
args <- list(x, na.rm = TRUE)
do.call(mean, args) # equivalent to `mean(x, na.rm = TRUE)`
## function (what, args, quote = FALSE, envir = parent.frame()) 
## NULL
## [1] 5.5

我们可以给函数的参数设定默认值,这些默认值可以来自于函数的其他参数,甚至可以来自于函数计算出来的变量(容易引起歧义),例如:

f <- function(a = 1, b = 2) {
  c(a, b)
}
f()

g <- function(a = 1, b = a * 2) {
  c(a, b)
}
g()
g(10)

h <- function(a = 1, b = d) {
  d <- (a + 1) ^ 2
  c(a, b)
}
h()
h(10)
## [1] 1 2
## [1] 1 2
## [1] 10 20
## [1] 1 4
## [1]  10 121

函数的缺失值是在函数内进行求值的。这也就是说,当一个表达式依赖于当前表达环境时,使用默认值还是直接指派值可能会使函数的运行结果不一样。例如

f <- function(x = ls()) {
  a <- 1
  x
}
f()
f(ls())
## [1] "a" "x"
## [1] "add"  "args" "f"    "g"    "h"    "i"    "x"    "y"
i <- function(a, b) {
  c(missing(a), missing(b))
}
i()
i(a = 1)
## [1] TRUE TRUE
## [1] FALSE  TRUE

惰性求值 (Lazy evaluation)指参数只有在真的被使用的时候才会被匹配。但是函数force()可以用来强迫对函数进行求值。比较下面的这个例子:

f <- function(x) {
  10
}
f(message("This is an error!"))
## [1] 10

和这个例子

f <- function(x) {
  force(x)
  10
}
f(message("This is an error!"))
## This is an error!
## [1] 10

需要注意的是从R 3.2.0版开始,

Higher order functions such as the apply functions and Reduce() now force arguments to the functions they apply in order to eliminate undesirable interactions between lazy evaluation and variable capture in closures. — NEWS

这使得R的表现不再像Hadley书中描述的一样了。例如:

add1 <- function(x) {
  function(y) x + y
}

adders1 <- lapply(1:10, add1)
adders1[[1]](10)
adders1[[10]](10)
## [1] 11
## [1] 20

从技术层次上来讲,一个没有被求值的参数叫做一个允诺(promise)。一个允诺由两部分组成:

- 被延迟计算的表达式本身,可用函数 `substitute` 通达。
- 延迟表达式产生和求值的运行环境。

三个点 ... 是一个特殊的参数。该参数将匹配所有还没有匹配过的参数。这些参数还很容易传递到别的函数中。为了使用方便,我们可以用 list(...) 来获得三连点包含的参数而不对这些参数进行求值。例如

f <- function(...) {
  names(list(...))
}
f(a = 1, b = 2)
## [1] "a" "b"

5. 特殊调用

R语言在调用中缀(infix function)和替换(replacement function)这两类特殊函数时,还支持一些额外的句法结构。

R语言中大多数函数为前缀算子(prefix operator),即函数名称在参数之前出现。但是R语言中也有一些中缀函数,即函数名称在参数之间出现。R语言内置的中缀函数包括:

::, :::, $, @, ^,  /,  +, -, >, =, >=, <, <=, ==, !=, !, &, &&, |, ||, ~, <-, <<
%%, %*%, %/%, %in%, %o%, %x%

使用者也可以自己定义中缀函数。自定义中缀函数必须位于两个百分符号%%之间。因为这是一种特殊名称,所以在自定义中缀函数时,函数名称必须放在反引号内。例如

`%pp%` <- function(a, b) paste(a, b, sep = " ")
"new" %pp% "string"
`%pp%`("new", "string")
## [1] "new string"
## [1] "new string"

实际上,中缀函数要比常见函数更具灵活性,因为两个百分数符号之间可以是任意字符串。当然当函数定义中包含特殊字符时,需要对该字符进行换码(escape),而在使用该函数时则不需要换码。

`% %` <- function(a, b) paste(a, b)
`%'%` <- function(a, b) paste(a, b)
`%/\\%` <- function(a, b) paste(a, b)
"a" % % "b"
"a" %'% "b"
"a" %/\% "b"
## [1] "a b"
## [1] "a b"
## [1] "a b"

根据R语言的优先级规则(precedence rule)当代码中有多个中缀算子出现时,将按从左向右的顺序运算,例如

`%-%` <- function(a, b) paste0("(", a, " %-% ", b, ")")
"a" %-% "b" %-% "c"
## [1] "((a %-% b) %-% c)"

替换函数(replacement function)的名称有如下特定格式XXX<-。替换函数的参数可以有很多个,但通常有两个参数:xvalue。替换函数的返回值一定是修改过后的对象。例如下面的替换函数就是把数组中的第二个元素替换为某个特定值:

`second<-` <- function(x, value) {
  x[2] <- value
  x
}
x <- 1:10
second(x) <- 5L
x
##  [1]  1  5  3  4  5  6  7  8  9 10

当R对上述赋值表达式进行求值(evaluate)时,R会首先搜索环境中是否存在一个简单名称second,如果不存在,R会再进一步搜寻是否存在一个替换函数second<-

替换函数看起来是把原有对象修改了,实际上它修改的只是一个副本,原有对象实际上依然存在。用 pryr 包中的 address() 函数我们可以发现这两个对象实际上是存在不同的位置的。比较下面的例子:

x1 <- 1:10
pryr::address(x1)
second(x1) <- 6L
pryr::address(x1)
## [1] "0x7fc12321f5f8"
## [1] "0x7fc11ef93838"

注:Hadley在书中(P.92)说而R语言中使用接口 .Primitive() 的内置函数是对原有对象本身进行修改的。但是我的运行结果显示,内置函数替换后的对象也处于不同位置中:

x2 <- 1:10
pryr::address(x2)
x2[2] <- 7L
pryr::address(x2)
## [1] "0x7fc123c1d128"
## [1] "0x7fc123c9c238"

当替换函数中包含有三个或者更多参数时,其余参数应该位于 xvalue 之间,例如

`modify<-` <- function(x, position, value) {
  x[position] <- value
  x
}
modify(x, 1) <- 10
x
##  [1] 10  5  3  4  5  6  7  8  9 10

当我们调用函数 modify(x, 1) <- 10 时,R 在背后进行的运算实际上是

(x <- 1:10)
(x <- `modify<-`(x, 1, 10))
##  [1]  1  2  3  4  5  6  7  8  9 10
##  [1] 10  2  3  4  5  6  7  8  9 10

所以下面的调用是不合法的:

modify(get("x"), 1) <- 10
get("x") <- `modify<-`(get("x"), 1, 10)

有时候把替换和求子集(subsetting)合到一起用会很有用,例如

x <- c(a = 1, b = 2, c = 2)
names(x)
names(x)[2] <- "two"
names(x)
## [1] "a" "b" "c"
## [1] "a"   "two" "c"

6. 返回值

R语言中函数只能返回一个对象。但这不是一个问题,因为这个返回值可以是一个由很多对象组成的一个列表。一个理想的函数应该是一个纯净函数(pure function),即同样的输入总是产生相同的输出,而且不对工作去产生其他影响。

函数也可以返回不可见的值 invisible,也就是当你调用这个函数的时候,函数值不会被显示出来。例如

f1 <- function() 1
f2 <- function() invisible(1)

f1()
f2()
f1() == 1
f2() == 1
## [1] 1
## [1] TRUE
## [1] TRUE

当然你可以把该函数放在小括号中,强迫他们现实出来,例如

(f2())
## [1] 1

常见的赋值符号 <- 也是隐藏返回结果的。例如

a <- 2
(a <- 2)
## [1] 2

这也就是为什么我们可以把同一个数赋值给很多个名称的原因:

a <- b <- c <- d <- 2

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

in_dir <- function(dir, code) {
  old <- setwd(dir)
  on.exit(setwd(old))
  force(code)
}
getwd()
in_dir("~", getwd())
## [1] "/Users/lzhan/Documents/ADMIN/Website/likan/content/cn/read/Advanced-R"
## [1] "/Users/lzhan"

注意:如果你在一个函数中多次使用on.exit()函数,一定要进行如下参数的设定 add = TRUE