【大前端攻城獅之路】JavaScript函數式編程

  轉眼之間已入五月,本身畢業也立刻有三年了。大學計算機系的同窗大多都在北京混跡,你們爲了升職加薪,娶媳婦買房,熬夜加班跟上線,出差pk腦殘客戶。同窗聚會時有很多兄弟已經體重飆升,開始關注13號地鐵線上鋪天蓋地的植髮廣告。都說25歲是一個男人的分界線,以前是越活越精緻,日後是越活越糙。如今體會到了。父母開始老去,本身尚一無全部,攢的錢不夠買一平米的房。昨天和一哥們擼串,我問:有啥打算?哥們吞了幾口羊肉串,喝了一口啤酒,說:存點錢吧,而後回家。javascript

  說實話,我之前的想法也同樣。奈何來北京容易,想走卻很難。清明節回太原一趟,總覺的路上太過於寂靜,你們走路速度太慢,商店關門太早,居然有些許不適應。兀的發覺,北京肉體雖然天天很疲憊,但靈魂力量卻修煉的很強。回到昌平的20平出租屋內,心裏暗想,繼續混,混到混不下去爲止。css

  以前寫過一篇博文《我在百度作外包》,沒想到許多同窗深有同感。北京的外包羣體是一個很大的社會組織結構,他們須要獲得更多的關注。但生存是社會永恆的主題,外包人員更要懂得,一切以實力說話,沒有技術就沒有發言權。沒有名校背景的同窗進大廠很難,反過來講外包也是另一條路,只不過這條路須要付出更多的努力,機會永遠留給有準備的人。html

  這篇博文,做爲我僥倖從外包轉成正式百度員工的記念。前端

  這是一個系列博客,但願他能在個人碼農生涯中留下些什麼。java


  閒話很少說,這篇文章主要和你們分析下前端的函數式編程思想。綱要以下:git

  • 函數式編程思惟

  • 函數式編程經常使用核心概念

  • 當下函數式編程最熱的庫

  • 函數式編程的實際應用場景

一 函數式編程思惟

範疇論 Category Theory

•1.函數式編程是範疇論的數學分支是一門很複雜的數學,認爲世界上全部概念體系均可以抽象出一個個範疇
•2.彼此之間存在某種關係概念、事物、對象等等,都構成範疇。任何事物只要找出他們之間的關係,就能定義
•3.箭頭表示範疇成員之間的關係,正式的名稱叫作"態射"(morphism)。範疇論認爲,同一個範疇的全部成員,就是不一樣狀態的"變形"(transformation)。經過"態射",一個成員能夠變造成另外一個成員。

對於前端,全部的成員是一個集合,變形關係是函數。編程

函數式編程基礎理論

1..函數式編程(Functional Programming)其實相對於計算機的歷史而言是一個很是古老的概念,甚至早於第一臺計算機的誕生。函數式編程的基礎模型來源於 λ (Lambda x=>x*2)演算,而 λ 演算並不是設計於在計算機上執行,它是在 20 世紀三十年代引入的一套用於研究函數定義、函數應用和遞歸的形式系統。canvas

2.函數式編程不是用函數來編程,也不是傳統的面向過程編程。主旨在於將複雜的函數符合成簡單的函數(計算理論,或者遞歸論,或者拉姆達演算)。運算過程儘可能寫成一系列嵌套的函數調用設計模式

3.JavaScript 是披着 C 外衣的 Lisp。數組

4.真正的火熱是隨着React的高階函數而逐步升溫。

5.函數是一等公民。所謂」第一等公民」(first class),指的是函數與其餘數據類型同樣,處於平等地位,能夠賦值給其餘變量,也能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。

6.不可改變量。在函數式編程中,咱們一般理解的變量在函數式編程中也被函數代替了:在函數式編程中變量僅僅表明某個表達式。這裏所說的’變量’是不能被修改的。全部的變量只能被賦一次初值。

7.map & reduce他們是最經常使用的函數式編程的方法。

將上面的概念簡述一下:

1. 函數是」第一等公民」

2. 只用」表達式",不用"語句"

3. 沒有」反作用"

4. 不修改狀態

5. 引用透明(函數運行只靠參數)

