函数早在基础概念中就介绍过了,比如下面这个例子:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
仔细观察squareit的定义,可以看到几个组成部分:
x。{ }括起来。squareit。在 R 中,函数是一等对象,和数字、字符串没什么两样。
因此,squareit <- function...这样的赋值在语法上和x <- 42完全一样。
与其他脚本语言(比如 Python 和 Julia)不同,R 并不明确区分位置实参和关键字实参。
函数调用既可以按位置传值,也可以按名称传值。 对于实参很多、顺序很难记住的复杂函数,后者特别有用。
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
可以在函数定义里指定默认实参值,但这些实参必须放在所有没有默认值的实参后面。
之后我们可以选择接受默认值,或者把它覆盖掉。
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
如果要接受任意数量的额外实参,可以在定义中使用...(省略号)。
你可以把函数调用里多出来的值转换成一个向量,不过请继续往下读,还有另一种使用这些“点参数”的方式(在其他几门语言里叫“varargs”)。
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
前面我们说过,形参和函数体都是函数的组成部分。
其实还有第三个组成部分:函数定义时所在的环境。
嵌套函数的情况可以说明这一点:
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
函数调用把x = 5传给外层函数,这个值在外层函数体里是可用的。
内层函数是外层函数体的一部分,因此能访问x的值。
换种说法,x = 5位于内层函数的环境中。
严格来说,这叫做闭包。
环境在处理点参数时尤其重要,因为通过点参数提供的值可以继续传递给函数体里的其他函数调用。 外层函数不需要知道、也不需要关心这些点参数是什么含义。
f_var <- function(x, ...) {
sum(x, ...)
}
x <- c(1, 2, NA, 6)
# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA
# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9
这种技巧被 Tidyverse 系列库大量使用,比如stringr。
许多stringr函数只是对stringi和 base R 中底层函数的友好封装。
传给str_*()函数的额外实参,会被直接传递到那些底层函数。
外层函数可以定义一个内层函数(可以是具名的,也可以是下面会讲到的匿名函数),并把它作为返回值。
被返回的函数会带上它定义时所在的环境。
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
定义函数时,我们通常会把生成的函数对象绑定到一个变量上:
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
这样在脚本后面使用这个函数会很方便,但这种绑定并不是必需的。 一个简短的、只用一次的函数,在当下这个场景里可能就够用了。 如果不绑定名称,它就叫做匿名函数。
匿名函数用得实在太多,所以(从 R v4.1.0 起)有了一种定义它们的简写语法:把function这个词换成反斜杠\。
上一节讲返回值时就用过这样的例子。
R 允许给向量的单个元素赋值。 如果我们把一个向量作为实参传进去,在函数体里修改它再返回,得到的就是修改后的向量。
但原来的那个向量怎么样了呢?
f <- function(vec) {
vec[1] <- 42
vec
}
vals <- c(1, 3, 4)
# f() returns a modified vector
f(vals)
#> [1] 42 3 4
# the original vector is unchanged.
vals
#> [1] 1 3 4
R 是一门为数据科学设计的语言。 收集这些数据可能要花大量时间、精力,甚至可能是天文数字般的金钱:所以千万不要把数据损坏了!
总的原则(少数例外除外)是修改时复制。 如果一个对象(比如向量)的改动可能给后面带来问题,R 会返回一个修改后的副本,而原来的对象保持不变。
复制大型数据结构可能很耗费计算资源,但相比数据被损坏,这通常算是两害相权取其轻。
小心那些会导致同一份数据被反复复制的操作。
用 Tidyverse 作者 Hadley Wickham 的话来说(大意):
循环本身并不慢,但它让人太容易在循环里加入慢操作,这一点很危险。
向量化或高阶函数可以帮助你避开这类性能杀手。