函式在 Basics 概念中就已經介紹過了,當時的範例像這樣:
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
這項技巧在 stringr 等 Tidyverse 函式庫中被大量使用。
許多 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)函式,它會回傳一個運用閉包的函式,對點執行可重複的 2D 平移與縮放。
縮放引數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