---
tags: DYKC
---
# Functional Programming 風格的 C 語言實作
contributed by < `jserv`, `JEFF1216`, `brian208579`, `happyincent` >
## 何謂 Functional Programming?
[Functional Programming](https://en.wikipedia.org/wiki/Functional_programming) (以下簡稱 FP) 是種 programming paradigm (開發典範),不是 design pattern 也不是 framework,更不是 language。簡單來說,FP 是種以數學函數為中心的「思考方式」與「程式風格」。
[Why Functional Programming Matters](http://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) 是篇 1984 年的論文,在 1990 年做小幅度修改,主要凸顯出 FP 的優點與重要特質。
* 中文翻譯:[函數式程式設計為什麼至關重要](https://www.byvoid.com/zhs/blog/why-functional-programming)
一般來說,FP 的幾個特點如下:
* 運算用的 function 大多只包含條件式及遞迴呼叫;
* [higher-order functions](https://en.wikipedia.org/wiki/Higher-order_function) 主要有兩個特點:
* function 可當作參數傳入 function 之中
* function 可以回傳 function
* 沒有隱含 (implicit) 的 [Side Effect](https://en.wikipedia.org/wiki/Side_effect_(computer_science)) (副作用)
* 將程式語言的函式視作看作是數學上的函數,乍看似乎沒問題,但副作用的存在不能保證每次呼叫的時候,傳回來的東西都會一樣,也就是程式本身,並非獨立於程式語言其他的部份。所以當程式出錯的時候,開發者需要走訪所有觸及的程式碼,方可確立問題的來源;
* 在 [Haskell](https://www.haskell.org/) 等純函數語言,副作用是必須「明確」地用型別標示出來,而不是隱含的。副作用本身不必然是 primitive 的操作,可以是由語言的其他構件組成。而如 OCaml 這樣的 FP 語言,甚至提供 (explicit) reference 這樣的型別。所以每次呼叫函數,總可以保證傳回的數值不會變,也因此要寫對程式,也變得比較容易,或者說,較難寫程式但可確保容易寫出對的程式;
* [lazy evaluation](https://en.wikipedia.org/wiki/Lazy_evaluation)
* 當變數被定義時,不會立即 evaluate,而是等待要使用時才 evaluate 並儲存已計算的結果,所以不會重複 evaluate 同一個定義
* 因為沒有副作用,所以可以把計算過的值記起來,以便下次再呼叫的時候直接拿來用,也就是 lazy evaluation 的精神。
延伸閱讀: [Functional Programming: Concept & Example](https://hackmd.io/p/rJV1zI6jX/)
程式語言引入函式傳遞是縮減複雜度的手法之一,然而,很多時候很難阻止開發者傳遞幾百或幾千行的函式內容。於是,程式語言的設計者則轉向焦點,思索如何規範開發者撰寫出意圖單一、實作單純的簡明函式,這即是不可變(immutable) 特性。當開發者無法任意更動變數內含值或物件狀態,便只能從既有變數值或物件狀態,來產生新的數值或物件狀態,這不啻貫徹數學上的數學函數嗎?換言之,開發者會將大任務切割為若干小任務,於是可察覺小任務的流程重複性,一旦開發者封裝重複的小任務,隨後即可在呼叫它們時,只專注在真正該關心的運算,並以函式傳遞運算。
若函式實作時貫徹不可變特性,該函式就會被稱為純函式 (pure function),相對地,就會是具有副作用的函式。值得留意的是,即便在 [Haskell](https://www.haskell.org/) 一類純函數式語言中,也是有副作用的部份,不然就無法接受外界輸入,也不能進行程式對外的輸出。純函數式程式設計規範的是,副作用函式與純函式有個明確的界線,一邊是完全純粹的世界,不純的世界是另一邊;副作用本身對電腦上執行的程式是必要的特徵,它並非萬惡,真正令開發者畏懼的是,開發者搞不清楚這函式是否有副作用,進一步導致他們在不知道的地方改變什麼。
近年 FP 被重新提出且廣泛應用,是基於以下考量:
1. 硬體單核處理器時脈出現瓶頸,中央處理器的運作時脈已經很難超過 5GHz,因此單執行緒程式速度也面臨瓶頸,但隨著半導體製程進步,單位面積可以塞入更多電晶體,因此處理器可納入更多核 (即 multi-core)。典型 imperative 寫法依賴 [side effect](https://en.wikipedia.org/wiki/Side_effect_(computer_science)),使得多核很難發揮效益,但 FP 沒有 side effect,很容易藉由平行運算,充分運用硬體能力;
2. side effect 寫法很難單元測試,因為不保證每次的結果都ㄧ樣,但 FP 的 pure function 很適合單元測試
3. OOP 的 object composition 需依賴 interface,容易造成 interface 混亂與 over design,但 FP 的 function compostion 不需要額外 interface,只要求你使用 Dataflow 思考,並使用 pure function 避免 side effect 即可
## 用 C 語言實作 FP 風格
在 FP 中,以下三種常見操作,搭配下圖食物對應:
* map(): 配合給定行為,回傳一個陣列或鏈結串列
* filter(): 用以檢索和篩選
* reduce(): 將素材混合在一塊,得到最終結果
![](https://i.imgur.com/jiZVpDx.png)
考慮 [test-functional.c](https://github.com/jserv/Functional-Programming-in-C/blob/master/test-functional.c) 的用法:
```cpp
void print_int(void *v, void *args) {
printf("[ op:%s ] %d\n", (char *) args, (int) (*(int *) v));
}
void print_char(void *v, void *args) {
static int count = 0;
printf("%c ", (char) (*(char *) v));
if (++count > 10) {
printf("\n");
count = 0;
}
}
bool isOdd(void *v, void *args) {
return (bool) (*((int *) v) & 1);
}
void *twice(void *v, void *args) {
int *o = malloc(sizeof(int));
*o = *((int *) v) * 2;
return o;
}
int main(int argc, char **argv) {
gc_init(); // initialize the garbage collector
iter(map(range(0, 10), twice, NULL), print_int,
"twice"); /* 0, 2, 4, .., 20 */
iter(filter(range(0, 10), isOdd, NULL), print_int,
"odd"); /* 1, 3, 5, 7, 9 */
iter(rangeChar('a', 'z'), print_char, NULL); /* a, b, c, ..., z */
iter(rangeChar(' ', 'z'), print_char, NULL); // won't get printed
iter(rangeChar('1', 'z'), print_char, NULL);
gc_collect();
exit(0);
}
```
執行輸出為:
```
[ op:twice ] 0
[ op:twice ] 2
[ op:twice ] 4
[ op:twice ] 6
[ op:twice ] 8
[ op:twice ] 10
[ op:twice ] 12
[ op:twice ] 14
[ op:twice ] 16
[ op:twice ] 18
[ op:twice ] 20
[ op:odd ] 1
[ op:odd ] 3
[ op:odd ] 5
[ op:odd ] 7
[ op:odd ] 9
a b c d e f g h i j k
l m n o p q r s t u v
w x y z 1 2 3 4 5 6 7
8 9 : ; < = > ? @ A B
C D E F G H I J K L M
N O P Q R S T U V W X
Y Z [ \ ] ^ _ ` a b c
d e f g h i j k l m n
o p q r s t u v w x y
z
```
第 25 行的 `map` 操作,將給定 [0, 10] 區間的整數都透過 `twice` 函式乘上 `2`,再透過 `print_int` 個別輸出產生的數值。
第 27 行的 `filter` 操作,依據 `isOdd` 函式的判斷,自 [0, 10] 區間篩選出奇數, 再透過 `print_int` 個別輸出篩選出來的數值。
第 29 行到 31 行則展示 map 搭配 filter,列舉出給定 ASCII 字元集的範圍,並透過 `print_char` 函式輸出。
參見其他實作:
* [Functional Programming](https://www.lucabol.com/tags/functional-programming/)
* [Functional Style Programming Using C](https://www.pluralsight.com/guides/functional-style-programming-using-c)
* [The GObject messaging system](https://developer.gnome.org/gobject/stable/chapter-signal.html)
## 案例: 反轉 linked list 元素
以下展示採用 [Functional Programming](https://en.wikipedia.org/wiki/Functional_programming) 風格開發的 singly-linked list 程式: [reverse.c](https://github.com/jserv/Functional-Programming-in-C/blob/master/reverse.c)
```cpp
#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){.val = list->val, .next = rlist},
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, size - 1, arr + 1);
}
```
精髓在於以下:
```cpp
/* integer linked list type */
typedef struct __int_list const *const int_list_t;
static int_list_t const Nil = NULL; /* empty list */
```
這是個 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,所以整體的函式執行順序是:
```
main -->
make_list -->
reverse_toarray -->
reverse -->
list2array -->
void_map_array
```
再來是建立 linked list 的部分:
這個是從陣列中的尾端建立起,亦即 `5` 是尾巴而 `99` 是頭,而在建立 linked list 過程中,lst 會將下一個結構的 next 儲存起來,並且將記憶體地址和數值複寫進下一個結構中,所以在 make_list 的順序是:
- [ ] lst
```graphviz
digraph foo {
rankdir=LR;
node [shape=record];
a [label="{ <data> 99 | <ref> }", width=1.2, color=red]
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
```graphviz
digraph foo {
rankdir=LR;
node [shape=record];
a [label="{ <data> 99 | <ref> }", width=1.2, color=red]
b [label="{ <data> 95 | <ref> }"];
c [label="..." shape=box];
d [label="{ <data> 5 | <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];
}
```
reverse 將開頭 (99) 的 next 指向一開始的 rlist (Null),再透過 rlist 儲存新的 (99) 的記憶體地址,再依序將 新的 (95) 的 next 指向新的 (99),如圖:
- [ ] 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];
}
```
```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];
}
```
直到將全部的 linked 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];
}
```
最後的 linked list 再傳回 `list2array` 並輸出最終結果。本程式的技巧除了透過陣列來建立一個 linked list,過程中也利用 `if` 的判斷式來進行遞迴並且傳給下一個副函式,例如在 `reverse` 副函式中,其終止的條件是,當 list 的記憶體地址為 NULL (也就是到最後一個 5 時) 會停止遞迴並將其中的值傳給下一個副函式:
```cpp
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);
}
```
注意到這裡的 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.
## 案例: 對 Linked List 元素進行合併排序
利用 merge sort 我們減少了程式碼中的 side effect 並且將每個副函式幾乎變成了 pure function。也就是說,每個函式我們都只用了區域變數,並讓其中的值只受到該區域的副函式影響,直到結束後傳遞結果為止。
- [ ] merge sort
merge sort 簡單來說就是將一個 linked list 拆成一個又一個獨立的結構再去作排列和重組。如圖:
![](https://i.imgur.com/mZWAlpF.png)
[mergesort.c](https://github.com/jserv/Functional-Programming-in-C/blob/master/mergesort.c)
我們運用兩指標 fast 和 slow,當 fast 不為 `NULL` 時,fast 往前讀取記憶體地址,若下個 fast 依舊不為 NULL,則 slow 和 fast 兩個指標繼續往前,直到 fast 指向 NULL 為止。如下圖:
- [ ] list
```graphviz
digraph foo {
rankdir=LR;
node [shape=record];
a [label="{ <data> 99 | <ref> }", width=1.2, color=red]
b [label="{ <data> 95 | <ref> }", width=1.2, color=red];
c [label="{ <data> 85 | <ref> }", width=1.2, color=red];
d [label="{ <data> 80 | <ref> }", width=1.2, color=red];
e [label="{ <data>. .. | <ref> }", width=1.2, color=red];
f [label="{ NULL }", width=1.2, color=red];
g [label="{ <data> slow }", width=1.2, color=black];
h [label="{ <data> fast }", width=1.2, color=black];
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, arrowsize=1.2];
c:ref:c -> d:ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
d:ref:c -> e:ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
e:ref:c -> f [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
a:ref:c -> g:data [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
b:ref:c -> h:data [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
}
```
若 fast 不為 NULL 則變成:
```graphviz
digraph foo {
rankdir=LR;
node [shape=record];
a [label="{ <data> 99 | <ref> }", width=1.2, color=red]
b [label="{ <data> 95 | <ref> }", width=1.2, color=red];
c [label="{ <data> 85 | <ref> }", width=1.2, color=red];
d [label="{ <data> 80 | <ref> }", width=1.2, color=red];
e [label="{ <data>. .. | <ref> }", width=1.2, color=red];
f [label="{ NULL }", width=1.2, color=red];
g [label="{ <data> slow }", width=1.2, color=black];
h [label="{ <data> fast }", width=1.2, color=black];
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, arrowsize=1.2];
c:ref:c -> d:ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
d:ref:c -> e:ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
e:ref:c -> f [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
a:ref:c -> g:data [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
c:ref:c -> h:data [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
}
```
若 fast 仍不為 NULL 的話則 fast 和 slow 各前進一格。
```graphviz
digraph foo {
rankdir=LR;
node [shape=record];
a [label="{ <data> 99 | <ref> }", width=1.2, color=red]
b [label="{ <data> 95 | <ref> }", width=1.2, color=red];
c [label="{ <data> 85 | <ref> }", width=1.2, color=red];
d [label="{ <data> 80 | <ref> }", width=1.2, color=red];
e [label="{ <data>. .. | <ref> }", width=1.2, color=red];
f [label="{ NULL }", width=1.2, color=red];
g [label="{ <data> slow }", width=1.2, color=black];
h [label="{ <data> fast }", width=1.2, color=black];
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, arrowsize=1.2];
c:ref:c -> d:ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
d:ref:c -> e:ref [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
e:ref:c -> f [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
b:ref:c -> g:data [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
d:ref:c -> h:data [arrowhead=dot, arrowtail=vee, dir=both, tailclip=false, arrowsize=1.2];
}
```
直到 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 來實現,從程式碼中
```cpp
typedef struct __list * list_t;
typedef struct __list {
list_t next;
} node_t;
typedef struct __ele {
int32_t const val;
list_t const list;
} ele_t;
```
我們將 `val` 和 `list` 的值宣告為 `const`,亦即無法對其值進行改變,然後將`next `宣告為一個指標的指標,圖示如下:
```graphviz
digraph foo {
rankdir=LR;
node [shape=record];
a [label="{ <data> val | <ref> list }", width=1.2, color=red]
b [label="{ <data> val | <ref> list }", width=1.2, color=red];
g [label="{ <data> next }", width=1.2, color=black];
g:ref:c -> b:ref [arrowhead=vee, arrowtail=dot, dir=both, tailclip=true arrowsize=0.5];
a:ref:c -> g:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=true, arrowsize=0.5];
}
```
- [ ] Nil
而在程式實現中,我們必須設計一個 Nil 結構參數,其目的是為了在各個函式之間呼叫,我們是以`ele_t `或是`list_t `的結構型態進行回傳資料和遞迴,所以我們無法直接利用 NULL 來作為函式中斷遞迴的判斷,因此我們令`Nil `為`static const ele_t Nil = {.val = 0, .list = &(node_t){ .next = NULL }}; ` 意思是當我們指向 Nil 時候就表示該記憶體地址指向 Null。如圖:
```graphviz
digraph foo {
rankdir=LR;
node [shape=record];
a [label="{ <data> val | <ref> list }", width=1.2, color=red]
b [label="{ <data> Nil(0) | <ref> list }", width=1.2, color=red];
c [label="{ <data> NULL }", width=1.2, color=black];
g [label="{ <data> next }", width=1.2, color=black];
g:ref:c -> b:ref [arrowhead=vee, arrowtail=dot, dir=both, tailclip=true, arrowsize=0.5];
a:ref:c -> g:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=true, arrowsize=0.5];
b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=true, arrowsize=0.5];
}
```
- [ ] container_of (ptr, type, member)
`container_of` 這樣的巨集用於 Linux 核心和一些專案中,作用是根據給定的結構體變數其中的一個成員變數的指標,來獲取指向整個結構體變數的指標。舉例來說,給定以下結構體:
```cpp
struct demo_struct {
type1 member1;
type2 member2;
type3 member3;
type4 member4;
};
```
假設結構體變數 `demo` 在執行時期的記憶體位置呈現如下圖:
```
demo
+-------------+ 0xA000
| member1 |
+-------------+ 0xA004
| member2 |
+-------------+ 0xA010
| member3 |
+-------------+ 0xA018
| member4 |
+-------------+
```
一旦執行程式碼 `container_of(memp, struct demo_struct, type3)`,會使取得的記憶體地址為該結構的初始地址,亦即 `0xA000`,而在作 merge sort 時,前述 fast 和 slow 指標必須是指向該 linked list 結構的初始記憶體地址,才可以對它作 dereference ,否則就會出現記憶體操作的錯誤,因次我們可定義`list_entry` 如下:
```cpp
#define list_entry(node, type, member) \
container_of(node, type, member)
```
例如在 `partition` 中,我們要對將其拆開時,便會使用到:
```cpp
fast = list_entry(head->list->next, ele_t, list);
```
- [ ] mergeLists
最後是關於 `mergeLists` 的實作,類似之前的程式碼,只是在於進行遞迴呼叫時,要用到 `tmp` 來當作暫存器儲存結構的記憶體地址。
```cpp=
if (a->list->next == NULL) {
return b;
} else if (b->list->next == NULL) {
return a;
}
if (a->val <= b->val) {
mergedList = a;
ele_t *tmp = mergeLists(list_entry(a->list->next, ele_t, list), b);
mergedList->list->next = (list_t)(&(tmp->list));
} else {
mergedList = b;
ele_t *tmp = mergeLists(a, list_entry(b->list->next, ele_t, list));
mergedList->list->next = (list_t)(&(tmp->list));
}
```
其中第 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下的觀察結果:
```shell
breakpoint 4, partition (head=0x7ffffffedee0, front=0x7ffffffedde0, back=0x7ffffffedde8) at mergesort.c:124
124 if (head == NULL || head->list->next == (list_t)(&(Nil.list))) {
(gdb)
129 slow = head;
(gdb)
130 fast = list_entry(head->list->next, ele_t, list);
(gdb)
132 while(fast->list->next != NULL) {
(gdb) p *head
$1 = {val = 99, list = 0x7ffffffeded8}
(gdb) p *fast
$2 = {val = 95, list = 0x7ffffffedf48}
(gdb) p *(head->list->next)
$3 = {next = 0x7ffffffedf48}
(gdb) p *(head->list)
$4 = {next = 0x7ffffffedf58}
(gdb) p &(head->list>next)
No symbol "next" in current context.
(gdb) p &(head->list->next)
$5 = (list_t *) 0x7ffffffeded8
(gdb) p (head->list)
$6 = (const list_t) 0x7ffffffeded8
(gdb) p (head->list->next)
$7 = (list_t) 0x7ffffffedf58
(gdb) p &(fast->list)
$8 = (const list_t *) 0x7ffffffedf58
```