R语言高级程序设计--函数

函数

本篇文章主要讲几个问题:

  1. 函数的三个组成部门
  2. 如何通过名字找到值
  3. R中的一切事情都是函数调用的结果
  4. 传递参数的三种方式
  5. 特殊函数 : 中缀函数,替换函数
  6. 函数的返回值,如何返回

install.packages('pryr')这个函数用于研究修改的向量何时发生了变化。

函数的组成部分

  • 函数题体
  • 形式参数列表
  • 环境

当你打印函数的时候,会出现这三个部分。如果环境没有显示出来,那么这个函数是在全局环境中创建的。像 R 语言中的所有对象一样,函数还可以拥有任意数量的附加属性。 被基础 R 语 言使用的一个属性,为"srcref",它是源引用(source reference)的简称,它指向 用于创建函数的源代码。 与函数体不同,它包含代码注释和其它格式。 你还可以 给一个函数添加属性。 例如,你可以设置类 class()和添加一个自定义的 print()方法。

原语函数

函数都有三个组成部分的规则有一个例外。 原语函数(Primitive functions),比如 sum(),它直接使用了.Primitive()调用 C 语言代码,并且不包含 R 语言代码。 因 此,它们的 formals()、body()和 environment()都是 NULL:

原语函数只存在于 base 包中,由于它们在底层运作,所以它们可以更加高效(原语 替换函数不需要进行复制),可以对参数匹配使用不同的规则(如 switch 和 call)。 然而,使用它们的成本是,它们与 R 语言所有的其它函数的行为都不同。 因此, R 语言核心团队通常尽量避免创建它们,除非没有其它选择。

词法的作用域

  1. 通过组合函数来构建工具,如第 10 章所述。
  2. 不按照通常的计算规则进行计算,而是进行非标准计算,如第 13 章所述。R 语言有两种类型的作用域:词法作用域(Lexical scoping),在语言层面自动实 现,以及动态作用域(Dynamic scoping),用于在交互式分析情景下,在选择函数 时,可以减少键盘输入。

在 R 语言中,词法作用域的实现背后,有四个基本原则: 1. 名字屏蔽

  1. 函数和变量
  2. 全新的开始状态
  3. 动态查找
f <- function(){x <- 1;y <- 2;c(x,y)}
f()
[1] 1 2

如果一个名字在函数中没有定义,那么 R 语言将向上一个层次查找。

首先,查看当前函数 的内部,然后是这个函数被定义的环境,然后继续向上,以此类推,一直到全局 环境,然后,再查找其它已经加载的包。

避免函数与变量重名。

exists()函数的作 用:如果以某个名字命名的变量存在,那么它返回 TRUE;否则,它返回 FALSE)

动态查找

词法作用域决定了去哪里查找值,而不是决定在什么时候查找值。 R 语言在函数 运行时查找值,而不是在函数创建时查找值。 这意味着,函数的输出是可以随着 它所处的环境外面的对象

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

你通常应该避免这种行为,因为这意味着函数不再是独立的。 这是一种常见的错 误——如果你的代码中存在拼写错误,那么当你创建一个函数时,你将得不到错误 信息,甚至于你可能在运行该函数的时候,也不会得到错误信息这取决于全局 环境中的变量定义。 发现这个问题的一种方法是使用 codetools 包中的 findGlobals()函数。 它会列出一个函数的所有外部依赖项:

f <- function() x + 1
codetools::findGlobals(f) #> [1] "+" "x"

尝试解决这个问题的另一种方法是把函数的环境手动(manually)更改成 emptyenv(),它是完全不包含任何对象的空环境:

environment(f) <- emptyenv()
f()
#> Error: could not find function "+"

这样是不能工作的,因为 R 语言搜索一切东西都是依靠词法作用域,甚至+运算 符。 无法让一个函数完全独立,因为你必须依赖于 R 语言的 base 包或者其它包中 定义的函数。
你可以使用同样的思路做一些其它极其不明智的事情。 例如,由于 R 语言中所有 的标准运算符都是函数,所以你可以使用自己实现的方法覆盖它们。 如果你是个 特别淘气的家伙,那么请在你的朋友离开电脑时,偷偷在他们的电脑上运行以下代 码:

  这样是不能工作的,因为 R 语言搜索一切东西都是依靠词法作用域,甚至+运算 符。 无法让一个函数完全独立,因为你必须依赖于 R 语言的 base 包或者其它包中 定义的函数。
