再談函數和一等公民

本文首發自本人博客 eczn.github.io/blog/cc2509… 如下是原文:git

寫篇文章再談談函數和一等公民,由於我發現了些有趣的東西。github

先前想在本身的 函數式方言解釋器 裏實現 元組 這種數據結構,可是沒有什麼方向,就去看了下 Scheme 的語法,看了下 Wiki,而後不知不覺間,看到了用 Lisp 實現 Pair。typescript

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
複製代碼

以上代碼完美的體現了函數是一等公民這個概念,由於咱們僅利用了函數去實現數據結構,這纔是一等公民背後的意義。ui

挑戰:函數式鏈表

如今,咱們有了 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 這方面我學習的不深。。。。不展開了。

相關文章
相關標籤/搜索