二 函數式編程經常使用核心概念

•純函數

•函數的柯里化

•函數組合

•Point Free

•聲明式與命令式代碼

•核心概念

1.純函數

什麼是純函數呢?

對於相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用,也不依賴外部環境的狀態的函數,叫作純函數。

舉個栗子:

var xs = [1,2,3,4,5];// Array.slice是純函數,由於它沒有反作用,對於固定的輸入,輸出老是固定的
xs.slice(0,3);
xs.slice(0,3);
xs.splice(0,3);// Array.splice會對原array形成影響,因此不純
xs.splice(0,3);

  

2.函數柯里化

傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。

咱們有這樣一個函數checkage:

var min = 18; 
var checkage = age => age > min;

  

這個函數並不純,checkage 不只取決於 age還有外部依賴的變量 min。 純的 checkage 把關鍵數字 18 硬編碼在函數內部,擴展性比較差,柯里化優雅的函數式解決。

var checkage = min => (age => age > min);

var checkage18 = checkage(18); // 先將18做爲參數,去調用此函數,返回一個函數age => age > 18;
checkage18(20);// 第二步,上面返回的函數去處理剩下的參數,即 20 => 20 > 18; return true;

  

 再看一個例子:
// 柯里化以前
function add(x, y) {
    return x + y;
}
add(1, 2) // 3 
// 柯里化以後
function addX(y) {
    return function (x) {
        return x + y;
    };
}
addX(2)(1) // 3

  

  事實上柯里化是一種「預加載」函數的方法, 經過傳遞較少的參數,獲得一個已經記住了這些參數的新函數,某種意義上講,這是一種對參數的「緩存」,是一種很是高效的編寫函數的方法。

3.函數組合

爲了解決函數嵌套過深,洋蔥代碼:h(g(f(x))),咱們須要用到「函數組合」,咱們一塊兒來用柯里化來改他,讓多個函數像拼積木同樣。

const compose = (f, g) => (x => f(g(x)));
var first = arr => arr[0];
var reverse = arr => arr.reverse();
var last = compose(first, reverse);
last([1, 2, 3, 4, 5]); // 5

  

函數組合交換律,相似於乘法交換律:

4.Point Free

把一些對象自帶的方法轉化成純函數,不要命名轉瞬即逝的中間變量。
你們看一下下面的函數:
const f = str => str.toUpperCase().split(' ');
這個函數中,咱們使用了 str 做爲咱們的中間變量,但這個中間變量除了讓代碼變得長了一點之外是毫無心義的。
 
下面咱們用函數組合去改造一下:
var toUpperCase = word => word.toUpperCase();
var split = x => (str => str.split(x));
var f = compose(split(' '), toUpperCase);
f("abcd efgh");

  

把一些對象自帶的方法轉化成純函數,而後經過函數組合去調用,這種風格可以幫助咱們減小沒必要要的命名,讓代碼保持簡潔和通用。是否是很方便!

5.聲明式與命令式代碼

在咱們平常業務開發中,寫的代碼絕大多數都爲命令式代碼;

咱們經過編寫一條又一條指令去讓計算機執行一些動做,這其中通常都會涉及到不少繁雜的細節。
而聲明式就要優雅不少了,咱們經過寫表達式的方式來聲明咱們想幹什麼,而不是經過一步一步的指示。
//命令式
let CEOs = [];
for (var i = 0; i < companies.length; i++) {
    CEOs.push(companies[i].CEO)
}
//聲明式
let CEOs = companies.map(c => c.CEO);

  

函數式編程的一個明顯的好處就是這種聲明式的代碼,對於無反作用的純函數,咱們徹底能夠不考慮函數內部是如何實現的,專一於編寫業務代碼。優化代碼時,目光只須要集中在這些穩定堅固的函數內部便可。相反,不純的函數式的代碼會產生反作用或者依賴外部系統環境,使用它們的時候老是要考慮這些不乾淨的反作用。在複雜的系統中,這對於咱們的心智來講是極大的負擔。

6.核心概念

下面咱們再深刻一下,你們注意好好理解吸取:

高階函數

高階函數,就是把函數當參數,把傳入的函數作一個封裝,而後返回這個封裝函數,達到更高程度的抽象。

