contributed by < jserv
, JEFF1216
, brian208579
, happyincent
>
Functional Programming (以下簡稱 FP) 是種 programming paradigm (開發典範),不是 design pattern 也不是 framework,更不是 language。簡單來說,FP 是種以數學函數為中心的「思考方式」與「程式風格」。
Why Functional Programming Matters 是篇 1984 年的論文,在 1990 年做小幅度修改,主要凸顯出 FP 的優點與重要特質。
一般來說,FP 的幾個特點如下:
延伸閱讀: Functional Programming: Concept & Example
程式語言引入函式傳遞是縮減複雜度的手法之一,然而,很多時候很難阻止開發者傳遞幾百或幾千行的函式內容。於是,程式語言的設計者則轉向焦點,思索如何規範開發者撰寫出意圖單一、實作單純的簡明函式,這即是不可變(immutable) 特性。當開發者無法任意更動變數內含值或物件狀態,便只能從既有變數值或物件狀態,來產生新的數值或物件狀態,這不啻貫徹數學上的數學函數嗎?換言之,開發者會將大任務切割為若干小任務,於是可察覺小任務的流程重複性,一旦開發者封裝重複的小任務,隨後即可在呼叫它們時,只專注在真正該關心的運算,並以函式傳遞運算。
若函式實作時貫徹不可變特性,該函式就會被稱為純函式 (pure function),相對地,就會是具有副作用的函式。值得留意的是,即便在 Haskell 一類純函數式語言中,也是有副作用的部份,不然就無法接受外界輸入,也不能進行程式對外的輸出。純函數式程式設計規範的是,副作用函式與純函式有個明確的界線,一邊是完全純粹的世界,不純的世界是另一邊;副作用本身對電腦上執行的程式是必要的特徵,它並非萬惡,真正令開發者畏懼的是,開發者搞不清楚這函式是否有副作用,進一步導致他們在不知道的地方改變什麼。
近年 FP 被重新提出且廣泛應用,是基於以下考量:
在 FP 中,以下三種常見操作,搭配下圖食物對應:
考慮 test-functional.c 的用法:
執行輸出為:
第 25 行的 map
操作,將給定 [0, 10] 區間的整數都透過 twice
函式乘上 2
,再透過 print_int
個別輸出產生的數值。
第 27 行的 filter
操作,依據 isOdd
函式的判斷,自 [0, 10] 區間篩選出奇數, 再透過 print_int
個別輸出篩選出來的數值。
第 29 行到 31 行則展示 map 搭配 filter,列舉出給定 ASCII 字元集的範圍,並透過 print_char
函式輸出。
參見其他實作:
以下展示採用 Functional Programming 風格開發的 singly-linked list 程式: reverse.c
精髓在於以下:
這是個 const 結構指標且該指標的記憶體地址和內含值無法透過區域變數去改變,正如上述所及的 FP 精神。
再來,是關於 callback 的函式指標的理解,這是個函式指標,而 MakeListCallback
就是拿來接收 reverse_toarray 的這個函式,並且將 lst
和 res
傳給 reverse 做下一個動作,打個比方:make_list(arr_size, arr, Nil, reverse_toarray, res);
會在這行程式碼去呼叫 make_list
副函式直到執行完透過 cb(lst,res)
將做好的 linked list 開頭的記憶體地址回傳給 reverse,所以整體的函式執行順序是:
再來是建立 linked list 的部分:
這個是從陣列中的尾端建立起,亦即 5
是尾巴而 99
是頭,而在建立 linked list 過程中,lst 會將下一個結構的 next 儲存起來,並且將記憶體地址和數值複寫進下一個結構中,所以在 make_list 的順序是:
reverse 將開頭 (99) 的 next 指向一開始的 rlist (Null),再透過 rlist 儲存新的 (99) 的記憶體地址,再依序將 新的 (95) 的 next 指向新的 (99),如圖:
直到將全部的 linked list 做完反轉後才結束,如圖:
最後的 linked list 再傳回 list2array
並輸出最終結果。本程式的技巧除了透過陣列來建立一個 linked list,過程中也利用 if
的判斷式來進行遞迴並且傳給下一個副函式,例如在 reverse
副函式中,其終止的條件是,當 list 的記憶體地址為 NULL (也就是到最後一個 5 時) 會停止遞迴並將其中的值傳給下一個副函式:
注意到這裡的 CPS:
Continuation Passing Style (CPS): Programming style characterized by functions with an additional continuation argument, which call their continuation instead of returning to their caller.
利用 merge sort 我們減少了程式碼中的 side effect 並且將每個副函式幾乎變成了 pure function。也就是說,每個函式我們都只用了區域變數,並讓其中的值只受到該區域的副函式影響,直到結束後傳遞結果為止。
我們運用兩指標 fast 和 slow,當 fast 不為 NULL
時,fast 往前讀取記憶體地址,若下個 fast 依舊不為 NULL,則 slow 和 fast 兩個指標繼續往前,直到 fast 指向 NULL 為止。如下圖:
若 fast 不為 NULL 則變成:
若 fast 仍不為 NULL 的話則 fast 和 slow 各前進一格。
直到 fast 碰到 NULL
為止,則該 list 就會被分為兩個和其他,以「案例: 反轉 linked list 元素」的輸入來說,就是 (99, 90, 95, 6, 85, 20, 75, 15) 一群,(10, 5) 為另一群這樣分下去。直到變成一個獨立的 linked list 後再去連接起來。
本次的程式執行順序為:
main –> make_list –> mergesort –> partition –> partition –> … –> mergesort –> mergelist –> mergesort –> list2array –> void_map_array
採用 non-intrusive 的 linked list 來實現,從程式碼中
我們將 val
和 list
的值宣告為 const
,亦即無法對其值進行改變,然後將next
宣告為一個指標的指標,圖示如下:
而在程式實現中,我們必須設計一個 Nil 結構參數,其目的是為了在各個函式之間呼叫,我們是以ele_t
或是list_t
的結構型態進行回傳資料和遞迴,所以我們無法直接利用 NULL 來作為函式中斷遞迴的判斷,因此我們令Nil
為static const ele_t Nil = {.val = 0, .list = &(node_t){ .next = NULL }};
意思是當我們指向 Nil 時候就表示該記憶體地址指向 Null。如圖:
container_of
這樣的巨集用於 Linux 核心和一些專案中,作用是根據給定的結構體變數其中的一個成員變數的指標,來獲取指向整個結構體變數的指標。舉例來說,給定以下結構體:
假設結構體變數 demo
在執行時期的記憶體位置呈現如下圖:
一旦執行程式碼 container_of(memp, struct demo_struct, type3)
,會使取得的記憶體地址為該結構的初始地址,亦即 0xA000
,而在作 merge sort 時,前述 fast 和 slow 指標必須是指向該 linked list 結構的初始記憶體地址,才可以對它作 dereference ,否則就會出現記憶體操作的錯誤,因次我們可定義list_entry
如下:
例如在 partition
中,我們要對將其拆開時,便會使用到:
最後是關於 mergeLists
的實作,類似之前的程式碼,只是在於進行遞迴呼叫時,要用到 tmp
來當作暫存器儲存結構的記憶體地址。
其中第 13 行將回傳 (a, Nil)
給 mergeLists
。
我們假設 a 是上述示意圖 95
的結構體,b 是 90
的結構體,在比較完大小後,在 tmp = mergeLists(a, list_entry(b->list->next, ele_t, list));
中會進行遞迴呼叫,隨即將 a 結構體指標和 Nil 回傳給 mergeLists 函式,經由 if 判斷式後回傳 a 結構體指標給 tmp 最後將 90
的 next ,也就是 b->list->next 由原本的指向 Nil 改成指向 95
的結構體,以此遞迴下去完成 non-intrusive linked-list 的重組。最後再輸出該 linked list,完成此次 FP 風格的實作。
gdb下的觀察結果: