# Functional Programming in C 考慮到以下採用 [Functional Programming](https://en.wikipedia.org/wiki/Functional_programming) 風格開發的 singly-linked list 程式: ```Clike #include <stdint.h> #include <stdio.h> #include <stdlib.h> /* integer linked list type */ typedef struct __int_list const *const int_list_t; static int_list_t const Nil = NULL; /* empty list */ /* singly linked list element */ typedef struct __int_list { int_list_t next; int32_t const val; } node_t; /* A pointer to where we are about to store the result of computation */ typedef void *const CPS_Result; /* prototypes for the continuations */ typedef void (*MakeListCallback)(int_list_t list, CPS_Result res); void make_list(uint32_t const arr_size, int32_t const array[], int_list_t lst, MakeListCallback const cb, CPS_Result res); typedef void (*ReversedListCallback)(int_list_t rlist, CPS_Result res); void reverse(int_list_t list, int_list_t rlist, ReversedListCallback const cb, CPS_Result res); typedef void (*VoidMappable)(int32_t const val); void void_map_array(VoidMappable const cb, uint32_t const size, int32_t const *const arr); void list2array(int_list_t list, CPS_Result res); /* reverse a list and then store it in an array */ void reverse_toarray(int_list_t list, CPS_Result res) { reverse(list, Nil, list2array, res); } static void print_val(int32_t const val) { printf("%d ", val); } #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) int main(int argc, char *argv[]) { int32_t arr[] = {99, 95, 90, 85, 80, 20, 75, 15, 10, 5}; uint32_t const arr_size = ARRAY_SIZE(arr); int32_t res[arr_size]; /* call make_list and pass reverse_toarray as the "continuation" */ make_list(arr_size, arr, Nil, reverse_toarray, res); /* print out the reversed array */ void_map_array(print_val, arr_size, res); printf("\n"); return 0; } /* construct a linked list from an array */ void make_list(uint32_t const arr_size, int32_t const arr[], int_list_t lst, MakeListCallback const cb, CPS_Result res) { if (!arr_size) { cb(lst, res); return; } make_list(arr_size - 1, arr, &(node_t){.val = arr[arr_size - 1], .next = lst}, cb, res); } /* transform a linked list into an array */ void list2array(int_list_t list, CPS_Result res) { if (Nil == list) return; int32_t *array = res; array[0] = list->val; list2array(list->next, array + 1); } void reverse(int_list_t list, int_list_t rlist, /* reversed list */ ReversedListCallback const cb, CPS_Result res) { if (Nil == list) { cb(rlist, res); return; } reverse(list->next, &(node_t) K1, cb, res); } /* iterate over an array and performs action cb on each element */ void void_map_array(VoidMappable const cb, uint32_t const size, int32_t const *const arr) { if (!size) return; cb(arr[0]); void_map_array(cb, K2, K3); } ``` 程式輸出為 `5 10 15 75 20 80 85 90 95 99`,也就是將輸入的數值順序予以反轉,過程中不需要 malloc/free,請補完上述程式碼。 K1 = ? * `(a)` {.val = list->val, .next = list} * `(b)` {.val = list->val, .next = rlist} * `(c)` {.val = rlist->val, .next = list} * `(d)` {.val = rlist->val, .next = rlist} K2 = ? * `(a)` size - 1 * `(b)` size * `(c)` size + 1 K3 = ? * `(a)` arr - 1 * `(b)` arr * `(c)` arr + 1 :::success 延伸題目: 1. 解釋上述程式碼的運作,並且透過修改和實驗,理解 functional programming (FP) 風格; 2. 比照 FP,改寫上述程式碼,實作出 quick sort,同樣不得配置 heap memory (也就是不得用 malloc/free); 3. 測試這樣的 integer list 和 non-intrusive linked list 的效能差異; ::: #### 1. 解釋上述程式碼的運作,並且透過修改和實驗,理解 functional programming (FP) 風格 首先必須先瞭解一下程式運作的邏輯,先看一下 reverse() 這個 function。他會將傳進去的 singly linked list 反轉後將結果轉成 array 存在 main() 中先宣告好的 res 中。而他的過程是透過巧妙的遞迴實作完成,下面以圖示說明: * 第一次呼叫時將原始的 lst 傳進去,紅色部分為指標處。 ### lst: ```graphviz digraph foo { rankdir=LR; node [shape=record]; a [label="{ <data> 99 | <ref> }", width=1.2, color=blue] b [label="{ <data> 95 | <ref> }"]; c [label="{ <data> 90 | <ref> }"]; d [label="..." shape=box]; e [label="{ <data> 5 | <ref> }",width=1.2]; a:ref:c -> b:ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2]; b:ref:c -> c:ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false]; c:ref:c -> d:ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false]; d->e :ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false]; } ``` * 進入遞迴第一次時將 list->next 放在第一個參數傳了進去,而比較特別的地方是的第二個參數的部分直接將一個 node_t 型態的 linked list 初始化為 val = 目前紅色指標處的 value,next = NULL (rlist 一開始為 NULL),也就是下一層遞迴裡的 rlist。 ### list: ```graphviz digraph foo { rankdir=LR; node [shape=record]; a [label="{ <data> 95 | <ref> }", width=1.2, color=red] b [label="{ <data> 90 | <ref> }"]; c [label="{ <data> 85 | <ref> }"]; d [label="..." shape=box]; e [label="{ <data> 5 | <ref> }",width=1.2]; a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; c:ref:c -> d [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; d->e } ``` ### rlist: ```graphviz digraph foo { rankdir=LR; node [shape=record]; a [label="{ <data> 99 | <ref> }", width=1.2, color=red] b [label="{ NULL }"]; a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; } ``` * 進第二層遞迴時又將 list->next 傳了進去,第二項參數的 rlist 的 val 初始化為 list->val ,也就是 95,next 再接回去原來傳進來的 rlist,就會變成下圖: ### list: ```graphviz digraph foo { rankdir=LR; node [shape=record]; a [label="{ <data> 90 | <ref> }", width=1.2, color=red] b [label="{ <data> 85 | <ref> }"]; c [label="{ <data> 80 | <ref> }"]; d [label="..." shape=box]; e [label="{ <data> 5 | <ref> }",width=1.2]; a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; c:ref:c -> d [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; d->e } ``` ### rlist: ```graphviz digraph foo { rankdir=LR; node [shape=record]; a [label="{ <data> 95 | <ref> }", width=1.2, color=red] b [label="{ <data> 99 | <ref> }"]; c [label="{ NULL }"]; a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; } ``` * 一直到最後 rlist 就會是 list 的反轉了: ### rlist: ```graphviz digraph foo { rankdir=LR; node [shape=record]; a [label="{ <data> 5 | <ref> }", width=1.2, color=red] b [label="{ <data> 10 | <ref> }"]; c [label="..." shape=box]; d [label="{ <data> 99 | <ref> }"]; e [label="{ NULL }"]; a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; c:ref:c -> d:data [arrowsize=1.2]; d:ref:c -> e [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; } ``` * 終止條件成立時會呼叫一開始傳進去的 list2array() 並將 rlist 轉成 array 存在 res 中,完成 function。 * 故第一題的 K1 就是 ‘{.val = list->val, .next = rlist}’ , 而 void_map_array 是將 array 內的 data 印出來,K2 為遞迴終止條件的變數,故應為 size-1,K3 等同於 arr[i++] 的功能,因此為 arr+1。 * 一開始在看這份 code 時覺得很不習慣,所有的事情都從遞迴以及傳遞參數之間完成,每個功能都拆開來撰寫,上網搜尋了一下 [Functional Programming](https://en.wikipedia.org/wiki/Functional_programming) 各類相關資料,發現大致上有幾個特點: * 一個運算用的 function 大多只包含條件式以及遞迴呼叫。 * [higher-order functions](https://en.wikipedia.org/wiki/Higher-order_function) 主要有兩個特點: * function 可當作參數傳入 function 之中。 * function 可以 return function. * [lazy evaluation](https://en.wikipedia.org/wiki/Lazy_evaluation) :當變數被定義時不會立即 evaluate,而是等待要使用時才 evaluate 並儲存已計算的結果,所以不會重複 evaluate 同一個定義。 * 不支援 flow Controls,像是迴圈以及 If-Else 條件判斷式,只使用 function 及 function 呼叫。 [Side Effect](https://en.wikipedia.org/wiki/Side_effect_(computer_science)) :::warning 關於 FP,可對照看之前報告 [MapReduce with POSIX Thread](https://hackmd.io/s/Hkb-lXkyg),注意裡頭引用的論文 :notes: jserv ::: #### 2. 比照 FP,改寫上述程式碼,實作出 quick sort,同樣不得配置 heap memory (也就是不得用 malloc/free) 回憶一下 [quicksort](http://notepad.yehyeh.net/Content/Algorithm/Sort/Quick/Quick.php) 演算法的流程: * 選定數列中的一個值當作 pivot ,而選擇的方法可以是隨機選,也可以是挑出陣列的第一項,當然也可以使用 [median of medians](https://en.wikipedia.org/wiki/Median_of_medians) 確保每次選的 pivot 都是接近中位數的數,以降低 quicksort 遞迴次數。 * 從數列的頭到尾對每個數比較,若大於 pivot 則將其置於右方,剩下的則置於 pivot 左方。 * 對左右兩邊的數列分別都再做上述步驟(也就是遞迴),一直到數列個數為 0 或 1 時結束遞迴。 開始實作演算法的部份,由於 functional progamming 的程式碼有一個很大的特色是:當一個變數建立出來後就不可以在更動他的值,在 C語言中也就是 const 型態的變數,這樣的限制也讓程式碼寫起來相當的不習慣。 我們先嘗試寫了一個 partition 的function: ```clike __list_strct partition( int_list_t cur, int_list_t llist, int_list_t pivot, int_list_t rlist) { if(Nil == cur) { __list_strct r = {.left = llist, .right = rlist}; return r; } /* Put the numbers which are larger than pivot into right. */ if(cur->val > pivot->val) { return partition(cur->next, llist, pivot, &(node_t){.val = cur->val, .next = rlist}); } return partition(cur->next, &(node_t){.val = cur->val, .next = llist}, pivot, rlist); } ``` 回傳值的型態是一個包含兩個 list 指標的 struct,目的是要呼叫 partition 這個 function 後可以拿到兩個分完堆的 list。 ```clike typedef struct list_strct { int_list_t left; int_list_t right; } __list_strct; ``` 然而這樣的作法出現了問題,儘管我們在 partition() 內測試了 function 內容都確定沒寫錯,但當 function return 後做下一步的執行就會出現難以預料的結果或是 segmentation fault,連多一個 printf 都會讓結果截然不同,發現我們犯了一個錯誤。 先觀察一下這行程式碼: ```clike /* integer linked list type */ typedef struct __int_list const *const int_list_t; ``` 他的意思是定義了一個 int_list_t 的宣告方式,他可以宣告一個本身是 const 的指標並且指向一個 const 的變數。 也就是說如果我們將他當作一個 function 的宣告,則這個 function 就可以被視為是一個 const 的變數,也就是回傳的結果必須固定不變,然而如果每次迴傳都式不同的變數時就會發生錯誤。這裡也讓我們想起 functional programming 的初衷,因此繼續修改程式碼。 :::danger 補完程式碼! :notes: jserv ::: 11/12更新 ### 關於functional programming的程式設計風格 簡單來說,其實和數學的函式有一些關係,亦即在我們的輸入和輸出都不會被干擾的情況下,輸入同一個函式會出現相同的規律,例如:5輸入一個f(x)的函式得到的數值是10,那麼輸入一個15的數值就要得到30,而不是其他數值。而且不會受到side effect的影響,若我們過程中想要改變我們的輸入是不行的,除非我們使用copy(call by value)的方式,但設想一個問題,如果今天我們要將一個內含26個元素的陣列其中一個元素作更改,可行辦法有複製一整個陣列後,然後再做更改。但這種作法會增加記憶體空間和降低效率,但若是透過一些資料結構,例如:tree或是link list等等....結構 而且functional programming是一旦執行了 就算有比它執行更快的函式出現 它依舊會跑完 不受影響。 :::info 搭配閱讀: [C 語言返回堆疊空間的結構體](http://www.zenlife.tk/return-struct-without-malloc.md) 需要釐清大量 callback 之後的記憶體配置方式 :notes: jserv ::: ### 程式邏輯