//命令式
var add = function (a, b) {
    return a + b;
};

function math(func, array) {
    return func(array[0], array[1]);
}
math(add, [1, 2]); // 3

  

遞歸與尾遞歸

指函數內部的最後一個動做是函數調用。 該調用的返回值, 直接返回給函數。 函數調用自身, 稱爲遞歸。 若是尾調用自身, 就稱爲尾遞歸。 遞歸須要保存大量的調用記錄, 很容易發生棧溢出錯誤, 若是使用尾遞歸優化, 將遞歸變爲循環, 那麼只須要保存一個調用記錄, 這樣就不會發生棧溢出錯誤了。通俗點說,尾遞歸最後一步須要調用自身,而且以後不能有其餘額外操做。
// 不是尾遞歸,沒法優化
function factorial(n) {
    if (n === 1) return 1;
    return n * factorial(n - 1);
}

function factorial(n, total) {
    if (n === 1) return total;
    return factorial(n - 1, n * total);
} //ES6強制使用尾遞歸

  

咱們看一下遞歸和尾遞歸執行過程:

遞歸:

function sum(n) {
    if (n === 1) return 1;
    return n + sum(n - 1);
}

  

sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1)))))
(5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15 // 遞歸很是消耗內存,由於須要同時保存不少的調用幀,這樣,就很容易發生「棧溢出」

  

尾遞歸

function sum(x, total) {
    if (x === 1) {
        return x + total;
    }
    return sum(x - 1, x + total);
}

  

sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14) 
15

  

整個計算過程是線性的,調用一次sum(x, total)後,會進入下一個棧,相關的數據信息和跟隨進入,再也不放在堆棧上保存。當計算完最後的值以後,直接返回到最上層的sum(5,0)。這能有效的防止堆棧溢出。 在ECMAScript 6,咱們將迎來尾遞歸優化,經過尾遞歸優化,javascript代碼在解釋成機器碼的時候,將會向while看起,也就是說,同時擁有數學表達能力和while的效能。
 

 範疇與容器

1.函數不只能夠用於同一個範疇之中值的轉換,還能夠用於將一個範疇轉成另外一個範疇。這就涉及到了函子(Functor)。
2.函子是函數式編程裏面最重要的數據類型,也是基本的運算單位和功能單位。它首先是一種範疇,也就是說,是一個容器,包含了值和變形關係。比較特殊的是,它的變形關係能夠依次做用於每個值,將當前容器變造成另外一個容器。

 容器與函子(Functor)

$(...) 返回的對象並非一個原生的 DOM 對象,而是對於原生對象的一種封裝,這在某種意義上就是一個「容器」(但它並不函數式)。

Functor(函子)遵照一些特定規則的容器類型。任何具備map方法的數據結構,均可以看成函子的實現。
Functor 是一個對於函數調用的抽象,咱們賦予容器本身去調用函數的能力。把東西裝進一個容器,只留出一個接口 map 給容器外的函數,map 一個函數時,咱們讓容器本身來運行這個函數,這樣容器就能夠自由地選擇什麼時候何地如何操做這個函數,以至於擁有惰性求值、錯誤處理、異步調用等等很是牛掰的特性。

下面咱們看下函子的代碼實現:

var Container = function (x) {
    this.__value = x;
}
// 函數式編程通常約定,函子有一個of方法
Container.of = x => new Container(x);
// Container.of(‘abcd’);
// 通常約定,函子的標誌就是容器具備map方法。該方法將容器
// 裏面的每個值, 映射到另外一個容器。
Container.prototype.map = function (f) {
    return Container.of(f(this.__value))
}
Container.of(3)
    .map(x => x + 1) //=> Container(4)
    .map(x => 'Result is ' + x); //=> Container('Result is 4')

  

class Functor {
    constructor(val) {
        this.val = val;
    }
    map(f) {
        return new Functor(f(this.val));
    }
}
(new Functor(2)).map(function (two) {
    return two + 2;
});
// Functor(4)

  

