這裡我們將詳細介紹 R 函數與 R 環境空間的使用方式。

環境空間

在 R 中所有的變數都是儲存在特定的環境空間(environments)中,而環境空間本身其實也是一種變數,可以當成一般變數使用(例如指定新的值、或是當成參數傳遞至函數中),它的性質跟列表(list)比較相近,甚至可以直接將列表變數轉換為環境空間變數,反之亦可。

通常我們在使用 R 的時候,不太需要去理會環境空間的問題,當我們在 R 的命令列建立一個變數時,R 會自動將該變數儲存在全域環境空間(global environment,這個空間也稱為 user workspace)中,另外當我們呼叫一個函數時,R 也會自動建立一個隸屬於該函數的環境空間來儲存與該函數相關的變數,理解變數以及環境空間的運作,可以幫助程式設計師更清楚 R 變數命名空間規則,以及程式除錯時的呼叫堆疊問題(call stack)。

若要建立一個新的 R 環境空間,可以呼叫 new.env

my.new.env <- new.env()

環境空間的操作方式跟列表變數相同,我們可以使用雙中括號或是錢字號的方式來在此環境空間中建立新的變數:

my.new.env[["name"]] <- "G.T.Wang"
my.new.env$foo <- c(1, 3, 5)

用來指定變數值的 assign 函數也可以使用 envir 參數來指定變數的環境空間:

assign(
  "bar",
  c(TRUE, FALSE, NA),
  my.new.env
)

要取出特定環境空間中的變數,也是跟列表的操作類似:

my.new.env[["name"]]
[1] "G.T.Wang"
my.new.env$foo
[1] 1 3 5

除此之外,也可以使用 get 函數:

get("bar", my.new.env)
[1]  TRUE FALSE    NA

lsls.str 函數也有提供使用者指定環境空間的功能:

ls(envir = my.new.env)
[1] "bar"  "foo"  "name"
ls.str(envir = my.new.env)
bar :  logi [1:3] TRUE FALSE NA
foo :  num [1:3] 1 3 5
name :  chr "G.T.Wang"

若要檢查特定的變數是否存在,可以使用 exists 函數:

exists("foo", my.new.env)
[1] TRUE

環境空間與列表之間若要互相轉換,可以使用 as.listas.environment 函數來處理:

my.list <- as.list(my.new.env)
my.list
$name
[1] "G.T.Wang"

$foo
[1] 1 3 5

$bar
[1]  TRUE FALSE    NA
as.environment(my.list)
<environment: 0x7fe9f2dd6a38>

list2env 也可以將列表轉為環境空間:

list2env(my.list)
<environment: 0x7fe9f2dd6a38>

在 R 中所有的環境空間都有繼承的關係,也就是說除了最頂層的空環境空間(empty environment)之外,任何一個環境空間都會有一個母環境空間(parent environment),在預設的狀況下,existsget 函數除了檢查目前的環境空間之外,也會一併檢查所有的母環境空間,如果要讓它只檢查目前的環境空間,可以加入 inherits = FALSE 參數:

child.env <- new.env(parent = my.new.env)
exists("name", child.env)
[1] TRUE
exists("name", child.env, inherits = FALSE)
[1] FALSE

我們在 R 命令列中所建立的任何變數預設都會被儲存於 global 這個環境空間中,若要取得 global 環境空間可以使用 globalenv 這個函數:

globalenv()
<environment: R_GlobalEnv>

或是直接取用 .GlobalEnv 這個變數也可以:

.GlobalEnv
<environment: R_GlobalEnv>

另外一個比較特別的環境空間是 base,他是 R 中最基本的一個環境空間,我們可以使用 baseenv 來取得之:

baseenv()
<environment: base>

其中儲存了很多 R 的基礎函數與運算子:

head(ls(envir = baseenv()), 20)
 [1] "-"            "-.Date"       "-.POSIXt"
 [4] ":"            "::"           ":::"
 [7] "!"            "!.hexmode"    "!.octmode"