你可以使用同样的思路做一些其它极其不明智的事情。 例如,由于 R 语言中所有 的标准运算符都是函数,所以你可以使用自己实现的方法覆盖它们。 如果你是个 特别淘气的家伙,那么请在你的朋友离开电脑时,偷偷在他们的电脑上运行以下代 码:
`(` <- function(e1) {
if (is.numeric(e1) && runif(1) < 0.1) { e1 + 1
} else {
e1
}
}
replicate(50, (1 + 2))
#> [1] 3 3 3 3 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 3 3 3 4 3 #> [30] 3 3 3 3 3 4 3 3 4 3 3 3 3 3 3 3 3 3 3 3 3
  
rm("(")

这将引入一个特别有害的错误:每 10%的次数中,任何括号内的数值计算结果都 会被加上 1。 这也是另一个在重启时要用干净的 R 会话的理由!

所有的操作都是函数调用

前面重新定义(的例子之所以可以成功,是因为 R 语言中的每个操作都是函数调 用,无论看起来像不像。 这也包括中缀运算符,比如+,控制流运算符,比如 for、if 和 while,取子集操作符,比如[]和$,甚至花括号{。 这意味着,在以下例 子中的每一对语句都是完全等价的。 注意"`",重音符,可以让你引用以保留字或 者非法字符命名的函数或者变量:

x <- 10; y <- 5
x+y
#> [1] 15
`+`(x, y)
#> [1] 15
for (i in 1:2) print(i)
#> [1] 1
#> [1] 2
`for`(i, 1:2, print(i))
#> [1] 1
#> [1] 2
if (i == 1) print("yes!") else print("no.") #> [1] "no."
`if`(i == 1, print("yes!"), print("no."))
#> [1] "no."
x[3]
#> [1] NA
`[`(x, 3)
#> [1] NA
{ print(1); print(2); print(3) } #> [1] 1
#> [1] 2
#> [1] 3
`{`(print(1), print(2), print(3)) #> [1] 1
#> [1] 2
#> [1] 3

我们可以覆盖这些特殊函数的定义,但是几乎可以肯定,这是一个坏主意。 然 而,有些场合可能是有用的:它可以让你做一些使用常规方法不可能做到的事情。 例如,这一特性可以让 dplyr 包把 R 表达式翻译成 SQL 表达式。

函数参数

区分函数的形式参数和实际参数是有用的。 形式参数是函数的一个属性,而实际 参数或调用参数可以在每次调用函数时都不同。 本节讨论调用参数是怎样映射到 形式参数的,怎样通过保存了参数的列表来调用函数,默认参数是如何工作的, 以及延迟计算的影响。

调用函数

当调用一个函数时,你可以通过参数的位置,或者通过完整的名称或者部分的名 称,来匹配参数。 参数匹配的顺序是:首先是精确的名称匹配(完美匹配),然后 通过前缀匹配,最后通过位置匹配。

给定一个参数列表来调用函数

假设你有一个函数的参数列表:
怎样把这个列表传递给 mean()函数呢? 这时候,你需要 do.call()函数:

do.call(mean, list(1:10, na.rm = TRUE))
#> [1] 5.5 # 相当于
mean(1:10, na.rm = TRUE)
#> [1] 5.5

默认参数与缺失参数

f <- function(a = 1, b = 2) {
c(a, b)
}
f()
#> [1] 1 2

默认参数甚至可以使用在函数内部创建的变量来定义。 这是经常在基本 R 函数中 使用的技术,但是我认为这是不好的做法,因为如果没有阅读过完整的函数源代 码,那么你将不知道默认值到底是什么

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

你可以使用 missing()函数来确定某个参数是否已经提供了。

i <- function(a, b) {
c(missing(a), missing(b)) }
i()
#> [1] TRUE TRUE
i(a = 1)
 #> [1] FALSE TRUE
i(b = 2)
#> [1] TRUE FALSE i(1, 2)
#> [1] FALSE FALSE

有时你想添加一个简单的默认值,这可能需要几行代码来计算。 为了不把这段代 码插入函数定义,你可以在必要的时候使用 missing()函数有条件地计算它。 但是,如果没有仔细阅读文档,那么将使我们很难知道哪些参数是必需的,哪些是可 选的。 所以,我通常设置默认值为 NULL,并且使用 is.null()来检查参数是否被提 供。

...

有一种特殊的参数称为...。 这个参数将匹配任何尚未匹配的参数,并且可以很容易 地传递给其它函数。 如果你想把参数收集起来去调用另一个函数,但是又不想提 前说明它们可能的名字,那么这是有用的。 ...常与 S3 泛型函数结合使用,可以让 每个方法变得更加灵活。
一个相对复杂的使用...的例子是 base 包中的 plot()函数。 plot()是一个泛型方法, 它带有参数 x、y 和...。 为了理解对于一个给定的函数,...做了什么,我们需要阅读 帮助文档,文档里面说到:"传递给方法的参数,比如图形参数"。 最简单的 plot() 的调用最终会调用 plot.default(),该函数有更多参数,也有...。 再次,阅读文档揭 示了...接受"其它图形参数",这些参数列在 par()函数的帮助文档中。 因此,我们 可以编写这样的代码:

 plot(1:5, col = "red")
plot(1:5, cex = 5, pch = 20)

使用...是要付出代价的,任何拼写错误的参数都不会得到错误提示,任何...后的参 数都必须使用全名。 这使得拼写错误很容易被忽视.

中缀函数

大多数 R 中的函数是前缀操作符:函数的名称排在参数的前面。 你还可以创建中 缀函数,函数名位于它的参数之间,比如+或-。 所有用户创建的中缀函数都必须 以%开始和结束,R 预定义了这些中缀函 数:%%、%*%、%/%、%in%、%o%、%x%。
不需要%的内置中缀操作符的完整列表为:
::, :::, $, @, ?, *, /, +, -, >, >=, <, <=, ==, !=, !, &, &&, |, ||, ~, <-, <<-

替换函数

替换函数的行为表现得好像它们可以就地修改(译者注:modify in place,即修改立 即生效,直接作用在被修改的对象上)参数,并且它们都拥有特别的名字 xxx<-。 它们通常有两个参数(x 和值),虽然它们可以有更多参数,但是它们必须返回修改 过的对象。

返回值

函数中最后一个表达式的计算结果会成为函数的返回值,也就是调用函数的结过,return

结束

除了返回一个值以外,在函数结束时,函数也可以使用 on.exit()函数,设置其它的 触发动作。 这是一种在函数退出时,恢复全局状态的常用方法。 无论函数是如何 退出的,比如明确的(早期的)返回、发生了错误或者干脆就是到了函数体的结尾, on.exit()中的代码总是会执行。

in_dir <- function(dir, code) {
old <- setwd(dir) on.exit(setwd(old)) force(code)
}
getwd()
#> [1] "/Users/hadley/Documents/adv-r/adv-r" in_dir("~", getwd())
#> [1] "/Users/hadley"

基本模式很简单:
首先,我们将工作目录设置到一个新的位置,使用 setwd()的输出来获取当前的位 置。
然后,我们使用 on.exit(),以确保无论函数在何时退出,都要把工作目录恢复到原 来的值。
最后,我们明确地强制计算代码。 (我们在这里实际上并不需要 force(),但是它让
读者明白我们要做什么。)
警告:如果你在一个函数中调用多个 on.exit()函数,那么请务必设置 add = TRUE。 不幸的是,在 on.exit()中 add 的默认值是 add = FALSE,这样每次你运行 它的时候,它都会覆盖已有的退出(exit)表达式。 由于 on.exit()的特殊实现方式, 因此无法创建一个默认设置为 add = TRUE 的变种函数,所以使用它的时候必须小 心。

你可能感兴趣的:(R语言高级程序设计--函数)