上面代碼中,Functor是一個函子,它的map方法接受函數f做爲參數,而後返回一個新的函子,裏面包含的值是被f處理過的(f(this.val))。
通常約定,函子的標誌就是容器具備map方法。該方法將容器裏面的每個值,映射到另外一個容器。上面的例子說明,函數式編程裏面的運算,都是經過函子完成,即運算不直接針對值,而是針對這個值的容器----函子。函子自己具備對外接口(map方法),各類函數就是運算符,經過接口接入容器,引起容器裏面的值的變形。
所以,學習函數式編程,實際上就是學習函子的各類運算。因爲能夠把運算方法封裝在函子裏面,因此又衍生出各類不一樣類型的函子,有多少種運算,就有多少種函子。函數式編程就變成了運用不一樣的函子,解決實際問題。

 

你可能注意到了,上面生成新的函子的時候,用了new命令。這實在太不像函數式編程了,由於new命令是面向對象編程的標誌。 函數式編程通常約定,函子有一個of方法,用來生成新的容器。
Functor.of = function (val) {
    return new Functor(val);
};
Functor.of(2).map(function (two) {
    return two + 2;
});
// Functor(4)

  

下面咱們介紹一些經常使用的函子。

Maybe 函子

函子接受各類函數,處理容器內部的值。這裏就有一個問題,容器內部的值多是一個空值(好比null),而外部函數未必有處理空值的機制,若是傳入空值,極可能就會出錯。
var Maybe = function (x) {
    this.__value = x;
}
Maybe.of = function (x) {
    return new Maybe(x);
}
Maybe.prototype.map = function (f) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}
Maybe.prototype.isNothing = function () {
    return (this.__value === null || this.__value === undefined);
}
//新的容器咱們稱之爲 Maybe(原型來自於Haskell,Haskell是通用函數式編程語言)

  

Functor.of(null).map(function (s) {
    return s.toUpperCase();
});
// TypeError

Maybe.of(null).map(function (s) {
    return s.toUpperCase();
});
// Maybe(null)

  

 錯誤處理、Either函子

咱們的容器能作的事情太少了,try/catch/throw 並非「純」的,由於它從外部接管了咱們的函數,而且在這個函數出錯時拋棄了它的返回值。Promise 是能夠調用 catch 來集中處理錯誤的。事實上 Either 並不僅是用來作錯誤處理的,它表示了邏輯或。

條件運算if...else是最多見的運算之一,函數式編程裏面,使用 Either 函子表達。Either 函子內部有兩個值:左值(Left)和右值(Right)。右值是正常狀況下使用的值,左值是右值不存在時使用的默認值

class Either extends Functor {
    constructor(left, right) {
        this.left = left;
        this.right = right;
    }
    map(f) {
        return this.right ?
            Either.of(this.left, f(this.right)) :
            Either.of(f(this.left), this.right);
    }
}
Either.of = function (left, right) {
    return new Either(left, right);
};

 

使用Either函子:

var addOne = function (x) {
    return x + 1;
};
Either.of(5, 6).map(addOne);
// Either(5, 7);
Either.of(1, null).map(addOne);
// Either(2, null);
Either
    .of({
        address: 'xxx'
    }, currentUser.address)
    .map(updateField);

  

AP函子

函子裏面包含的值,徹底多是函數。咱們能夠想象這樣一種狀況,一個函子的值是數值,另外一個函子的值是函數。
class Ap extends Functor {
    ap(F) {
        return Ap.of(this.val(F.val));
    }
}

  

function addOne(x) {
      return x + 1;
}
Ap.of(addOne).ap(Functor.of(1)) // ap函子,讓addOne能夠用後面函子中的val運算 結果爲Ap(2)

  

IO函子

真正的程序總要去接觸骯髒的世界。
function readLocalStorage(){
        return window.localStorage;
}
IO 跟前面那幾個 Functor 不一樣的地方在於,它的 __value 是一個函數。它把不純的操做(好比 IO、網絡請求、DOM)包裹到一個函數內,從而延遲這個操做的執行。因此咱們認爲,IO 包含的是被包裹的操做的返回值。
IO其實也算是惰性求值。
IO負責了調用鏈積累了不少不少不純的操做,帶來的複雜性和不可維護性
 
class IO extends Monad {
    map(f) {
        return IO.of(compose(f, this.__value))
    }
}

  

