函式

函式 在 R

1 個練習

關於 函式

我們在基礎概念中介紹過函式,舉過像這樣的例子:

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一樣。

Advanced

在一般使用中很少會存取函式的組成部分,但其實相當容易。

class(squareit)
#> [1] "function"
formals(squareit)
#> $x

body(squareit)
#> {
#>     x * x
#> }

用formals()取得的參數看起來像一個陣列,並使用$x語法。 事實上,它的型別是pairlist,一種包含鍵值對的特殊陣列。

body是可執行的程式碼,型別為language(這大概不是你需要在意的事)。

引數

和其他腳本語言(例如 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 會回傳修改後的副本,並保持原始物件不變。

複製大型資料結構的運算成本可能很高,但相較於資料損毀,這通常是比較小的代價。

Caution

小心那些會導致同一份資料被反覆複製的操作。

引用(Tidyverse 作者)Hadley Wickham 的話:

迴圈本身並不慢,但寫成迴圈後,很容易不小心把緩慢的操作放進迴圈裡。

向量化或高階函式可以幫你避開這類效能殺手。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 函式