函数最早是在基础这个概念里介绍的,当时用了这样的例子:
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 的说法:
循环本身并不慢,但一不留神就很容易把慢操作放进循环里。
向量化或高阶函数可以帮你避开这类性能杀手。
你的公司一直在使用地图数据来构建网页。 经过一番讨论,你们决定开始使用 R 来动态完成一些计算。
请尽量直接处理向量点:不需要把它们拆分成单独的x和y坐标。
实现scale(point, s)函数,它接收一个任意维度的点,并按预先定义好的s对它进行缩放。
# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4
实现translate(point, ...)函数,它返回一个新的点,其中每个坐标都按省略号里的值移动。
这个函数必须能处理任意维度的点,并且省略号中参数的数量要与该维度相匹配。
point2d <- c(2, 3)
# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6
point3d <- c(2, 3, 4)
# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7
你的有些队友对 R 不太熟悉,所以你决定用函数闭包来为{x, y}坐标对创建可复用的变换。
实现transform2d(dx, dy, s)函数,它返回一个函数,利用闭包对点进行可重复的二维平移和缩放。
缩放参数s应该是可选的,默认值为1。
返回的函数应接收一个 2D 点,然后:
dx和dy平移它s缩放它# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`
tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"
tf2d(c(2, 3))
#> [1] 5.0 7.2
# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)
tf2d(c(2, 3))
#> 2.5 3.6
运算顺序很重要:先平移再缩放才能得到正确结果,先缩放再平移则不行。 用专业一点的说法,这两个运算是_不可交换的_。
地图数据也可能包含高度,用来在屏幕上添加阴影和等高线。
实现transform3d(dx, dy, dz, s)函数,它返回一个函数。
缩放参数s应该是可选的,默认值为1。
返回的函数应接收一个 3D 点,按预先定义好的值平移它,然后按s缩放它。
# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7
# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29