在這裏,咱們提到了Monad,Monad就是一種設計模式,表示將一個運算過程,經過函數拆解成互相鏈接的多個步驟。你只要提供下一步運算所需的函數,整個運算就會自動進行下去。Promise 就是一種 Monad。Monad 讓咱們避開了嵌套地獄,能夠輕鬆地進行深度嵌套的函數式編程,好比IO和其它異步任務。

class Monad extends Functor {
    join() {
        return this.val;
    }
    flatMap(f) {
        return this.map(f).join();
    }
}

  

關於更多的Monad介紹,能夠移步知乎什麼是Monad

三 流行的幾大函數式編程庫

1.Rxjs

響應式編程是繼承自函數式編程,聲明式的,不可變的,沒有反作用的是函數式編程的三大護法。其中不可變武功最高深。一直使用面向對象範式編程的咱們,習慣了用變量存儲和追蹤程序的狀態。RxJS從函數式編程範式中借鑑了不少東西,好比鏈式函數調用,惰性求值等等。在函數中與函數做用域以外的一切事物有交互的就產生了反作用。好比讀寫文件,在控制檯打印語句,修改頁面元素的css等等。在RxJS中,把反作用問題推給了訂閱者來解決。
在 Rxjs 中,全部的外部輸入(用戶輸入、網絡請求等等)都被視做一種 『事件流』:用戶點擊了按鈕 --> 網絡請求成功 --> 用戶鍵盤輸入 --> 某個定時事件發生 —> 這種事件流特別適合處理遊戲,上上下下上上下下舉個最簡單的例子,下面這段代碼會監聽點擊事件,每 2 次點擊事件產生一次事件響應:
var clicks = Rx.Observable
    .fromEvent(document, 'click')
    .bufferCount(2)
    .subscribe(x => console.log(x)); // 打印出前2次點擊事件

  

2.Cycle.js

Cycle.js 是一個基於 Rxjs 的框架,它是一個不折不扣的 FRP 理念的框架,和 React 同樣支持 virtual DOM、JSX 語法,但如今彷佛尚未看到大型的應用經驗。
本質的講,它就是在 Rxjs 的基礎上加入了對 virtual DOM、容器和組件的支持,好比下面就是一個簡單的『開關』按鈕:
function main(sources) {
    const sinks = {
    DOM: sources.DOM.select('input').events('click')
    .map(ev => ev.target.checked)
    .startWith(false)
    .map(toggled =>
    <div>
    <input type="checkbox" /> Toggle me
    <p>{toggled ? 'ON' : 'off'}</p>
    </div>
    )
    };
    return sinks;
    }
    const drivers = {
    DOM: makeDOMDriver('#app')
    };
    run(main, drivers);

  

3.Underscore.js

Underscore 是一個 JavaScript 工具庫,它提供了一整套函數式編程的實用功能,可是沒有擴展任何 JavaScript 內置對象。 他解決了這個問題:「若是我面對一個空白的 HTML 頁面,並但願當即開始
工做,我須要什麼?」 他彌補了 jQuery 沒有實現的功能,同時又是 Backbone 必不可少的部分。
Underscore 提供了100多個函數,包括經常使用的:map、filter、invoke— 固然還有更多專業的輔助函數,如:函數綁定、JavaScript 模板功能、建立快速索引、強類型相等測試等等。

4.Lodash.js

lodash是一個具備一致接口、模塊化、高性能等特性的JavaScript工具庫,是underscore.js的fork,其最初目標也是「一致的跨瀏覽器行爲。。。,並改善性能」。
lodash採用延遲計算,意味着咱們的鏈式方法在顯式或者隱式的value()調用以前是不會執行的,所以lodash能夠進行shortcut(捷徑) fusion(融合)這樣的優化,經過合併鏈式大大下降迭代的次數,從而大大提高其執行性能。就如同jQuery在所有函數前加全局的$同樣,lodash使用全局的_來提供對工具的快速訪問。
var abc = function (a, b, c) {
    return [a, b, c];
};
var curried = _.curry(abc);
curried(1)(2)(3);

  

function square(n) {
    return n * n;
}
var addSquare = _.flowRight(square, _.add); // 相似於上面說的函數組合
addSquare(1, 2);
// => 9

  

5.Ramdajs

