JavaScript 函數(shù)式編程

來(lái)源 | https://github.com/cheogo/learn-javascript
如何實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用
var Container = function(x) {this.__value = x;}Container.of = function(x) { return new Container(x); };Container.prototype.map = function(f){return Container.of(f(this.__value))}
上述代碼實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的鏈?zhǔn)秸{(diào)用,讓我們看看如何使用它。
Container.of(3); // Container {__value: 3}Container.of(4); // Container {__value: 4}var add1 = function (num) { return num + 1 };var add2 = function (num) { return num + 2 };Container.of(3).map(add1).map(add2) // Container {__value: 6}Container.of(4).map(add2).map(add2).map(add2) // Container {__value: 10}
在這個(gè)實(shí)例中出現(xiàn)的 Container 是一個(gè)容器,通過(guò) Container.of 來(lái)實(shí)例化保存值到 this.__value 。
add1、add2 都是 純函數(shù),我們通過(guò) map 函數(shù)來(lái)操作容器內(nèi)的值,我們把 Container 看作數(shù)據(jù)結(jié)構(gòu),這種數(shù)據(jù)結(jié)構(gòu)可以通過(guò) map 操作,那么它就叫 functor。
純函數(shù)
什么是純函數(shù):純函數(shù)是這樣一種函數(shù),即相同的輸入,永遠(yuǎn)會(huì)得到相同的輸出,而且沒(méi)有任何可觀察的副作用。
比如 slice 和 splice,這兩個(gè)函數(shù)的作用并別無(wú)二致。但是我們說(shuō) slice 符合純函數(shù)的定義是因?yàn)閷?duì)相同的輸入它保證能返回相同的輸出。
而 splice 的調(diào)用卻會(huì)產(chǎn)生可觀察到的副作用,這個(gè)數(shù)組被永久地改變了。
var xs = [1,2,3,4,5];// 純的xs.slice(0,3); // => [1,2,3]xs.slice(0,3); // => [1,2,3]// 不純的xs.splice(0,3); // => [1,2,3]xs.splice(0,3); // => [4,5]
在函數(shù)式編程中,我們盡量杜絕 splice 這種會(huì)改變數(shù)據(jù)的函數(shù)。我們追求的是 slice 那種可靠的,每次都能返回同樣結(jié)果的函數(shù)。
再看另一個(gè)例子
// 不純的var num_1 = 1var add1 = function (num) { return num + num_1 };// 純的var add1 = function (num) { return num + 1 };
在不純的版本中,add1 的結(jié)果將取決于 num_1 這個(gè)可變變量的值。換句話說(shuō),它取決于系統(tǒng)狀態(tài)(system state)。因?yàn)樗肓送獠康沫h(huán)境,從而增加了認(rèn)知負(fù)荷(cognitive load)。
這種依賴狀態(tài)是影響系統(tǒng)復(fù)雜度的罪魁禍?zhǔn)祝粌H讓它變得不純,而且導(dǎo)致每次我們思考整個(gè)軟件的時(shí)候都痛苦不堪。
為什么要使用純函數(shù)呢?舉例容易看到的好處:1. 可緩存性,因?yàn)榧兒瘮?shù)對(duì)于相同的輸入有相同的輸出,所以純函數(shù)是可以緩存運(yùn)算結(jié)果的;2. 可移植性,因?yàn)椴粫?huì)受環(huán)境變量等外部狀態(tài)的影響,可以方便移植;3. 可測(cè)試性,無(wú)需配置外部變量,一個(gè)輸入一個(gè)輸出,直接斷言;等等。
有哪些不純的情況呢?
1. IO 操作,你不知道你讀取的內(nèi)容會(huì)是怎樣;
2. 接口請(qǐng)求,你確定返回的內(nèi)容是什么;
3. dom 操作,引起了副作用;
4. 甚至連 console.log 都是不純的,因?yàn)樗懈弊饔茫坏鹊取?/span>
對(duì)于不純的函數(shù)我們盡量把它控制在可控范圍內(nèi)發(fā)生,這個(gè)會(huì)在文章后面提到。
函數(shù)柯里化
什么是柯里化(curry)?curry 的概念很簡(jiǎn)單,只傳遞給函數(shù)一部分參數(shù)來(lái)調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)
簡(jiǎn)單的實(shí)例:
var add = function (x, y) { return x + y; }add(1, 2) // 3add(10, 1) // 11add(10, 2) // 12add(10, 3) // 13// curryvar add = function(x) {return function(y) {return x + y;};};var increment = add(1);var addTen = add(10);increment(2); // 3addTen(1); // 11addTen(2); // 12addTen(3); // 13
我們把 add 函數(shù)通過(guò)柯里化變成了接受部分參數(shù)并返回一個(gè)處理剩余函數(shù)且返回結(jié)果的函數(shù)。在實(shí)際環(huán)境中我們可能用到 ramda 這樣的庫(kù)來(lái)幫助我們實(shí)現(xiàn)柯里化。
var R = require('ramda');var add = function (x, y) { return x + y; }var addTen = R.curry(add)(10)addTen(1); // 11addTen(2); // 12
柯里化是函數(shù)式編程的工具,他能實(shí)現(xiàn)預(yù)加載函數(shù)、分步取值、避免重復(fù)傳參、鎖定函數(shù)運(yùn)行環(huán)境等等功能。
函數(shù)組合
這就是組合(compose)
// 簡(jiǎn)單實(shí)現(xiàn),復(fù)雜實(shí)現(xiàn)可以傳遞多個(gè)函數(shù)用于組合var compose = function(f,g) {return function(x) {return f(g(x));};};
組合多個(gè)函數(shù)生成一個(gè)新的函數(shù),并且函數(shù)從右往左運(yùn)行。
var double = function (num) { return num * 2 }var add = R.curry(function (x, y) { return x + y; })var price = compose(double, add(10)) // 通過(guò)成本獲取商品價(jià)格price(10) // 40price(20) // 60
通過(guò)函數(shù)組合我們可以,一次性的合并多個(gè)處理函數(shù),并且可以方便的改變函數(shù)的執(zhí)行順序。
容器和函子(functor)
讓我們回顧開(kāi)頭的例子
var Container = function(x) {this.__value = x;}Container.of = function(x) { return new Container(x); };Container.prototype.map = function(f){return Container.of(f(this.__value))}
現(xiàn)在我們轉(zhuǎn)換角度,把調(diào)用 Container.of 返回的對(duì)象看作一種數(shù)據(jù)結(jié)構(gòu) Container {__value: 3} ,這種數(shù)據(jù)結(jié)構(gòu)只能使用 map 方法進(jìn)行操作,類似這樣的數(shù)據(jù)結(jié)構(gòu)被稱為 functor。
這樣做的好處是什么呢?我們能在不離開(kāi) 容器(Container) 的情況下操作容器里面的值,操作完成之后又放回容器。
我們可以不斷的進(jìn)行這一操作,就像 組合函數(shù) 一樣。這是一種抽象,我們讓容器保存值,并且請(qǐng)求容器通過(guò) map 里的函數(shù)去操作值。
總結(jié)
在文章中,提到了 純函數(shù)、柯里化(curry)、組合(compose)、容器(container)、函子(functor),不要看它們很遙遠(yuǎn)其實(shí)已經(jīng)或多或少出現(xiàn)在我們身邊。舉個(gè)例子:尖頭函數(shù)。
const curryAdd = x => y => x + yconst compose = (f, g) => x => f(g(x))const double = num => num * 2const price = y => compose(double, curryAdd(10))(y)console.log(price(0)) // 20
僅僅幾行代碼就可以體驗(yàn) curry 和 compose 工具。
本文完~
學(xué)習(xí)更多技能
請(qǐng)點(diǎn)擊下方公眾號(hào)
![]()

