函數的傳遞與使用

R 的函數也可以像一般的變數一樣,當作參數傳入其他的函數中使用,或是作為函數的傳回值。最簡單的例子就是 do.call 函數,它提供了另外一種函數的呼叫方式,可以讓使用者將參數以列表變數的方式傳入:

do.call(hypotenuse, list(x = 3, y = 4))
[1] 5

這樣的效果相當於直接呼叫:

hypotenuse(x = 3, y = 4)

在將函數當作參數使用時,不一定要先建立具名的函數,假設我們自訂一個函數為 my.plus,將其傳入 do.call 中使用:

my.plus <- function(x, y) x + y
do.call(my.plus, list(1, 2))
[1] 3

我們可以改用匿名函數的方式,讓程式碼更簡潔:

do.call(function(x, y) x + y, list(1, 2))
[1] 3

將函數作為傳回值的狀況比較少見,ecdf 這個計算 empirical cumulative distribution function 的函數是一個比較有可能會遇到的例子:

x <- rnorm(50)
x.ecdf <- ecdf(x)
is.function(x.ecdf)
[1] TRUE
x.ecdf(-1.3)
[1] 0.02

變數範圍

變數範圍(variable scope)是指一個變數的存在範圍,亦即變數可以被使用的區域。

R 在取用變數時,會依循環境空間的繼承性原則,先從目前的環境空間中尋找變數,若目前的環境空間中沒有要找的變數,就會循著母環境空間持續往上尋找,直到找到符合的變數為止。

依據 R 環境空間的繼承性原則,在函數中我們可以直接取用外部全域的變數:

x.out <- 8
my.func <- function() {
  message("x.out is ", x.out)
}
my.func()
x.out is 8

當我們在 my.func 函數中取用 x.out 這個變數時,R 會先在 my.func 的環境空間中尋找 x.out 這個變數,當發現這個變數不存在時,就會繼續從它的母環境空間中尋找,也就是全域環境空間(global environment),最後取得在全域環境空間中所定義的 x.out

全域環境空間中所定義的變數,在任何地方都可以被使用,所以定義在全域環境空間中的變數也稱為全域變數(global variables),而定義在一般函數中的變數,則稱為區域變數(local variables)。

若在一個函數中建立一個變數後,可以在函數的內部使用該變數,但在此函數的外部就無法使用:

my.func <- function() {
  x.in.func <- 12
  message("x.in.func is ", x.in.func)
}
my.func()
x.in.func is 12

如果在函數外部就無法存取 x.in.func 這個函數內部的變數:

message("x.in.func is ", x.in.func)
Error in message("x.in.func is ", x.in.func) : 找不到物件 'x.in.func'

如果在一個函數中又建立了一個子函數,則在子函數中可以直接取用上層函數中的變數:

f <- function(x)
{
  y <- 1
  g <- function(x)
  {
    (x + y) / 2
  }
  g(x)
}
f(5)
[1] 3

若我們將 g 函數移到 f 函數之外,這樣的話 g 函數就不是 f 的子函數,也就無法取得 f 函數內部的變數:

f <- function(x)
{
  y <- 1
  g(x)
}
g <- function(x)
{
  (x + y) / 2
}
f(5)
Error in g(x) : 找不到物件 'y'

環境空間的繼承性原則可以讓 R 的程式開發者更容易撰寫程式,不過也時常會帶來一些困擾,讓程式碼變得難以維護,假設有一個 h 函數定義如下:

h <- function(x) {
  x + y
}

乍看之下,這個 h 函數中的 y 變數沒有定義,應該無法執行,若在一個乾淨的 R 執行環境之下,會產生找不到 y 變數的錯誤:

h(3)
Error in h(3) : 找不到物件 'y'

但是如果我們在全域環境空間中建立一個 y 變數,狀況就會改觀了:

y <- 1:3
h(3)
[1] 4 5 6

原本應該會產生錯誤的函數,現在變成可以正常執行了,這是因為 R 在執行 h 函數時,當發現 h 函數中無法找到 y 這個變數的定義時,就會循著 h 函數上層的環境空間來尋找,由於我們在全域環境空間中建立了一個 y 變數,所以 R 就會直接取用這個 y 全域變數。

當開發大型程式時,要小心使用全域變數,否則很容易讓程式產生很多 bugs,而且也非常難維護。我們來看下面這個例子:

h2 <- function(x)
{
  if (runif(1) > 0.5) y <- 2
  x * y
}

在這個例子,y 有一半的機率會在 h2 被定義,如果 h2 函數中有定義 y,它就會使用 h2 自己定義的 y,若 h2 函數中沒有定義自己的 y 變數,就會使用全域的 y 變數,相信大家都可以看得出來,這樣的程式碼架構是很容易出現 bugs 的。

通常在撰寫 R 程式時,應該盡可能將所有需要使用到的變數都透過參數來傳遞,不要使用全域變數的方式來取得,以降低程式出錯的機率與維護的成本。