[10] "!="           "("            "["
[13] "[.AsIs"       "[.data.frame" "[.Date"
[16] "[.difftime"   "[.Dlist"      "[.factor"
[19] "[.hexmode"    "[.listof"

parent.env 這個函數可以用來取得指定環境空間的母環境空間:

parent.env(globalenv())
<environment: 0x7fa52d118720>
attr(,"name")
[1] "tools:RGUI"

在 R 中會遇到環境空間的狀況主要有兩種,一種是呼叫函數時,每個函數會有自己專屬的環境空間(也稱為 closure),而另外一種則是載入各種 R 套件時,套件內的函數都會放在套件自己的環境空間中,透過搜尋路徑的方式來讓使用者取得套件內的各種函數,這些狀況會在之後的教學中詳細說明。

函數

R 的函數也是一種特別的變數,它除了可以接受輸入的變數、並在執行一些運算之後傳回結果之外,也可以當成一般的變數使用,例如指定新的函數內容,或是當成別的函數的輸入參數等,以下介紹函數的使用方式。

建立與呼叫函數

在建立函數之前,我們先來看一下函數的內容。直接輸入函數的名稱,即可顯示函數的內容:

rt
function (n, df, ncp)
{
    if (missing(ncp))
        .Call(C_rt, n, df)
    else rnorm(n, ncp)/sqrt(rchisq(n, df)/df)
}
<bytecode: 0x27f97f0>
<environment: namespace:stats>

上面這些輸出就是 rt 函數(用來產生 t 分佈隨機變數的函數)的內容,function 關鍵字之後用小括號包起來的就是此函數的參數,從中可以看出 rt 這個函數的輸入參數有三個,分別是 ndfncp

一般在呼叫函數時所傳入的值稱為該函數的參數(arguments),而這裡在函數內容中 function 內的參數名稱則稱為正式參數(formal arguments),這兩者的區別在一般的情況下並不是很重要,所以在以下的教學內容中,我們不會特別去區分這兩種參數。

在參數之後,以大括號包起來的部分就是函數的主體(body),也就是每次該函數被呼叫時會執行的程式碼。

在 R 的函數中若要將計算的結果傳回,可以使用 return 這個關鍵字再加上要傳回的資料,除此之外,若沒有明確呼叫 return 指定傳回值的時候,R 會把此函數中最後一個運算式的結果當作傳回值。以這個 rt 函數來說,如果沒有指定 ncp 參數,R 就會執行 .Call 並以其執行結果作為傳回值,反之若 ncp 餐數有被指定,則會執行 rnorm 那一行運算,並將其運算結果作為傳回值。

若要建立一個自訂的函數,就只要將函數的整個內容指定給一個變數即可:

hypotenuse <- function(x, y)
{
  sqrt(x ^ 2 + y ^ 2)
}

這裡我們建立了一個 hypotenuses 函數,而其包含兩個正式參數,分別是 xy,在大括號中的程式碼就是函數的主體。

如果是像這樣只有一行程式碼的 R 函數,我們可以將大括號省略,以更簡潔的寫碼來指定函數:

hypotenuse <- function(x, y) sqrt(x ^ 2 + y ^ 2)

這樣只要一行程式碼即可建立一個 R 函數。

自訂的函數在建立之後,就可以立即使用:

hypotenuse(x = 3, y = 4)
[1] 5

在有明確指定參數名稱的狀況下,參數的順序可以任意排列:

hypotenuse(y = 4, x = 3)
[1] 5

當呼叫 R 的函數時,若沒有明確指定參數的名稱,R 就會依照輸入參數的位置來判別,所以上面的函數呼叫也可以寫成這樣:

hypotenuse(3, 4)
[1] 5

這裡的第一個參數 3 就會被指定給函數中的 x,而第二個參數 4 就會被指定給 y

在建立函數時,也可以指定每個參數的預設值:

hypotenuse <- function(x = 5, y = 12)
{
  sqrt(x ^ 2 + y ^ 2)
}

這樣一來在呼叫函數時,若沒有指定輸入的參數,R 就會使用參數的預設值進行運算:

hypotenuse()
[1] 13

formals 函數可以列出函數的每個參數以及預設值:

formals(hypotenuse)
$x
[1] 5

$y
[1] 12

formals 函數的傳回值是一個列表變數,若是要給人閱讀的話,可以改用 args 函數:

args(hypotenuse)
function (x = 5, y = 12)
NULL

formalArgs 則會傳回簡單的參數名稱:

formalArgs(hypotenuse)
[1] "x" "y"

至於函數的主體可以使用 body 來取得:

body(hypotenuse)
{
    sqrt(x^2 + y^2)
}

若要將函數主體的程式碼都轉為字串,可以搭配 deparse 使用:

deparse(body(hypotenuse))
[1] "{"                   "    sqrt(x^2 + y^2)"
[3] "}"

參數的預設值除了一般的常數值之外,也可以使用各種的 R 運算式,甚至還可以依據其他的正式參數來計算參數值,以下我們自訂一個簡單的標準化函數,他可以將一個數值向量標準化,讓平均數為 0,而標準差為 1

normalize <- function(x, m = mean(x), s = sd(x))
{
  (x - m) / s
}
x <- c(1.2, 3.5, 6.1, 4.3)
x.normalized <- normalize(x)
[1] -1.2672025 -0.1353323  1.1441731  0.2583617
mean(x.normalized)
[1] -5.551115e-17
sd(x.normalized)
[1] 1

這個自訂的標準化函數有個小問題,如果輸入的數值向量當中含有缺失值,會讓整個結果都變成缺失值:

y <- c(1.2, 3.5, 6.1, NA)
normalize(y)
[1] NA NA NA NA

會出現這樣的狀況主要是由於 meansd 若遇到缺失值,其計算的結果就會是缺失值,進而導致 normalize 的計算結果也都變成缺失值。若要修正這個問題,可以在 meansd 中加入 na.rm 參數,讓它們在計算平均數、以及標準差時,將缺失值排除:

normalize <- function(x,
  m = mean(x, na.rm = na.rm),
  s = sd(x, na.rm = na.rm),
  na.rm = FALSE)
{
  (x - m) / s
}
normalize(y, na.rm = TRUE)
[1] -0.97898042 -0.04079085  1.01977127          NA

這裡我們在 normalize 函數中加入一個 na.rm 這個正式參數,並且讓這個參數直接傳入 meansd 函數中,所以當我們在呼叫 normalize 函數值,將 na.rm 設定為 TRUE,這個設定就會自動套用至 meansd 函數中。

雖然缺失值的問題解決了,但這樣的寫法非常冗長,而且如果遇到還有其它的參數也需要這樣傳遞的話,就更麻煩了,對於這種只用於參數列內部傳遞的參數,R 提供了一個簡單的 ... 寫法,所有使用名稱或位置都無法匹配的參數都會被納入其中,直接傳遞給參數列內部的函數:

normalize <- function(x,
  m = mean(x, ...),
  s = sd(x, ...), ...)
{
  (x - m) / s
}
normalize(y)
[1] NA NA NA NA
normalize(y, na.rm = TRUE)
[1] -0.97898042 -0.04079085  1.01977127          NA

這裡 normalize 函數中的 na.rm 參數並沒有在 normalize 函數的正式參數當中(既不是 xm 也不是 s),所以就會被納入 ... 中,而在呼叫 mean(x, ...) 這樣有包含 ... 的函數時,na.rm 就會被傳入,也就是相當於 mean(x, na.rm = TRUE)

函數的傳遞與使用

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 程式時,應該盡可能將所有需要使用到的變數都透過參數來傳遞,不要使用全域變數的方式來取得,以降低程式出錯的機率與維護的成本。