使用高階函數

上一節討論了閉包,即定義在父函數中的函數。接下來要討論的是高階函數,也就是
將另外一個函數做爲參數的函數。
在進入這個專題以前,咱們須要先了解一下,當一個函數做爲變量或參數傳遞給另外一
個函數時的具體運行方式。
1.爲函數建立別名
首先要考慮的問題是:若是將一個現有的函數賦給一個變量,會影響函數的封閉環境
嗎?若是會影響的話,那麼非局部定義的符號的搜索路徑就會不一樣的。
如下代碼演示了爲何一個函數被賦給另外一個符號時,其封閉環境不會改變。咱們定
義一個簡單函數 f1( ),調用該函數會輸出其執行環境、封閉環境和調用環境。而後定義
另外一個輸出 3 個環境的函數 f2( ),不一樣的是,f2( ) 先將 f1( ) 賦值給一個局部變量 p,
而後在 f2( ) 的內部調用它。
若是 p<- f1 局部定義了一個函數,那麼 p 的封閉環境就是 f2( ) 的執行環境。否
則,其封閉環境還是定義 f1( ) 的環境 —— 全局環境:
f1 <- function() {
cat("[f1] executing in ")
print(environment())
cat("[f1] enclosed by ")
print(parent.env(environment()))
cat("[f1] calling from ")
print(parent.frame())
}
f2 <- function() {
cat("[f2] executing in ")
print(environment())
cat("[f2] enclosed by ")
print(parent.env(environment()))
cat("[f2] calling from ")
print(parent.frame())
p <- f1
p()
}
f1()
## [f1] executing in <environment: 0x000000001435d700>
## [f1] enclosed by <environment: R_GlobalEnv>
## [f1] calling from <environment: R_GlobalEnv>
f2()
## [f2] executing in <environment: 0x0000000014eb2200>
## [f2] enclosed by <environment: R_GlobalEnv>
## [f2] calling from <environment: R_GlobalEnv>
## [f1] executing in <environment: 0x0000000014eaedf0>
## [f1] enclosed by <environment: R_GlobalEnv>
## [f1] calling from <environment: 0x0000000014eb2200>
咱們依次調用這兩個函數,發現 p 是從 f2( ) 的執行環境中調用,但它的封閉環境沒
有變。換句話說,p 和 f1( ) 的搜索路徑徹底相同。事實上,p<-f1 這個語句就是用 p 表
示函數 f1( ),它們指向徹底相同的函數。
2.將函數看成變量使用
R 中的函數不像其餘編程語言的函數那樣特殊,一切事物都是對象。函數也不例外,
而且能夠經過變量來引用。
假設咱們有這樣一個函數:
f1 <- function(x, y) {
if (x > y) {
x + y
} else {
x - y
}
}
在上面這個函數中,兩個條件分支分別轉向不一樣的表達式,所以結果也可能不一樣。爲
了達成相同的目標,咱們也能夠將條件分支轉向不一樣的函數,將結果存儲在一個變量中,
最後調用該變量所表明的函數得到最終結果:
f2 <- function(x, y) {
op <- if (x > y) `+` else `-`
op(x, y)
}
須要注意到的是,在 R 中,咱們所作的一切都是由函數完成的。最基本的運算符 + 和 −
也是函數。它們也能夠賦給變量 op,若是 op 確實是一個函數的話,即可以調用它。
3.將函數看成參數傳遞
前面的例子代表,咱們能夠像傳遞其餘對象同樣輕鬆地傳遞函數,包括在參數中傳遞
函數。
在下面的例子中,咱們定義兩個函數 add( )和 product( ):
add <- function(x, y, z) {
x + y + z
}
product <- function(x, y, z) {
x * y * z
}
252 第 9 章 元編程
而後再定義一個函數 combine( ),以參數 f 指定的某種方式將 x、y 和 z 組合起來。
這裏,咱們假設 f 是一個函數,而且在調用時須要 3 個參數。這樣 combine( ) 函數就會
更靈活。它沒有限定必須以某種特定的方式來組合輸入,而是容許用戶指定組合方式:
combine <- function(f, x, y, z) {
f(x, y, z)
}
咱們能夠將剛纔定義的函數 add( )和 product( ) 傳遞給 combine( ),看看是否
能夠運行:
combine(add, 3, 4, 5)
## [1] 12
combine(product, 3, 4, 5)
## [1] 60
當咱們調用 combine(add, 3, 4, 5) 時,函數體包含 f = add 和 f(x, y, z),
即 add(x, y, z)。一樣的邏輯也適用於將 product 傳遞給 combine( ) 函數。因爲
第 1 個參數接收了一個函數,所以 combine( ) 是一個高階函數。
咱們須要高階函數的另外一個緣由是,它使代碼在更高的抽象化層次下讀寫起來更容易。
在許多狀況下,使用高階函數會使代碼更短,表達力更強。例如,for 循環就是一個沿着
向量或列表迭代的普通的控制流工具。
假設咱們須要將一個名爲 f 的函數應用在向量 x 的每一個元素上。若是函數自己是向量
化的,就能夠直接調用 f(x)。然而並非每一個函數都支持向量化操做,也不是全部函數
都須要向量化。若是想完成上述操做,運用下面的 for 循環就能夠了:
result <- list()
for (i in seq_ _along(x)) {
result[[i]] <- f(x[[i]])
}
result
在上面這個循環中,seq_along(x) 生成從 1 開始,與 x 等長的整數序列,效果等價
於 1:length(x)。代碼看起來很簡單且容易實現,但若是一直使用它,缺點就很明顯了。
假設每次迭代中的運算都很複雜,那麼 for 循環就會變得難以理解。仔細想一想,咱們
就會發現這段代碼只是告訴 R 怎樣完成任務,而不是這個任務是什麼。當看到一段很長,
有時還包含嵌套的循環時,就很難搞明白它到底在幹什麼。
9.1 函數式編程 253
相反,咱們能夠經過調用 lapply( ) 將一個函數(f)做用於向量或列表(x)的每
個元素上,前面的章節也介紹過 lapply( ) 函數:
lapply(x, f)
實際上,lapply( ) 函數和下列代碼是等價的,儘管下列代碼是在 C 語言中實現的:
lapply <- function(x, f, ...) {
result <- list()
for (i in seq_ _along(x)) {
result[[i]] <- f(x[i], ...)
}
}
這個函數就是一個高階函數,由於它在更高的抽象層級上工做。儘管在函數內部仍然
使用了一個 for 循環,它卻將工做分紅了兩個抽象層級,這樣每一個層級看起來都很簡單。
事實上,lapply( ) 一樣支持含有額外參數的 f。例如, + 有兩個參數,如如下代碼所示:
lapply(1:3, `+`, 3)
## [[1]]
## [1] 4
##
## [[2]]
## [1] 5
##
## [[3]]
## [1] 6
上面的代碼等價於:
list(1 +3, 2 +3, 3 +3)
也等價於使用閉包生成 x+3 函數的狀況:
lapply(1:3, addn(3))
## [[1]]
## [1] 4
##
## [[2]]
## [1] 5
##
## [[3]]
## [1] 6
正如咱們在前面章節中提到的,lapply( ) 函數返回一個列表。若是想要以向量形
式返回,可使用 sapply( ) 函數:
sapply(1:3, addn(3))
## [1] 4 5 6
或者,使用 vapply( ) 函數並加上類型檢驗:
vapply(1:3, addn(3), numeric(1))
## [1] 4 5 6
除了這些函數, R 還提供了一些其餘 apply 函數族,以及 Filter( )、Map( )、
Reduce( )、Find( )、Position( ) 和 Negate( ) 函數。詳情請參閱幫助文檔: ?Filter 。
此外,使用高階函數不只使代碼可讀性更高、表達力更強,還將每一個抽象層級的實現
分離,使它們彼此獨立。相比於改進整合在一塊兒的邏輯束,改進其簡單的部分要容易得多。
例如,給定一個函數,咱們能夠用 apply 函數族來執行向量映射。若是每次迭代都相
互獨立,就能夠用多核 CPU 進行並行計算,同時執行更多任務。可是,若是一開始沒有使
用高階函數,而是用 for 循環,則須要花費一段時間才能將其轉換爲並行代碼。
例如,假設咱們使用 for 循環獲取結果,每次迭代都執行計算繁重的任務。即便發現
每次迭代都相互獨立,也不是總能直接將其轉換爲並行代碼:
result <- list()
for (i in seq_ _along(x)) {
# heavy computing task
result[[i]] <- f(x[[i]])
}
result
可是,若是咱們使用高階函數 lapply( ),事情就簡單多了:
result <- lapply(x, f)
只須要一個很小的改動,就能將原代碼轉換爲並行代碼。使用 parallel::mclapply( ),
就能夠利用多核計算將 f 做用到 x 的每一個元素上:
result <- parallel::mclapply(x, f)
不幸的是,mclapply( ) 不支持 Windows 操做系統。在 Windows 操做系統下須要更
多代碼來執行並行應用函數。編程

相關文章
相關標籤/搜索