我們在基礎概念中介紹過函式,舉過像這樣的例子:
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
若要接受任意數量的額外引數,可以在定義中使用...(省略符號)。
可以把函式呼叫中的額外值轉成向量,不過請繼續往下讀,看看使用這些「dot args」的另一種方式(在其他一些語言中稱為「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位於內層函式的環境中。
技術上來說,這稱為閉包。
環境在使用 dot args 時特別重要,因為用這種方式提供的任何值,都能傳遞給函式主體中的其他函式呼叫。 外層函式不需要知道,也不需要在意這些 dot args 代表什麼。
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 的話:
迴圈本身並不慢,但寫成迴圈後,很容易不小心把緩慢的操作放進迴圈裡。
向量化或高階函式可以幫你避開這類效能殺手。