輕鬆學習 R 語言:函數型程式設計

輕鬆學習 R 語言:函數型程式設計如何以 R 實踐函數型程式設計(Functional Programming)Yao-Jen KuoBlockedUnblockFollowFollowingDec 12Photo by Devon Wilson on UnsplashR, at its heart, is a functional programming (FP) language.Hadley Wickham我們在輕鬆學習 R 語言:自訂函數探討了很多自訂函數的相關主題,也並沒有諱言 R 語言的核心其實就是函數型程式設計(Functional Programming,FP),這代表除了許多用於創建函數的知識以外,還有相應來操作函數的各種工具。雖然我們早已經知道如何使用迴圈迭代解決重複執行的任務,但這個小節將試著以函數型程式設計的視角切入,提供另一種重複執行的解決方案。解決重複執行任務的三種方案利用 R 語言解決重複執行任務是非常簡單的,原因是 R 的基本單位是資料結構(data structure)而非純量,實踐的方式有三種方案:向量運算apply() 系列函數(函數型程式設計)迴圈與迭代先以一個極度單純(Supersimple?)的例子來看,該如何將一個數列 11:20 中的每個數字都進行平方運算。## > num_seq <- 11:20## > # Solution 1: 向量運算## > num_seq**2## [1] 121 144 169 196 225 256 289 324 361 400## > # Solution 2: apply() 系列函數## > sapply(num_seq, FUN = function(x) x**2)## [1] 121 144 169 196 225 256 289 324 361 400## > # Solution 3: 迴圈與迭代## > seq_length <- length(num_seq)## > num_seq_squared <- rep(NA, times = seq_length)## > for (i in 1:seq_length) {## + num_seq_squared[i] <- num_seq[i]**2## + }## > num_seq_squared## [1] 121 144 169 196 225 256 289 324 361 400既然三種解決方案都可以達成目標,又該如何著手?我們考量的面向是程式碼的多寡與執行速度的快慢;程式碼的多寡在範例程式中已經一目瞭然,接著將數列延展長度為 100,000,再利用 system.time() 函數來衡量執行速度的快慢(所需系統時間。)## > num_seq <- rep(10, times = 100000)## > # Solution 1: 向量運算## > system.time(## + num_seq_squared <- num_seq**2## + )## user system elapsed ## 0 0 0 ## > # Solution 2: apply() 系列函數## > system.time(## + num_seq_squared <- sapply(num_seq, FUN = function(x) x**2)## + )## user system elapsed ## 0.072 0.001 0.073 ## > # Solution 3: 迴圈與迭代## > seq_length <- length(num_seq)## > num_seq_squared <- rep(NA, times = seq_length)## > system.time(## + for (i in 1:seq_length) {## + num_seq_squared[i] <- num_seq[i]**2## + }## + )## user system elapsed ## 0.015 0.001 0.016綜合考量程式碼的多寡與執行速度的快慢這兩個面向,我們很武斷地建議,倘若在三種解決方案都適用的情況下,採用的優先順序為向量運算、apply() 系列函數最後才是迴圈與迭代。如何實踐函數型程式設計採用三種方案中的第二項:apply() 系列函數來解決重複執行的任務,就被視作是一種函數型程式設計(Functional Programming),其中 Functional 可被解釋為將函數作為輸入或將函數作為輸出的手法,更具體的說明就是將自訂函數作為 apply() 系列函數的輸入之一,來將自訂函數的功用映射(apply)至指定資料結構中的每一個元素,而為了方便搭配 apply() 系列函數,有時我們會將簡單的函數省略命名,改以匿名函數作為輸入,例如像是先前舉的例子:該如何將一個數列 11:20 中的每個數字都進行平方運算。## > # 有命名的函數## > get_squared <- function(x) {## + return(x**2)## + }## > num_seq <- 11:20## > # 映射命名函數## > sapply(num_seq, get_squared)## [1] 121 144 169 196 225 256 289 324 361 400## > # 映射匿名函數## > sapply(num_seq, function(x) x**2)## [1] 121 144 169 196 225 256 289 324 361 400在程式中我們使用的 sapply() 函數是 apply() 系列函數的成員之一,這是為了因應多樣資料結構的輸入以及輸出,打造出各司其職的系列函數,像是 apply() 、 lapply() 或 sapply() 等。印製超級球星的球衣接著我們考量一個不若「將一個數列 11:20 中的每個數字都進行平方運算」這麼單純的任務:印製球星的球衣。在球衣的設計上,除了會印製背號以外,亦會印製球員的姓氏(family name);像是 LeBron James 的球衣,除了 23 號還會有「JAMES」字樣。NBA Store假如在 super_nba_stars 這個文字向量中儲存了多位超級 NBA 球星(退役或現役,)我們這時需要想辦法將每個球員的姓氏從 super_nba_stars 中取出,再把姓氏所有字母轉換為大寫(upper-cased。)## > # 超級球星## > super_nba_stars <- c("Steve Nash", "Michael Jordan", "LeBron James", "Dirk Nowitzski", "Hakeem Olajuwon")## > super_nba_stars## [1] "Steve Nash" "Michael Jordan" "LeBron James" ## [4] "Dirk Nowitzski" "Hakeem Olajuwon"這時可以使用 strsplit() 函數將每個球星的名字與姓氏分開,得到一個長度為 5 的 list 為輸出,裡面的每一筆資料都是一個長度為 2 的文字向量:索引值 1 是名字、索引值 2 是姓氏。## > # 超級球星## > super_nba_stars <- c("Steve Nash", "Michael Jordan", "LeBron James", "Dirk Nowitzski", "Hakeem Olajuwon")## > split_names <- strsplit(super_nba_stars, split = " ")## > split_names## [[1]]## [1] "Steve" "Nash" ## ## [[2]]## [1] "Michael" "Jordan" ## ## [[3]]## [1] "LeBron" "James" ## ## [[4]]## [1] "Dirk" "Nowitzski"## ## [[5]]## [1] "Hakeem" "Olajuwon"由於輸出資料結構為 list,得先排除解決重複執行任務方案中的第一種:向量運算,考慮使用 apply() 系列函數或迴圈迭代。我們先從熟悉的迴圈迭代著手,讓一個 iterator 從 list 中由 [[1]] 迭代至 [[5]] ,在每一次迭代中取出向量中的第二個資料,並以 toupper() 函數轉換為大寫後儲存至一個新的文字向量中。## > # 超級球星## > super_nba_stars <- c("Steve Nash", "Michael Jordan", "LeBron James", "Dirk Nowitzski", "Hakeem Olajuwon")## > split_names <- strsplit(super_nba_stars, split = " ")## > # Solution: 迴圈迭代## > star_jerseys <- c()## > for (i in 1:length(split_names)) {## + family_name <- split_names[[i]][2]## + star_jerseys[i] <- toupper(family_name)## + }## > star_jerseys## [1] "NASH" "JORDAN" "JAMES" "NOWITZSKI"## [5] "OLAJUWON"接著我們採用 apply() 系列函數,定義一個函數 get_star_jersey() ,這個函數的功能是取出文字向量中的第二個文字並轉換為大寫。get_star_jersey() 函數的功能並不複雜,假如我們偏好簡潔大過於可讀性,可以將這個功能以匿名函數撰寫。接著是選擇從 apply() 系列函數中選出合適成員作為 get_star_jersey() 函數或者匿名函數的輸入對象,「將函數輸入函數」聽起來像是繞口令一般,不過這確實是 Functionals 拗口的定義。認識 apply() 系列函數apply() 系列函數中第一個最該被認識的成員是 lapply() 函數,全名為 list apply,字面上的意思是接受一個函數作為輸入,並將它應用於 list 中的每筆資料,並以 list 資料結構回傳結果。## > # 取出文字向量中的第二個文字並轉換為大寫## > get_star_jersey <- function(x) {## + family_name <- x[2]## + upper_cased <- toupper(family_name)## + return(upper_cased)## + }## > ## > # 超級球星## > super_nba_stars <- c("Steve Nash", "Michael Jordan", "LeBron James", "Dirk Nowitzski", "Hakeem Olajuwon")## > split_names <- strsplit(super_nba_stars, split = " ")## > # Solution: lapply(FUN = get_star_jersey)## > star_jerseys <- lapply(split_names, FUN = get_star_jersey)## > star_jerseys## [[1]]## [1] "NASH"## ## [[2]]## [1] "JORDAN"## ## [[3]]## [1] "JAMES"## ## [[4]]## [1] "NOWITZSKI"## ## [[5]]## [1] "OLAJUWON"## ## > # Solution: lapply(FUN = 匿名函數)## > star_jerseys <- lapply(split_names, FUN = function(x) toupper(x[2]))## > star_jerseys## [[1]]## [1] "NASH"## ## [[2]]## [1] "JORDAN"## ## [[3]]## [1] "JAMES"## ## [[4]]## [1] "NOWITZSKI"## ## [[5]]## [1] "OLAJUWON"apply() 系列函數中第二組該被認識的成員是 sapply() 與 vapply() 函數,全名分別為 simplify apply 及 vector apply,字面上的意思是接受一個函數作為輸入,並將它應用於 list 中的每筆資料,並以向量資料結構回傳結果。值得注意的是 vapply() 函數中有一個難懂的 FUN.VALUE 參數,這個參數使用者必須指定預期輸出向量的型別與長度;我們必須輸入 FUN.VALUE = character(1) ,因為球星姓氏大寫是長度為 1 的文字向量。## > # 取出文字向量中的第二個文字並轉換為大寫## > get_star_jersey <- function(x) {## + family_name <- x[2]## + upper_cased <- toupper(family_name)## + return(upper_cased)## + }## > # 超級球星## > super_nba_stars <- c("Steve Nash", "Michael Jordan", "LeBron James", "Dirk Nowitzski", "Hakeem Olajuwon")## > split_names <- strsplit(super_nba_stars, split = " ")## > star_jerseys <- vapply(split_names, FUN = get_star_jersey, FUN.VALUE = character(1))## > star_jerseys## [1] "NASH" "JORDAN" "JAMES" "NOWITZSKI"## [5] "OLAJUWON" ## > star_jerseys <- sapply(split_names, FUN = get_star_jersey)## > star_jerseys## [1] "NASH" "JORDAN" "JAMES" "NOWITZSKI"## [5] "OLAJUWON"apply() 系列函數中第三個該被認識的成員是 Map() 函數,字面上的意思是接受一個函數作為輸入,並接受兩個以上的 list 作為其他輸入與參數,並以 list 資料結構回傳結果。當我們要映射的函數需要兩個以上的輸入時就會派上用場,例如將 get_star_jersey() 函數加入新的參數 n ,當我們指定 n = 1 就是取出名字(given name)轉換為大寫,指定 n = 2 則依然維持取出姓氏(family name)轉換為大寫。在以下的範例中我們輸入 n_list 作為參數 n ,因此 Michael Jordan 與 Dirk Nowitzski 會回傳姓氏大寫,而其他三位球星會回傳名字大寫。## > # 取出文字向量中的第 n 個文字並轉換為大寫## > get_star_jersey <- function(x, n) {## + name <- x[n]## + upper_cased <- toupper(name)## + return(upper_cased)## + }## > # 超級球星## > super_nba_stars <- c("Steve Nash", "Michael Jordan", "LeBron James", "Dirk Nowitzski", "Hakeem Olajuwon")## > split_names <- strsplit(super_nba_stars, split = " ")## > n_list <- list(1, 2, 1, 2, 1)## > star_jerseys <- Map(get_star_jersey, split_names, n_list)## > star_jerseys## [[1]]## [1] "STEVE"## ## [[2]]## [1] "JORDAN"## ## [[3]]## [1] "LEBRON"## ## [[4]]## [1] "NOWITZSKI"## ## [[5]]## [1] "HAKEEM"apply() 系列函數中第四個該被認識的成員是 apply() 函數,接受一個函數作為輸入,並能夠選擇映射至二維資料結構(矩陣、資料框)的列或欄,並以向量資料結構回傳結果,當 MARGIN 參數指派為 1 時表示將函數映射至矩陣或資料框的所有列,MARGIN 參數指派為 2 時表示將函數映射至矩陣或資料框的所有欄。## > # apply() 函數可以映射到矩陣或資料框## > my_mat <- matrix(11:20, nrow = 5)## > col_1 <- 11:15## > col_2 <- 16:20## > df <- data.frame(col_1, col_2)## > apply(my_mat, MARGIN = 1, FUN = sum)## [1] 27 29 31 33 35## > apply(my_mat, MARGIN = 2, FUN = sum)## [1] 65 90## > apply(df, MARGIN = 1, FUN = sum)## [1] 27 29 31 33 35## > apply(df, MARGIN = 2, FUN = sum)## col_1 col_2 ## 65 90在這個程式範例中我們創建的矩陣與資料框外觀都是 5 x 2、映射的函數是 sum(),當 MARGIN = 1 的時候會得到五個列的加總,當 MARGIN = 2 的時候會得到兩個欄的加總。小結在這個小節中我們簡介如何以函數型程式設計(Functional Programming)的思維解決需要重複執行的任務,比較解決重複執行任務的三種方案、探討如何實踐函數型程式設計、利用一個印製超級球星球衣的案例認識 apply() 系列函數,包含 lapply() 、 vapply() 、 sapply() 、 Map() 與 apply() 。延伸閱讀R FunctionsThis course will teach you the fundamentals of writing functions in R so that you can make your code more readable…www.datacamp.com. More details

Leave a Reply