基本概念
- 函數(shù)式編程
(Functional programming)與面向?qū)ο缶幊?code>(Object-oriented programming)和過(guò)程式編程(Procedural programming)并列的編程范式。
過(guò)程式編程毫無(wú)邊界,只關(guān)心完成目標(biāo)的具體操作步驟,這個(gè)很接近機(jī)器的指令式思維。
面向?qū)ο缶幊?,開(kāi)始有邊界了。第一層邊界是對(duì)象,有隔離,有封裝;第二層邊界是環(huán)境;
函數(shù)式編程的邊界進(jìn)一步縮小。第一層邊界是函數(shù),獨(dú)立的,純的函數(shù),不依賴外界的狀態(tài)。第二層邊界是容器(集合),從一個(gè)集合變換到另外一集合。這兩個(gè)集合是互相獨(dú)立的,只是有映射關(guān)系,而且這種映射關(guān)系是單向的,一對(duì)一或者是多對(duì)一的。 - 最主要的特征是,函數(shù)是第一等公民。所謂“一等公民”,其實(shí)就是“普通公民”。函數(shù)可以是參數(shù),可以是返回值,可以是數(shù)組的成員等等。
- 值的集合組成一個(gè)容器,或者叫范疇
category;值的變換關(guān)系叫函數(shù),可以一對(duì)一,多對(duì)一,但是不能一對(duì)多。(對(duì)于給定的輸入,有確定的輸出)。函數(shù)式編程的本質(zhì)是從一個(gè)容器變換到另外一個(gè)容器:變換的函數(shù)是容器的方法,變換的成員是容器中的成員,容器中的成員個(gè)數(shù)保持不變,或者越來(lái)越少,甚至最后變成一個(gè)(reduce)。為了簡(jiǎn)單起見(jiàn),函數(shù)的輸入?yún)?shù)是一個(gè)(curry),輸出也是一個(gè)(可能是函數(shù)),直到所有的參數(shù)都處理完,最后形成一條串行管道(pipe)。
容器和成員變換關(guān)系,是兩大基本元素。思路要轉(zhuǎn)變到這兩個(gè)焦點(diǎn)上面。 - 函數(shù)式編程有兩個(gè)最基本的運(yùn)算:合成和柯里化。
- 函數(shù)式編程要求是純函數(shù),但是現(xiàn)實(shí)的異步編程基本上都是不純的函數(shù)。所以重點(diǎn)是想辦法將不純的函數(shù)變成純的函數(shù)。
純函數(shù)
純函數(shù)的定義是,對(duì)于相同的輸入,永遠(yuǎn)會(huì)得到相同的輸出,而且沒(méi)有任何可觀察的副作用,也不依賴外部環(huán)境的狀態(tài)。
函數(shù)f的概念就是,對(duì)于輸入x 產(chǎn)生一個(gè)輸出y = f(x)。
- 純的容器(集合)的方法(函數(shù))不能改變自己所屬容器(集合)的成員,應(yīng)該返回一個(gè)新的容器(集合),其成員是變換(映射)后的結(jié)果。
比如數(shù)組的splice方法操作自身成員,是不純的;slice方法返回一個(gè)映射結(jié)果的新數(shù)組,是純的
var input = [1,2,3,4,5];
var output = [];
// Array.slice是純函數(shù),因?yàn)樗鼪](méi)有副作用,對(duì)于固定的輸入,輸出總是固定的
// 可以,這很函數(shù)式
output = input.slice(0,3);
//=> [1,2,3]
output = input.slice(0,3);
//=> [1,2,3]
// Array.splice是不純的,它有副作用,對(duì)于固定的輸入,輸出不是固定的
// 這不函數(shù)式
output = input.splice(0,3);
//=> [1,2,3]
output = input.splice(0,3);
//=> [4,5]
output = input.splice(0,3);
//=> []
- 純的函數(shù)不能依賴函數(shù)外部的變量(狀態(tài)),函數(shù)跟外部的接口只能通過(guò)參數(shù),并且參數(shù)要求是值傳遞,不能是引用傳遞(有共享的內(nèi)存,函數(shù)就依賴外部的狀態(tài)了)。
// 不純的
var minimum = 21;
var checkAge = function(age) {
return age >= minimum;
};
// 純的
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
函數(shù)組合
- 如果一個(gè)值要經(jīng)過(guò)多個(gè)函數(shù),才能變成另外一個(gè)值,就可以把所有中間步驟合并成一個(gè)函數(shù),這叫做"函數(shù)的合成"
(compose)。
var compose = function(g,f) {
return function(x) {
return g(f(x));
};
};
var toUpperCase = function(x) { return x.toUpperCase(); }; // f
var exclaim = function(x) { return x + '!'; }; // g
var shout = compose(exclaim, toUpperCase); // 先f(wàn)后g
console.log(shout("send in the clowns"));
//=> "SEND IN THE CLOWNS!"
- 函數(shù)的合成還必須滿足結(jié)合律。但是一般不滿足交換律。實(shí)際使用中這條鏈可能很長(zhǎng),結(jié)合的個(gè)數(shù)也不限于2個(gè),可以有多個(gè),具體怎么結(jié)合,可以根據(jù)具體的業(yè)務(wù)來(lái)。并且鏈路也可能有多條,交叉,但是要求單向流動(dòng),一般不能有回路。