ramda是一個很是優秀的js工具庫, 跟同類比 更函數式主要體如今如下幾個原則
1. ramda裏面的提供的函數所有都是curry的 意味着函數沒有默認參數可選參數從而減輕認知函數的難度。
2. ramda推崇pointfree簡單的說是使用簡單函數組合實現一個複雜功能, 而不是單獨寫一個函數操做臨時變量。
3. ramda有個很是好用的參數佔位符 R._ 大大減輕了函數在pointfree過程當中參數位置的問題相比underscore / lodash 感受要乾淨不少。
 

四 實際應用場景  

易調試、熱部署、併發

1.函數式編程中的每一個符號都是 const 的,因而沒有什麼函數會有反作用。誰也不能在運行時修改任何東西,也沒有函數能夠修改在它的做用域以外修改什麼值給其餘函數繼續使用。這意味着決定函數執行結果的惟一因素就是它的返回值,而影響其返回值的惟一因素就是它的參數。
2.函數式編程不須要考慮」死鎖"(deadlock),由於它不修改變量,因此根本不存在"鎖"線程的問題。沒必要擔憂一個線程的數據,被另外一個線程修改,因此能夠很放心地把工做分攤到多個線程,部署"併發編程"(concurrency)。
3.函數式編程中全部狀態就是傳給函數的參數,而參數都是儲存在棧上的。這一特性讓軟件的熱部署變得十分簡單。只要比較一下正在運行的代碼以及新的代碼得到一個diff,而後用這個diff更新現有的代碼,新代碼的熱部署就完成了。

單元測試

1.嚴格函數式編程的每個符號都是對直接量或者表達式結果的引用,沒有函數產生反作用。由於從未在某個地方修改過值,也沒有函數修改過在其做用域以外的量並被其餘函數使用(如類成員或全局變量)。這意味着函數求值的結果只是其返回值,而唯一影響其返回值的就是函數的參數。
2.這是單元測試者的夢中仙境(wet dream)。對被測試程序中的每一個函數,你只需在乎其參數,而沒必要考慮函數調用順序,不用謹慎地設置外部狀態。全部要作的就是傳遞表明了邊際狀況的參數。若是程序中的每一個函數都經過了單元測試,你就對這個軟件的質量有了至關的自信。而命令式編程就不能這樣樂觀了,在 Java 或 C++ 中只檢查函數的返回值還不夠——咱們還必須驗證這個函數可能修改了的外部狀態。
 

總結和補充

函數式編程不該被視爲靈丹妙藥。相反,它應該被視爲咱們現有工具箱的一個很天然的補充—— 它帶來了更高的可組合性,靈活性以及容錯性。現代的JavaScript庫已經開始嘗試擁抱函數式編程的概念以獲取這些優點。Redux 做爲一種 FLUX的變種實現,核心理念也是狀態機和函數式編程。
 
本文章介紹了純函數、柯里化、Point Free、聲明式代碼和命令式代碼的區別,你們只要記住『函數對於外部狀態的依賴是形成系統複雜性大大提升的主要緣由』以及『讓函數儘量地純淨』就好了。
而後介紹了『容器』的概念和 Maybe、Either、IO 這三個強大的 Functor。是的,大多數人或許都沒有機會在生產環境中本身去實現這樣的玩具級 Functor,但經過了解它們的特性會讓你產生對於函數式編程的意識。
 
軟件工程上講『沒有銀彈』, 函數式編程一樣也不是萬能的,它與爛大街的 OOP同樣,只是一種編程範式而已。不少實際應用中是很難用函數式去表達的,選擇OOP 亦或是其它編程範式或許會更簡單。但咱們要注意到函數式編程的核心理念,若是說 OOP 下降複雜度是靠良好的封裝、繼承、多態以及接口定義的話,那麼函數式編程就是經過純函數以及它們的組合、柯里化、Functor 等技術來下降系統複雜度,而 React、Rxjs、Cycle.js 正是這種理念的代言。讓咱們一塊兒擁抱函數式編程,打開你程序的大門!
 

參考文獻:

1.阮一峯 函數式編程入門教程

2.知乎 什麼是 Monad (Functional Programming)?

相關文章
相關標籤/搜索