Advanced R之函数
转载请注明出处,谢谢。
再次声明下,本人水平有些,错误之处敬请指正.
函数
函数是R基本的块结构单元:为了掌握本书中的更高级技术,你需要对函数有扎实的了解。也许你已经写过一些函数,并了解函数的基本知识。本章的焦点在于将你现有的对函数非正式的认识,转变为严密的理解,你将了解什么是函数,函数是如何工作的。在这一章你将看到一些有意思的技巧和技术,但最重要的是构造块结构更高级的技术。
理解R最重要的一点是,函数是自治的对象。可以像其他任何类型的对象一样来使用函数。这一点在函数编程中将深入讲解。
测试
回答下面的问题以便测试你是否需要阅读本章。可以在本章结尾处的答案查看答案。
- 函数的3个组成部分是什么?
-
下面的代码返回什么?
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!"))
-
中缀函数是什么?如何写?替换函数是什么?如何写?
-
使用什么函数来确保清理工作发生,无论函数是否正常终止?
概要
-
函数组成,该部分描述了函数3个主要组成部分。
-
语法作用域,介绍R中如何根据名字寻找值,即确定语法范围的过程。
-
每个操作都是函数,将向你展示在R中任何发生的事情都是函数调用的结果,即使看起来并非如此。
-
函数参数,讨论了3种给函数赋参数值的方式,如何在给定参数列表时调用函数,以及延迟计算的影响。
-
特殊调用,描述了2种特殊类型的函数:中缀函数和替换函数。
-
返回值,讨论了函数如何以及什么时候返回值,以及在退出前如何确保执行某些操作。
预备知识
唯一需要的包是pryr,利用该包,将展示当修改向量时发生了什么.使用install.packages("pryr")
安装该包.
函数组成
所有R函数都有3个部分:
-
body(),函数内的代码.
-
formals(),函数参数列表,控制了如何调用函数.
-
enviroment(),函数变量位置的"地图".
当你打印R函数时,将会显示其3个主要组成部分.如果没有显示环境,说明该函数的环境是全局环境.
f <- function(x) x^2 f #> function(x) x^2 formals(f) #> $x body(f) #> x^2 environment(f) #> <environment: R_GlobalEnv>
对body(),formals(),enviroment()赋值,可以用来修改函数.
如同R中其他对象一样,函数也可以有一系列附加的attributes()
.其中基本R使用的一个属性是“srcref”,它是源代码引用的缩写,它指向创建函数的源代码.不同于body(),“srcref”包含了代码注释和其他格式.你也可以对函数添加属性.比如,你可以设置class()
,添加一个自定义的print()函数.
原始函数
对于函数有3个主要组成部分的说法,有一个例外.原始函数,比如sum(),是通过.Primitive()
直接调用的C代码,其中没有R代码.因此其formals()
, body()
, 和environment()都是NULL:
sum #> function (..., na.rm = FALSE) .Primitive("sum") formals(sum) #> NULL body(sum) #> NULL environment(sum) #> NULL
原始函数只存在于base包,因此属于低级的操作,更加高效(原始替代函数没有拷贝),而且可以有不同的参数匹配规则(比如swich和call).然后这是有代价的,就是它们与其他函数行为不同.因此R核心团队一般会避免使用原始函数,如非没有选择.
练习
-
哪个函数可以判别一个对象是否是函数?哪个函数用来判别一个函数是否是原始函数?
-
下面的代码列出了base包中所有的函数.
objs <- mget(ls("package:base"), inherits = TRUE) funs <- Filter(is.function, objs)
利用它来回答下面的问题:
-
base包中哪个函数有最多的参数?
-
base包中有多少函数没有参数?这些函数有什么特别之处?
-
如何修改上述代码来找出所有原始函数?
-
-
函数的3个主要部分是什么?
-
如果打印一个函数,没有显示环境,是什么情况?
词法作用域
作用域是R寻找符号值的一系列规则.下面的例子中,作用域是R根据符号x寻找它的值10的规则:
x <- 10 x #> [1] 10
理解作用域可以:
-
通过组合函数创建工具,在函数式编程中介绍.
-
打破常规计算规则,创建非标准计算规则,在非标准计算中介绍.
R有2种类型的作用域:词法作用域和动态作用域.词法作用域在语言层面自动实现,动态作用域使用在交互式分析过程中选择函数,可以减少键盘输入.在这里我们讨论词法作用域因为它与函数创建紧密联系.动态作用域在作用域(译者注:在非标准计算一章中)中讨论.
词法作用域查找符号的值,根据的是函数创始时是如何嵌套的,而不是调用时如何嵌套的.在词法作用域下,你不必根据函数是如何调用的来确定变量的值的查找范围.你需要查看函数的定义.
"词法"一词在词法作用域中与其英文的常规定义("语言的或者与语言相关的词汇或者词汇表,用来区分其语法和结构")不同.而是来自计算机科学术语"lexing",是指将文字表示的代码转换为编程语言能理解的有意义片段过程的一部分.
R实现词汇作用域有4个基本规则:
- 名称屏蔽
- 函数与变量
- 全新的开始
- 动态查找
你可能已经知道其中一些规则,尽管你没有意识到.在看答案前,在脑子里过下下面的代码,以测试下你的理解.
名称屏蔽
下面的例子揭示了词法作用域最基本的规则,你应该可以正确预测结果.
f <- function() { x <- 1 y <- 2 c(x, y) } f() rm(f)
如果函数内部没有定义某个命名的变量,R会在上一级范围内寻找.
x <- 2 g <- function() { y <- 1 c(x, y) } g() rm(x, g)
如果函数定义在另一个函数内部,规则同样适用:首先在当前函数内部查找,然后在当前函数所在的函数查找,循环如此,直到全局环境,然后查找其他加载的包.在脑子中过下下面的代码,然后运行下以确认结果.
x <- 1 h <- function() { y <- 2 i <- function() { z <- 3 c(x, y, z) } i() } h() rm(x, h)
同样的规则适用于闭合函数,即其他函数创建的函数.闭合函数在函数式编程中介绍.这里我们只看下它们是如何影响作用域的.下面的函数j()返回一个函数.如果我们调用它,你认为该函数返回什么?
j <- function(x) { y <- 2 function() { c(x, y) } } k <- j(1) k() rm(j, k)
看起来有点不可思议(在函数被调用后,R是如何知道y的值的).这是因为k保留了其所在的环境,而该环境包含y的值.环境定义了其中关联到每个函数的值.
函数与变量
无论关联的值的类型,都适用同样的规则-查找函数与查找变量完全一样:
l <- function(x) x + 1 m <- function() { l <- function(x) x * 2 l(10) } m() #> [1] 20 rm(l, m)
对于函数有个地方稍有不同.如果在某个上下文中你使用了一个明显是个函数的名称,R查找时会忽略非函数的对象.下面例子中,基于R查找函数还是变量,n取不同的值.
n <- function(x) x / 2 o <- function() { n <- 10 n(n) } o() #> [1] 5 rm(n, o)
然而函数或者其他对象使用相同名字会造成令人困惑的代码,最好不要这么干.
全新的开始
多次调用一个函数,返回值会发生什么?第一次调用时会发生什么?第二次那?(如果你之前没有遇到过exists():如果存在匹配名称的变量,会返回TRUE,否则返回FALSE.)
j <- function() { if (!exists("a")) { a <- 1 } else { a <- a + 1 } print(a) } j() rm(j)
你会惊讶的发现它总是返回同样的值1.这是因为函数每次被调用时,会创建一个新的环境来承载执行.函数无法知道它上次执行时发生了什么;每次调用都是完全独立的.(我们将在函数式编程一章的易变状态部分展开介绍.)
动态查找
词法作用域定义了去哪儿找值,没有定义什么时候找.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"
另外一种可以尝试的解决办法是手动将函数的环境修改为emptyenv()
,这是一个完全的空环境:
environment(f) <- emptyenv() f() #> Error in f(): could not find function "+"
但是这并不好使,因为R会根据语法作用域找到任何东西,即使是操作符+.永远不可能使得函数完全自包含,因为你总是依赖于基础R或者其他包.
你可以据此去干一些恶作剧.比如,因为R中所有的标准操作符都是函数,你可以用自己的函数覆盖它们.你可以恶搞一下你的朋友,在他们不在的时候,在他们电脑上运行下面的代码:
`(` <- function(e1) { if (is.numeric(e1) && runif(1) < 0.1) { e1 + 1 } else { e1 } } replicate(50, (1 + 2)) #> [1] 3 3 3 3 3 3 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 #> [36] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 rm("(")
这将导致一个恶性bug:10%情况下,括号内的数值计算会被加1.这也是另一个以一个干净的R对话重启R的理由!
练习
-
下面代码返回什么?为什么?3个c的含义是什么?
c <- 10 c(c = c)
-
R查找值的4个规则是什么?
-
下面代码返回什么?运行代码前预测下.
f <- function(x) { f <- function(x) { f <- function(x) { x ^ 2 } f(x) + 1 } f(x) * 2 } f(10)
任何操作都是函数
“要理解R的计算,要记住2句话:
- 所有东西都是对象.
- 发生的事情都是函数."
— John Chambers
上个例子重新定义了(,这是因为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.领域语言利用该思想,通过已有的R结构,创建特定的领域语言来表达新的观点.
将特殊函数作为普通函数对待更加常见.比如,通过定义函数add(),我们可以使用sapply()来对列表中的每个元素加3,如下:
add <- function(x, y) x + y sapply(1:10, add, 3) #> [1] 4 5 6 7 8 9 10 11 12 13
我们也可以通过内置的+函数实现.
sapply(1:5, `+`, 3) #> [1] 4 5 6 7 8 sapply(1:5, "+", 3) #> [1] 4 5 6 7 8
注意`+`
与"+"不同.第一个是调用+函数,第二个是字符串+.第二个版本好使,是因为sapply()可以指定函数名称而不用指定函数本身:如果阅读sapply的源代码,你会发现第一行使用了match.fun()来进行名称匹配查找函数.
一个更加有用的应用是在构造子集时,联合使用lapply()或者sapply():
x <- list(1:3, 4:9, 10:12) sapply(x, "[", 2) #> [1] 2 5 11 # equivalent to sapply(x, function(x) x[2]) #> [1] 2 5 11
记住R中所有发生的事情都是函数调用,这将帮助你理解元程序.
函数参数
区分函数的形参和实参是有用的.形参是函数的一个属性,而实参在每次调用时都可能不同.这部分讨论实参如何映射到对应的形参上,给定一系列参数如何调用函数,默认参数如何工作,延迟计算的影响.
调用函数
调用函数时,可以根据位置,全称,或者部分名称来指定参数.参数首先按照准确的名称匹配(完美匹配),然后按照前缀匹配,最后按照位置.
f <- function(abcdef, bcde1, bcde2) { list(a = abcdef, b1 = bcde1, b2 = bcde2) } str(f(1, 2, 3)) #> List of 3 #> $ a : num 1 #> $ b1: num 2 #> $ b2: num 3 str(f(2, 3, abcdef = 1)) #> List of 3 #> $ a : num 1 #> $ b1: num 2 #> $ b2: num 3 # Can abbreviate long argument names: str(f(2, 3, a = 1)) #> List of 3 #> $ a : num 1 #> $ b1: num 2 #> $ b2: num 3 # But this doesn't work because abbreviation is ambiguous str(f(1, 3, b = 1)) #> Error in f(1, 3, b = 1): argument 3 matches multiple formal arguments
一般情况下,只有一个或者2个参数时,你会想根据位置匹配参数.这是最常用的方式,也是大多数读者了解的.对于不常用的参数,应该避免使用位置匹配,局部匹配时只使用可阅读的缩写.(如果你写的代码要打包发布到CRAN,你不能使用局部匹配,必须使用全称.)命名的参数应该始终在未命名参数的后面.如果一个函数使用...(将在下面讨论),你应该在...后面列出指定全称的参数.
下面的调用是个正确范例:
mean(1:10) mean(1:10, trim = 0.05)
下面的可能就有没必要了:
mean(x = 1:10)
下面的只能造成困惑:
mean(1:10, n = T)
mean(1:10, , FALSE)
mean(1:10, 0.05)
mean(, TRUE, x = c(1:10, NA))
给定参数列表时调用函数
假设你有一个参数列表:
args <- list(1:10, na.rm = TRUE)
你如何将其赋予给函数mean()?你需要调用do.call():
do.call(mean, list(1:10, na.rm = TRUE)) #> [1] 5.5 # Equivalent to mean(1:10, na.rm = TRUE) #> [1] 5.5
缺省和缺失参数
R中的函数参数可以有缺省值.
f <- function(a = 1, b = 2) { c(a, b) } f() #> [1] 1 2
由于R中的函数参数是延迟计算的(下面将进一步讨论),缺省值可以定义为其他参数的表达式:
g <- function(a = 1, b = a * 2) { c(a, b) } g() #> [1] 1 2 g(10) #> [1] 10 20
缺省值甚至可以被定义为函数内部变量的表达式.在基础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()来检查参数是否被赋值.
延迟计算
缺省情况下,R的函数参数是延迟计算的-只有当被使用到时它们才会被计算:
f <- function(x) { 10 } f(stop("This is an error!")) #> [1] 10
如果你想确保参数被计算,可以使用force():
f <- function(x) { force(x) 10 } f(stop("This is an error!")) #> Error in force(x): This is an error!
利用lapply()或者循环来创建闭合函数时,这非常重要:
add <- function(x) { function(y) x + y } adders <- lapply(1:10, add) adders[[1]](10) #> [1] 11 adders[[10]](10) #> [1] 20
当你调用adder函数中的一个时,x是延迟计算的.这时,循环完成,x的最终值是10.所以adder函数的所有函数都会在输入值上加10,这恐怕不是你想要的!(译者注:我没搞明白,修改参数后输出好像不是这样的.)手动设置强制计算来解决这个问题:
add <- function(x) { force(x) function(y) x + y } adders2 <- lapply(1:10, add) adders2[[1]](10) #> [1] 11 adders2[[10]](10) #> [1] 20
这相当于
add <- function(x) { x function(y) x + y }
这时因为force函数被定义为force <- function(x) x
.然而使用这个函数显示地表明你在使用强制计算,而不是偶然输入x.
缺省参数在函数内部计算.这意味着如果表达式依赖于当前环境,结果会根据你是否使用缺省值或者显示赋予一个值而不同.
f <- function(x = ls()) { a <- 1 x } # ls() evaluated inside f: f() #> [1] "a" "x" # ls() evaluated in global environment: f(ls()) #> [1] "add" "adders" "adders2" "args" "f" "funs" "g" #> [8] "h" "i" "objs" "path" "x" "y"
更具技术性的,一个未被计算的参数称为一个promise,或者(不常用)一个形式转换.一个promise由2部分组成:
-
表达式,它导致了延迟计算.(可以通过substitue()访问.更多内容参见非标准计算.)
-
环境,表达式创建和应该被计算的地方.
一个promise第一次被访问时,表达式会在其创建环境中被计算.其值会被缓存,所以连续访问被计算的promise不会重新计算其值(但是初始表达式仍然与值关联,所以substittue()可以继续访问它).使用pryr::promise_info()可以得到关于promise更多的信息.该函数使用C++代码,在不计算promise的情况下提取promise信息,这在纯R代码来说是不可能的.
延迟计算在if语句中很有用-下面第二个语句,只有当第一个返回TRUE时才会被计算.如果不是这样,那么将得到一个错误因为NULL > 0是一个长度为0的逻辑向量,对于if来说是个无效输入.
x <- NULL if (!is.null(x) && x > 0) { }
我们可以自己实现“&&”:
`&&` <- function(x, y) { if (!x) return(FALSE) if (!y) return(FALSE) TRUE } a <- NULL !is.null(a) && a > 0 #> [1] FALSE
该函数没有延迟计算,因此不能正常工作,因为x和y总是被计算,即使x是NULL也会测试a > 0.
有时你可以利用延迟计算来消除if语句.比如:
if (is.null(a)) stop("a is null") #> Error in eval(expr, envir, enclos): a is null
你可以这么写:
!is.null(a) || stop("a is null") #> Error in eval(expr, envir, enclos): a is null
...
...是个特殊的参数,这个参数可以匹配所有没有被匹配上的参数,而且可以很容易传递到其他函数中.如果你想搜集参数去调用其他函数,而不想指定它们的名字,使用...会很有帮助....常被与S3的泛型函数一起使用来使得单独的方法更加灵活.
一个对...相对高级的使用是base包中的plot()函数.plot()函数是个泛型函数,其参数是x,y和....要了解一个给定函数的...做什么用,我们需要阅读帮助:"被传递到方法的参数,比如图形参数".大多数对plot()函数的简单调用以调用plot.default()函数结束,plot.default()有更多的参数,也含有....同样我们阅读par()函数的帮助文档发现...接收"其他图形参数".这允许我们书写下面的代码:
plot(1:5, col = "red") plot(1:5, cex = 5, pch = 20)
这显示了...的优势和劣势:它使得plot()函数非常灵活,但是我们需要仔细阅读文档.另外,如果我们阅读plot.default的源代码,我们会发现一些文档中没有的特性.我们可以传递其他参数到Axis()函数和box()函数:
plot(1:5, bty = "u") plot(1:5, labels = FALSE)
为了以一种容易使用的方式捕获...的值,你可以使用list(...).(参见非标准计算中的捕获未计算的...内容,了解在不计算参数的情况下捕获...)
f <- function(...) { names(list(...)) } f(a = 1, b = 2) #> [1] "a" "b"
使用...是有代价的-任何拼写错误都将导致错误,...后面的参数都必须指定全名.这很容易忽略书写错误:
sum(1, 2, NA, na.mr = TRUE) #> [1] NA
最好显示写明参数而不是隐式表示,你不大可能要求用户提供一系列额外参数.对多附加功能的函数来说,使用...当然更容易.
练习
-
搞清楚下列函数调用的参数:
x <- sample(replace = TRUE, 20, x = c(1:10, NA)) y <- runif(min = 0, max = 1, 20) cor(m = "k", y = y, u = "p", x = x)
-
下面函数返回什么?为什么?揭示了什么规则?
f1 <- function(x = {y <- 1; 2}, y = 0) { x + y } f1()
-
下列函数返回什么?为什么?揭示了什么规则?
f2 <- function(x = z) { z <- 100 x } f2()
特殊调用
R支持2种调用特殊类型函数的额外语法:中缀函数和替代函数.
中缀函数
大多数R函数是"前缀"操作符:函数名称出现在参数之前.你也可以创建中缀函数,其函数名称出现在参数之间,比如+和-.所有用户自定义的中缀函数名称必须以%开始和结尾.R已经定义了如下中缀函数:%%
, %*%
, %/%
, %in%
, %o%
, %x%
.(不需要%的完整的内置中缀操作符列表是::, :::, $, @, ^, *, /, +, -, >, >=, <, <=, ==, !=, !, &, &&, |, ||, ~, <-, <<-
)
例如我们可以创建一个将字符串连接在一起的新的操作符:
`%+%` <- function(a, b) paste0(a, b) "new" %+% " string" #> [1] "new string"
注意当创建函数时,将函数名称放在`号内部,因为它是特殊的名称.对于普通函数调用,这只是一个语法糖;就R而言下面的2个表达式没有区别:
"new" %+% " string" #> [1] "new string" `%+%`("new", " string") #> [1] "new string"
又比如下面2个相等的表达式
1 + 5 #> [1] 6 `+`(1, 5) #> [1] 6
中缀函数的名称比常规函数更加灵活:它们可以包含任意字符(当然除了%).定义函数时,对于特殊字符需要进行转义字符,调用时不需要:
`% %` <- function(a, b) paste(a, b) `%'%` <- function(a, b) paste(a, b) `%/\\%` <- function(a, b) paste(a, b) "a" % % "b" #> [1] "a b" "a" %'% "b" #> [1] "a b" "a" %/\% "b" #> [1] "a b"
R默认的优先规则是,中缀操作符是从左至右组成的:
`%-%` <- function(a, b) paste0("(", a, " %-% ", b, ")") "a" %-% "b" %-% "c" #> [1] "((a %-% b) %-% c)"
我最常用的一个中缀函数是%||%,它由Ruby的||逻辑操作符启发,尽管它在R中稍有不同,这是因为Ruby对于if语句中计算什么为TRUE有更灵活的定义.万一另外一个函数的输出为NULL,提供一个缺省值是非常有用的:
`%||%` <- function(a, b) if (!is.null(a)) a else b function_that_might_return_null() %||% default value
替换函数
替换函数看起来就像它们修改了参数,并且有特殊的名字xxx<-.它们一般有2个参数(x和value),尽管它们可以有更多参数,而且它们必须返回修改后的对象.比如下面的函数修改了向量的第二个元素:
`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计算赋值语句second(x) <- 5
时,它注意到<-左侧不是一个简单的名称,所以它会寻找一个名字为second<-的函数来完成替代.
我说它们"看起来"像修改了参数,因为它们的确创建了一个修改后的拷贝.我们可以用pryr::address()来查看对象的内存地址.
library(pryr) x <- 1:10 address(x) #> [1] "0x2c29230" second(x) <- 6L address(x) #> [1] "0x397efe0"
使用.Primitive()实现的内置函数会直接修改:
x <- 1:10 address(x) #> [1] "0x103945110" x[2] <- 7L address(x) #> [1] "0x103945110"
由于其显著的效率,注意到这一点非常重要.
如果你要提供额外的参数,这些参数应该放在x和value之间:
`modify<-` <- function(x, position, value) { x[position] <- value x } modify(x, 1) <- 10 x #> [1] 10 6 3 4 5 6 7 8 9 10
当你调用modify(x, 1) <- 10
时,R会将其转换为:
x <- `modify<-`(x, 1, 10)
这也就意味着你不能这么干:
modify(get("x"), 1) <- 10
因为这将导致无效的代码:
get("x") <- `modify<-`(get("x"), 1, 10)
将替换函数与构造子集联合使用非常有用:
x <- c(a = 1, b = 2, c = 3) names(x) #> [1] "a" "b" "c" names(x)[2] <- "two" names(x) #> [1] "a" "two" "c"
这好使因为表达式names(x)[2] <- "two"
的计算如同:
`*tmp*` <- names(x) `*tmp*`[2] <- "two" names(x) <- `*tmp*`
(是的,的确是创建了一个局部变量*tmp*,它随后会被移除.)
练习
- 找出base包中所有的替换函数.哪些是原始函数?
- 用户自定义中缀函数的有效名称是什么?
- 创建一个中缀操作符
xor()
. - 创建设置函数
intersect()
,union()
,setdiff()对应的中缀函数.
- 创建一个修改向量随机位置的替换函数.
返回值
函数最后一个计算的表达式会作为返回值,即调用函数的结果.
f <- function(x) { if (x < 10) { 0 } else { 10 } } f(5) #> [1] 0 f(15) #> [1] 10
通常我觉得在提前返回时,显示使用return()是个好的编码风格,比如遇到错误或者一个简单的情况.这种风格可以减少缩进的层次,而且代码容易阅读,因为你使得局部代码更合理.
f <- function(x, y) { if (!x) return(y) # complicated processing here }
函数只能返回一个对象.但是这并不是一个限制,因为可以返回一系列对象的列表.
最易理解和推论的函数是纯函数:纯函数总是将相同的输入映射到相同的输出,并且不对工作空间作出其他影响.换句话说,纯函数没有副作用:除了它们返回的值,它们不影响其他状态.
R提供了一种限制副作用的机制:大多数R对象在修改时会产生拷贝.所以修改一个函数参数并不影响原始值:
f <- function(x) { x$a <- 2 x } x <- list(a = 1) f(x) #> $a #> [1] 2 x$a #> [1] 1
(修改时产生拷贝这个规则有2个例外:环境和引用类.它们可以直接被修改,所以要多加小心)
与Java等语言有很大不同,Java可以修改函数的输入.修改时产生拷贝这种行为会产生严重的性能损耗,在优化代码一章中的性能分析部分深入讨论.(注意,性能损耗看作是R实现修改生成拷贝机制的结果;但是也不完全对.Clojure是一个新的语言,它大量使用了拷贝修改的机制,但是性能损失有限.)
大多数基础R函数是纯函数,但是有一些例外:
-
library()
,该函数加载包,因此修改了查询路径. -
setwd()
,Sys.setenv()
,Sys.setlocale()它们分别
修改工作目录,环境变量,局部设置. -
plot()和friends,产生图形输出.(译者注:没搞懂friends什么意思.)
-
write()
,write.csv()
,saveRDS()
,等函数,它们保存输出到磁盘. -
options()和
par(),它们修改全局设置.
-
S4相关的函数,它们修改类和方法的全局表.
-
随机数字生成器,每次运行它们会产生不同的数字.
一般来说尽量减小副作用是个好主意,如果可能的话,应该应该将纯函数和非纯函数分开,以减小副作用.纯函数很容易测试(你唯一要关心的是输入和输出),而且对不同版本的R和平台,都相差不大.比如,ggplot2有这样的一个规则:绝大部分作用于对象的操作都代表一个plot,只有最后的print或者plot调用才在实际绘制plot时产生副作用.
有些函数返回invisible
的值,当调用这些函数时,默认不打印输出.
f1 <- function() 1 f2 <- function() invisible(1) f1() #> [1] 1 f2() f1() == 1 #> [1] TRUE f2() == 1 #> [1] TRUE
你可以强制显示一个不可见值,只要将其放在括号内:
(f2()) #> [1] 1
最常见的返回不可见值的函数是<-:
a <- 2 (a <- 2) #> [1] 2
这使得可以将一个值赋给多个变量:
a <- b <- c <- d <- 2
等同于:
(a <- (b <- (c <- (d <- 2)))) #> [1] 2
退出
如同返回一个值,当函数结束时,函数可以使用on.exit()来设置一些触发器,保证一些代码被执行.这通常用来保证当函数结束时,对于全局状态的改变得到保存.on.exit()的代码将执行,无论函数如何结束,无论是否显示(或提前)返回,或遇到错误,亦或到达了函数体末尾.
in_dir <- function(dir, code) { old <- setwd(dir) on.exit(setwd(old)) force(code) } getwd() #> [1] "/home/travis/build/hadley/adv-r" in_dir("~", getwd()) #> [1] "/home/travis"
基本步骤很简单:
-
首先设置目录到一个新的位置,通过setwd()的输出获取当前位置.
-
使用on.exit()保证工作目录返回到之前的值,无论函数如何结束.
-
最后我们显示强制计算代码.(我们本不需要force(),只是为了显得我们是故意这么做的.)
注意:在一个函数内部多次使用on.exit()时,要确保设置add = TRUE
.不幸的是,on.exit()
中的默认设置是add = FALSE
,所以你每次运行它,它都会覆盖存在的退出表达式.因为这种实现方式,使用add = TRUE
不可能创建一个变体,所以使用时要小心.
练习
-
如何比较
source()
中的参数chdir与
in_dir()?你为何更喜欢其中一个?
-
哪个函数取消
library()的效果
?如何保存options()
和par()的值
? -
写一个函数,来打开图形设备,运行提供的代码,然后关闭图形设备(不要考虑图形代码是否可以工作).
-
可以使用on.exit()来实现
capture.output()
函数的一个简单版本.capture.output2 <- function(code) { temp <- tempfile() on.exit(file.remove(temp), add = TRUE) sink(temp) on.exit(sink(), add = TRUE) force(code) readLines(temp) } capture.output2(cat("a", "b", "c", sep = "\n")) #> [1] "a" "b" "c"
比较
capture.output()
和capture.output2()
.这2个函数有何不同?我移除了哪些特性以便使得关键点更容易被发现?我是否使得关键点更易理解哪?
测试答案
-
函数的3个组成部分是函数体,参数和环境.
-
f1(1)()
返回11. -
一般的写法是利用中缀函数:
1 + (2 * 3)
. -
将调用重写为
mean(c(1:10, NA), na.rm = TRUE)
比较容易理解. -
不,它不会抛出错误因为第二个参数不会被使用所以它不会被计算.
-
参见中缀函数与替换函数部分的内容.
-
细节
参见退出部分的内容.