函數(shù)組合.png
compose(h, compose(g, f))
// 等同于
compose(compose(h, g), f)
// 等同于
compose(h, g, f)
- 圖上的箭頭和順序“從左向右”,但是書(shū)寫(xiě)和執(zhí)行的順序“從右向左”,剛好相反,這點(diǎn)要注意。如果非要搞成一致,那么建議畫(huà)圖的時(shí)候,箭頭和順序也“從右向左”。沒(méi)有為什么,約定俗成罷了,習(xí)慣了就好了。
-
ABCD看成是容器(或者集合),fgh看成函數(shù)(或者映射),思維轉(zhuǎn)換過(guò)來(lái),習(xí)慣了就好了。函數(shù)(屬性,比如map)是容器(類(lèi),比如Array)的方法,作用的對(duì)象是容器的成員。只關(guān)注輸入輸出的映射關(guān)系,不關(guān)心具體的循環(huán)遍歷方式以及中間變量(比如循環(huán)序號(hào)i) -
fgh等函數(shù)只允許有一個(gè)參數(shù),簡(jiǎn)化處理;多參數(shù)的情況就把鏈路拉長(zhǎng)一點(diǎn) - 從起點(diǎn)到終點(diǎn),容器內(nèi)的成員只能保持不變或者越來(lái)越少。原因是函數(shù)的映射關(guān)系只能是一對(duì)一或者多對(duì)一,不能一對(duì)多。
Point Free
不要命名轉(zhuǎn)瞬即逝的中間變量
//這不Piont free
var f = str => str.toUpperCase().split(' ');
這個(gè)函數(shù)中,我們使用了 str 作為我們的中間變量,但這個(gè)中間變量除了讓代碼變得長(zhǎng)了一點(diǎn)以外是毫無(wú)意義的。
var compose = (f, g) => (x => f(g(x)));
var toUpperCase = word => word.toUpperCase();
var split = x => (str => str.split(x));
var f = compose(split(' '), toUpperCase);
var result = f("abcd efgh");
console.log(result);
// [ 'ABCD', 'EFGH' ]
柯里化(curry)
- 所謂"柯里化",就是把一個(gè)多參數(shù)的函數(shù),轉(zhuǎn)化為單參數(shù)函數(shù)。
- 只傳遞給函數(shù)一部分參數(shù)來(lái)調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)。形成一個(gè)調(diào)用鏈,直到處理完所有的參數(shù)。
- 也是從左向右書(shū)寫(xiě),但是從右到左執(zhí)行,將最外層的參數(shù)寫(xiě)在最左邊,最后處理。最內(nèi)層的參數(shù)寫(xiě)在最右邊,最先處理。
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
函子(Functor)
- 函子是函數(shù)式編程里面最重要的數(shù)據(jù)類(lèi)型,也是基本的運(yùn)算單位和功能單位。
- 它首先是一種范疇,也就是說(shuō),是一個(gè)容器,包含了值和變形關(guān)系。比較特殊的是,它的變形關(guān)系可以依次作用于每一個(gè)值,將當(dāng)前容器變形成另一個(gè)容器。
- 任何具有
map方法的數(shù)據(jù)結(jié)構(gòu),都可以當(dāng)作函子的實(shí)現(xiàn)。 - 一般約定,函子的標(biāo)志就是容器具有
map方法。該方法將容器里面的每一個(gè)值,映射到另一個(gè)容器。 - 函數(shù)式編程里面的運(yùn)算,都是通過(guò)函子完成,即運(yùn)算不直接針對(duì)值,而是針對(duì)這個(gè)值的容器----函子。
- 函子本身具有對(duì)外接口(
map方法),各種函數(shù)就是運(yùn)算符,通過(guò)接口接入容器,引發(fā)容器里面的值的變形。 - 學(xué)習(xí)函數(shù)式編程,實(shí)際上就是學(xué)習(xí)函子的各種運(yùn)算。函數(shù)式編程就變成了運(yùn)用不同的函子,解決實(shí)際問(wèn)題。
- 函數(shù)式編程一般約定,函子有一個(gè)
of方法,用來(lái)生成新的容器。
class Functor {
constructor(val) {
this.val = val;
}
static of(val) {
return new Functor(val);
}
map(f) {
return new Functor(f(this.val));
}
}
console.log(Functor.of(2).map(function (two) {
return two + 2;
}));
// Functor { val: 4 }
console.log(Functor.of('flamethrowers').map(function(s) {
return s.toUpperCase();
}));
// Functor { val: 'FLAMETHROWERS' }
console.log(Functor.of('bombs').map(function(s){
return s.concat(' away');
}).map(function(s) {
return s.length;
}));
// Functor { val: 10 }
- 這個(gè)
of方法這里是一個(gè)靜態(tài)方法,用類(lèi)名來(lái)訪問(wèn),替代構(gòu)造方法,將new關(guān)鍵字隱藏起來(lái)
- 這里的函數(shù)執(zhí)行順序是“從左到右”的,和示意圖流的方向一致。這個(gè)和前面函數(shù)的合成和柯里化那部分是相反的,要注意區(qū)別。
Maybe 函子
Maybe 函子是為了處理空值而設(shè)計(jì)的。簡(jiǎn)單說(shuō),它的map方法里面設(shè)置了空值檢查。
class Maybe extends Functor {
static of(val) {
return new Maybe(val);
}
map(f) {
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
}
try {
console.log(Functor.of(null).map(function (s) {
return s.toUpperCase(); // Functor沒(méi)有空值檢查,當(dāng)輸入null時(shí)拋出異常
}));
} catch (error) {
console.log(error.message);
// Cannot read property 'toUpperCase' of null
}
console.log(Maybe.of(null).map(function (s) {
return s.toUpperCase();
}));
// Maybe { val: null }
Either 函子
- 條件運(yùn)算
if...else是最常見(jiàn)的運(yùn)算之一,函數(shù)式編程里面,使用Either函子表達(dá)。 -
Either函子內(nèi)部有兩個(gè)值:左值(Left)和右值(Right)。右值是正常情況下使用的值,左值是右值不存在時(shí)使用的默認(rèn)值。
class Either {
constructor(left, right) {
this.left = left;
this.right = right;
}
static of(left, right) {
return new Either(left, right);
}
map(f) {
return this.right ?
Either.of(this.left, f(this.right)) :
Either.of(f(this.left), this.right);
}
}
// Either用來(lái)提供默認(rèn)值
var addOne = function (x) {
return x + 1;
};
console.log(Either.of(5, 6).map(addOne));
// Either { left: 5, right: 7 }
console.log(Either.of(1, null).map(addOne));
// Either { left: 2, right: null }
-
Either函子的另一個(gè)用途是代替try...catch,使用左值表示錯(cuò)誤。一般來(lái)說(shuō),所有可能出錯(cuò)的運(yùn)算,都可以返回一個(gè)Either函子。
function parseJSON(json) {
try {
return Either.of(null, JSON.parse(json));
} catch (e: Error) {
return Either.of(e, null);
}
}
ap 函子
- 容器中的成員是函數(shù)
- 這些函數(shù)是
curry函數(shù) - 包含
ap方法 -
ap方法的參數(shù)不是函數(shù),而是另一個(gè)函子 - ap 函子的意義在于,對(duì)于那些多參數(shù)的函數(shù),就可以從多個(gè)容器之中取值,實(shí)現(xiàn)函子的鏈?zhǔn)讲僮鳌?/li>
- 取ap函子中的函數(shù),參數(shù)從其他的函子中取,最后得到一個(gè)結(jié)果的集合。這個(gè)集合不是ap函子,也不是參數(shù)函子,而是結(jié)果的集合。
class Ap {
constructor(val) {
this.val = val;
}
static of(val) {
return new Ap(val);
}
map(f) {
return new Ap(f(this.val));
}
ap(F) {
return Ap.of(this.val(F.val));
}
}
class Maybe {
constructor(val) {
this.val = val;
}
static of(val) {
return new Maybe(val);
}
map(f) {
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
}
function add(x) {
return function (y) {
return x + y;
};
}
var reslut = Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
console.log(reslut);
// Ap { val: 5 }
Monad 函子
- Monad 函子的作用是,總是返回一個(gè)單層的函子。
- 它有一個(gè)
flatMap方法,與map方法作用相同,唯一的區(qū)別是如果生成了一個(gè)嵌套函子,它會(huì)取出后者內(nèi)部的值,保證返回的永遠(yuǎn)是一個(gè)單層的容器,不會(huì)出現(xiàn)嵌套的情況。 -
flatMap方法也叫chain方法,就是比普通的map多一個(gè)取值操作 - Monad 函子的重要應(yīng)用,就是實(shí)現(xiàn) I/O (輸入輸出)操作。
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
return this.map(f).join();
}
}
-
Monad,英文單詞翻譯成中文是“單子”。作用是實(shí)現(xiàn)輸入輸出都是“類(lèi)”的函數(shù)調(diào)用,最后形成一個(gè)鏈?zhǔn)秸{(diào)用。直觀的感覺(jué)就是“類(lèi)”(或者叫容器)中的方法,全部返回“類(lèi)”本身,那么就可以一直點(diǎn)點(diǎn)點(diǎn)下去,將很多函數(shù)調(diào)用寫(xiě)在一行,成為“一條鏈” - 從本質(zhì)上講,輸入輸出都是“類(lèi)”的函數(shù)是沒(méi)有意義的。值到值的變換才是函數(shù)的本質(zhì)。借助
Monad這個(gè)概念,實(shí)現(xiàn)了函數(shù)鏈?zhǔn)秸{(diào)用。首先取出傳過(guò)來(lái)的“類(lèi)”參數(shù)中的值,經(jīng)過(guò)函數(shù)運(yùn)算,得到結(jié)果,然后把這個(gè)值再“封裝”到“類(lèi)”中,返回這個(gè)類(lèi)。
圖解 Monad這篇文章很好地描述了這個(gè)過(guò)程,好好看看。
IO 操作
- I/O 是不純的操作,普通的函數(shù)式編程沒(méi)法做,這時(shí)就需要把 IO 操作寫(xiě)成Monad函子,通過(guò)它來(lái)完成。
- 讀取文件和打印本身都是不純的操作,但是
readFile和print卻是純函數(shù),因?yàn)樗鼈兛偸欠祷?IO 函子。
var fs = require('fs');
var readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
});
};
var print = function(x) {
return new IO(function() {
console.log(x);
return x;
});
}
參考文章
函數(shù)式編程入門(mén)教程
這篇博客寫(xiě)的很好,對(duì)于函數(shù)式編程概念的理解很有幫助,強(qiáng)烈推薦
JS函數(shù)式編程指南
這個(gè)都寫(xiě)成書(shū)了,應(yīng)該好好看看,寫(xiě)得很好,強(qiáng)烈推薦
JavaScript函數(shù)式編程(一)
JavaScript函數(shù)式編程(二)
JavaScript函數(shù)式編程(三)
很用心寫(xiě)的一系列文章,值得看看