函数
函数是R的基石:要想掌握这本书中更多的高级技能,你必须对函数运行了如指掌。也许你已经写了不少 R 语言函数,也熟悉了函数运行的基本知识。这一章的主要目的,是帮助你将已有的不成体系的函数知识,转化为对函数本质和函数如何运行的牢固知识体系。在这一章里,你将见识到不少有趣的小窍门,但是你将学到的大多数东西更重要,因为它们是学习高级技能的基础。functions
理解R最重要的一点是,函数自身也是对象。你可以像处理其他类型的对象一样处理函数。关于函数会在函数式编程进行更加深入的讨论。
测试
做做这个简单的测试看看你是否需要阅读本章内容。如果你能很快地得到答案,可以轻松地跳过本章。本章最后提供参考答案。
函数的三大组成部分是什么?
下面代码的返回值是什么?
x <- 10 f1 <- function(x) { function() { x + 10 } } f1(1)()
如何用更通俗的方式编写下面的代码?
`+`(1, `*`(2, 3))
如何用更易读的方式调用这个函数?
mean(, TRUE, x = c(1:10, NA))
这个函数在调用是是否会报错?为什么?
f2 <- function(a, b) { a * 10 } f2(10, stop("This is an error!"))
什么是中缀函数(infix functions)?如何编写?什么是置换函数(replacement functions)?如何编写?
什么函数能确保无论函数如何终止都执行清除指令?
大纲
函数组成 介绍函数的三大组成部分。
作用域 告诉你R如何从名称中找到值,作用域的过程。
操作即调用 R中的一切都是函数调用的结果,即使看起来一点儿也不像。
函数参数 讨论将参数传递到函数的三种方式,如何调用给定参数列表的函数,以及延迟求值的作用。
特殊调用 介绍两种特殊的函数:中缀函数和替代函数(infix and replacement functions)。
返回值 讨论函数如何以及何时返回结果,还将探讨如何确保函数在退出之前进行某些操作。
先决条件
你唯一需要的包是 pryr
, 当修改对应位置的向量时会它可以告诉我们发生了什么。安装命令为 install.packages("pryr")
。
函数组成
所有的R函数都有三个部分:
主体
body()
,函数内部的代码。形式参数
formals()
,控制如何调用函数的参数列表。环境
environment()
,函数变量的位置映射。
当你在R中打印一个函数,就会看到这三个组成部分。如果没有显示环境,那么就意味着这个函数是在全局环境中被创建的。
f <- function(x) x^2
f
#> function(x) x^2
formals(f)
#> $x
body(f)
#> x^2
environment(f)
#> <environment: R_GlobalEnv>
body()
、formals()
和 environment()
的赋值形式也能用于修改函数。
和R中的所有对象一样,函数也能拥有任意数量额外属性 attributes()
。R中的一种基础属性是“源码引用”(srcref,source reference的简称),用来指向创造函数的源代码。与body()
不同,属性可以包含代码注释和其他格式。你也可以在函数中添加属性。比如,你可以在print()
函数中添加类属性class()
来定制打印方法。\index{functions!attributes}
原函数
函数包含三个组件,这一规则只有一个例外。原函数(primitive functions),比如sum()
,直接使用.Primitive()
调用C语言代码而不包含任何R代码。因此,它的三个组件均为null
。
sum
formals(sum)
body(sum)
environment(sum)
原函数只存在于基础包(base)中,因为它们是在底层进行运算,所以运算效率更高(原函数的置换函数不需要复制),参数匹配规则也不同(如switch
和call
)。这使得原函数与R中的其他函数的表现不一样。因此除非别无他法,R核心团队通常避免创建原函数。
练习
1.什么函数能够帮助你判断一个对象是不是函数?什么函数能够帮助你判断一个函数是不是原函数?
2.这段代码创建了一个列表,里面包含了基础包中的所有函数。
objs <- mget(ls("package:base"), inherits = TRUE)
funs <- Filter(is.function, objs)
根据它来回答下列问题:
a.哪一个基础包中函数的参数最多?
b.有多少基础包中的函数没有参数?这些函数有什么特点?
c.你能修改这段代码,找出所有的原函数吗?
3.函数的三个组件是什么?
4.当打印一个函数时没有显示环境,那么它是在什么环境中被创建的?
作用域
作用域是控制R如何查找值的规则的集合。下例中,作用域就是,R如何应有这些规则,让符号x
找到它的值10
:
\index{scoping!lexical|see{lexical scoping}} \index{lexical scoping}
x <- 10
x
理解作用域可以帮助你:
通过创建函数构建工具,详见 函数式编程。
重新覆盖常用的参数传递规则,进行非标准参数传递(non-standard evaluation),详见 non-standard evaluation
R 语言有两种类型的作用域:静态作用域,在语言层面自动实现;动态作用域,在交互分析中用于选择函数,可以节省打字。在这里我们只讨论静态作用域,因为它是在函数创建时就确定了的。动态作用域详见 scoping-issues。
静态作用域查找符号的值是根据函数在创建时是如何嵌套的,而不是在调用的时候。在静态作用域下,你不必知晓函数是如何被调用的就可以知道去哪里找变量的值。只用看看函数的定义就行了。
静态作用域(lexical scoping)中“lexical”一词并不是通常英语中的定义(“为了区分一门语言的语法和结构而与单词或词汇相关的”),而是计算机科学术语“词法分析”(lexing),将文本代码转化为计算机可以理解的片段。
R语言的静态作用域实现有四个基本规则:
- 名称覆盖(name masking)
- 函数 vs. 变量
- 初始化(a fresh start)
- 动态查找(dynamic lookup)
也许你已经知道了这些原则,但你可能还没有仔细想过。在脑海中运行代码,记得在那之前不要看答案。
名称覆盖
下面的例子阐释了静态作用域的最基本原则,预测下面的输出值应该毫无压力。
```{r, eval = FALSE} f <- function() { x <- 1 y <- 2 c(x, y) } f() rm(f)
如果在函数内部名称没有被定义,R会在上一层级里查找。
```{r, eval = FALSE}
x <- 2
g <- function() {
y <- 1
c(x, y)
}
g()
rm(x, g)
这一规则同样适用于在另外一个函数内部定义的函数:先在当前函数内部查找,然后在函数被创建的环境中查找,以此类推,直到全局环境,最后到其他被加载的包中查找。在脑海中运行代码,然后运行R代码,确认答案。
{r, eval = FALSE}
x <- 1
h <- function() {
y <- 2
i <- function() {
z <- 3
c(x, y, z)
}
i()
}
h()
rm(x, h)
这一规则也适用于闭合环境,