本文首發自本人部落格 eczn.github.io/blog/cc2509… 以下是原文:
寫篇文章再談談函式和一等公民,因為我發現了些有趣的東西。
先前想在自己的 函式式方言直譯器 裡實現 元組
這種資料結構,但是沒有什麼方向,就去看了下 Scheme 的語法,看了下 Wiki,然後不知不覺間,看到了用 Lisp 實現 Pair。
Lisp 你可能聽過,這裡我們不需要它,但後面的 Pair 是啥,它其實是一種很簡單的資料結構,類似這樣,用 TyepScript 表示就是這樣:
// 建立一個 pair
function cons(l: number, r: number) {
return [l, r]
}
// 取出 pair 的左邊
function car(my_pair: number[]) {
return my_pair[0];
}
// 取出右邊
function cdr(my_pair: number[]) {
return my_pair[1];
}
複製程式碼
閒話一下,scheme 裡建立 pair 的函式名就是 cons,還有它的兩個操作 car 和 cdr 也是這個名字,因此本文也都用這個名字。
好吧,進入正題,形如上面這種樣子的資料結構,叫做 Pair
,在很多 js 庫裡也有它的存在,比如 React.useState 返回的是左邊是值,右邊是 dispatcher 的 pair。
但我們這裡討論的是利用了陣列去實現的,有沒有別的方法去實現這種資料結構呢?答案當然是有的啦,下文將會給出僅利用函式的方式來實現這種資料結構,以及僅用函式去實現連結串列、二叉樹。
Pair 的函式式實現
因為本人一開始是想在自己寫的的一個函式式方言的直譯器里加上 pair 這個型別的,因此看了 Pair 的函式式實現,如下:
;; 程式碼從 Wiki 摘錄
;; https://en.wikipedia.org/wiki/Cons
(define (cons x y)
(lambda (m) (m x y)))
(define (car z)
(z (lambda (p q) p)))
(define (cdr z)
(z (lambda (p q) q)))
複製程式碼
翻譯成 TypeScript 就是這樣:
// 姑且把 Pair 這種資料結構型別定義為函式
type Pair = Function;
// 建立一個 cons
function cons(x: number, y: number): Pair {
return (how_to_do_it: Function) => {
return how_to_do_it(x, y);
}
}
// 取出 pair 的左值
function car(pair: Pair) {
return pair((l, r) => l)
}
// 取出 pair 的右值
function cdr(pair: Pair) {
return pair((l, r) => r)
}
const xy = cons(1, 2);
// 呼叫 cons 返回一個函式
const x = car(xy);
// => 1
const y = cdr(xy);
// => 2
複製程式碼
以上程式碼完美的體現了函式是一等公民這個概念,因為我們僅利用了函式去實現資料結構,這才是一等公民背後的意義。
挑戰:函式式連結串列
現在,我們有了 Pair,它有兩個值,此外,這兩個值也是有序的,它可以用來構造連結串列嗎?
當然可以,因為我們沒有考慮到它有兩個值裡的值是啥。Pair 裡的左值右值,既可以是數字,也可以是另一個 Pair,也就是說,Pair 裡可以巢狀 Pair,據此可以遞迴地定義連結串列:
Pair(number, number | Pair | null) 當且僅當右值為 null 的時候,我們才認為連結串列到達了盡頭
由上面的討論可以很容易的利用 Pair 遞迴地構造出連結串列,也就是說,可以僅用函式實現連結串列。
或許你還沒理解所謂遞迴地定義連結串列是啥意思,你可以看看下面的程式碼形式,便一目瞭然:
cons(1, cons(2, cons(3))) 1 -> 2 -> 3
根據上面的討論,其 TypeScript 實現如下:
type Pair = Function;
function cons(x: number, y?: number | Pair): Pair {
return (how_to_do_it: Function) => {
return how_to_do_it(x, y);
}
}
function car(pair: Pair) {
return pair((l, r) => l)
}
function cdr(pair: Pair) {
return pair((l, r) => r)
}
// 取出連結串列的第 n 個
function nth(pair: Pair, n: number) {
if (n === 0) {
return car(pair);
} else {
return nth(cdr(pair), n - 1);
}
}
const lst = cons(1, cons(2, cons(3)))
// 1 -> 2 -> 3
const number = nth(lst, 0);
// => 1
複製程式碼
可以看到,cons 的 y 可以為空或者也是一個 pair 了,這裡存在遞迴,因此取連結串列的時候用遞迴實現比較容易。
挑戰:函式式二叉樹
上面的討論已經實現了連結串列,而連結串列裡一個最特殊的地方便是引用,如果引用變成兩個,連結串列就可以推廣成二叉樹。
以下是我的實現:
// 在把函式作為一等公民的語言裡,
// 函式是一種資料型別/結構
type Tree = Function;
// 函式式二叉樹
// 如果不指定範型,則認為樹上存的值是數字;
function Tree<T = number>(v: T, l?: Tree, r?: Tree): Tree {
return (how_to_do_it: Function) => {
return how_to_do_it(v, l, r);
}
}
// 取左子樹
function lchild(tree: Tree) {
return tree((v, l, r) => l);
}
// 取右子樹
function rchild(tree: Tree) {
return tree((v, l, r) => r);
}
// 取值
function valOf(tree: Tree) {
return tree((v, l, r) => v);
}
// 函式式二叉樹的前序遍歷
function traversal(t?: Tree) {
if (!t) return;
const l = lchild(t);
const r = rchild(t);
const v = valOf(t);
console.log(v);
traversal(l);
traversal(r);
}
// 建立一棵樹。。。
// 雖然有點繞...
const t =
Tree(1,
Tree(2,
Tree(3), Tree(4)), Tree(5, Tree(6)))
// 結構如下:
// 1
// / \
// 2 5
// / \ /
// 3 4 6
// 先序遍歷結果
traversal(t);
// => 列印:123456
複製程式碼
上面的程式碼實現了二叉樹的表示,並給出了二叉樹的先序遍歷方式。
那麼,什麼是函式呢?
我還不知道 233,不過時斷時續的學習函式式語言,每次都有不同的收穫是真。
上面的說明裡可以窺見,所謂函式是一等公民,其實意味著函式可以實現程式語言裡的其他事物,如資料結構。
這部分涉及到了 Lambda Calculus 這方面我學習的不深。。。。不展開了。