《JS函數(shù)式編程指南》這本書值得讀很多遍,特別推薦哦~~~
看這本書初衷是看懂ramda,結(jié)果發(fā)現(xiàn) 函數(shù)式編程這個(gè)大蘿卜抵拘。哈哈哈哈。型豁。僵蛛。
一. 相關(guān)術(shù)語
- 函數(shù)范式(functional paradigm)
- 命令式 (imperative)
- 可變狀態(tài)(mutable state)
- 無限制副作用(unrestricted side effects)
- 無原則設(shè)計(jì)(unprincipled design)
- 認(rèn)知負(fù)荷(cognitive load)
- 可緩存性(Cacheable)
- 可移植性/自文檔化(Portable/Self-Documenting)
- 類型簽名(type signatures)
- 類型約束(type constraints)
- 。迎变。充尉。
我理解的函數(shù)式編程就是運(yùn)用數(shù)學(xué)中函數(shù)
的方式,以通用衣形、可組合的組件形式進(jìn)行編程驼侠,而不是過程化地命令計(jì)算機(jī)去怎么做。函數(shù)式編程優(yōu)勢(shì)主要體現(xiàn)在數(shù)據(jù)不變性
和函數(shù)無副作用
兩方面;
二. 應(yīng)避免出現(xiàn)的情況
-
: 用一個(gè)函數(shù)將另一個(gè)函數(shù)包裝起來谆吴, 目的只是延遲執(zhí)行倒源;
// wrong var getServerStuff = function(callback){ return ajaxCall(function(json){ return callback(json); }); }; // right var getServerStuff = ajaxCall;
因?yàn)椋?/p>
return ajaxCall(function(json){ return callback(json); }); // 等價(jià)于 ajaxCall(callback);
第一種書寫方式,雖然更易于理解句狼,但是內(nèi)層函數(shù)參數(shù)改變時(shí)笋熬,外層包裹函數(shù)也需要同時(shí)改變。
- :在命名時(shí)將自己限定在特定的數(shù)據(jù)/情景中腻菇;
這是重復(fù)造輪子的一大原因胳螟;
三. 什么是純函數(shù)
純函數(shù)是這樣一種函數(shù)昔馋,即相同的輸入,永遠(yuǎn)會(huì)得到相同的輸出糖耸,而且沒有任何可觀察的副作用
比如: 數(shù)組中的slice 函數(shù)則為純函數(shù)秘遏,每次應(yīng)用會(huì)得到相同的數(shù)據(jù),而splice則不同蔬捷;
純函數(shù)就是數(shù)學(xué)上的函數(shù)垄提,而且是函數(shù)式編程的全部
還有一種的情況就是:在函數(shù)中引入了外部的環(huán)境榔袋,從而增加了認(rèn)知負(fù)荷;
舉例:
// 不純的
var minimum = 21;
var checkAge = function(age) {
return age >= minimum;
};
// 純的
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
在不純的版本函數(shù)中周拐,其輸入值依賴于系統(tǒng)狀態(tài)。
對(duì)于純函數(shù)定義中提到的副作用是指:
副作用是在計(jì)算結(jié)果的過程中凰兑,系統(tǒng)狀態(tài)的一種變化妥粟,或者與外部世界進(jìn)行的可觀察的交互。
只要是和函數(shù)外部環(huán)境發(fā)生的交互就都是副作用吏够;
在純函數(shù)中勾给,并不是要禁止一切副作用,而是讓副作用發(fā)生在可控的范圍內(nèi)锅知,在純函數(shù)中使用functor和monad進(jìn)行控制副作用播急。
四. 純函數(shù)的好處
- 純函數(shù)總能根據(jù)輸入來做緩存。
實(shí)現(xiàn)緩存的一種典型方式是memoize技術(shù)售睹。
對(duì)于含有類似請(qǐng)求等不純的函數(shù)桩警,通過包裹一層新的函數(shù)的延遲執(zhí)行的方式把不純的函數(shù)變成純函數(shù)。var memoize = function(f) { var cache = {}; return function() { var arg_str = JSON.stringify(arguments); cache[arg_str] = cache[arg_str] || f.apply(f, arguments); return cache[arg_str]; }; }
- 可移植性/自文檔化(Portable/Self-Documenting)
純函數(shù)的依賴都在參數(shù)中指明昌妹,更易于觀察理解捶枢。
??:
在JS中,可移植性意味著把函數(shù)序列化并通過socket發(fā)送飞崖,也可以意味著代碼可以在web worker 在運(yùn)行烂叔。// 不純的 var signUp = function(attrs) { var user = saveUser(attrs); welcomeUser(user); }; // 純的 var signUp = function(Db, Email, attrs) { return function() { var user = saveUser(Db, attrs); welcomeUser(Email, user); };
命令式編程中的方法和過程都深深的和其運(yùn)行環(huán)境相關(guān),功能通過狀態(tài)固歪、依賴和有效作用達(dá)成蒜鸡。而純函數(shù)正好相反,與環(huán)境無關(guān)牢裳,因此可以移植逢防。 - 可測(cè)試性(Testable)
只需要簡(jiǎn)單的給函數(shù)一個(gè)輸入,然后斷言輸出就好了贰健。 - 合理性(Reasonable)
如果一段代碼可以替換成它所執(zhí)行的所得的結(jié)果胞四,而且是在不改變整個(gè)程序行為的前提下替換的,則稱這段代碼是引用透明的(referential transparency)伶椿。
由于純函數(shù)總是相同的輸入得到相同的輸出辜伟,所以純函數(shù)也是引用透明
的氓侧。這也是純函數(shù)的很大的一個(gè)優(yōu)點(diǎn)。 - 并行代碼
我們可以并行運(yùn)行任意的純函數(shù)导狡。因?yàn)榧兒瘮?shù)根本不需要訪問共享的內(nèi)存约巷,而且根據(jù)其定義,純函數(shù)也不會(huì)因?yàn)楦弊饔枚M(jìn)入競(jìng)爭(zhēng)態(tài)(race condition)旱捧;
五. 快速實(shí)現(xiàn)純函數(shù)化的工具--柯里化(curry)
-
什么是Curry独郎?
Curry:只傳遞函數(shù)的一部分參數(shù)來調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)枚赡。
可以一次性地調(diào)用curry函數(shù)氓癌,也可以每次只傳一個(gè)參數(shù)分多次調(diào)用。
Ramda 函數(shù)本身都是自動(dòng)柯里化; -
Curry 幫助函數(shù)
Lodash贫橙、Ramda庫中都有Curry 幫助函數(shù)贪婉。在使用這類函數(shù)時(shí)有一個(gè)很重要的模式就是將要操作的數(shù)據(jù)放在最后的一個(gè)參數(shù)中。
?? 1:const R = require('ramda'); const match = R.curry((what, str) => { return str.match(what); }) match(/\s+/g, 'hhhh'); // or match(/\s+/g)('hhhh');
?? 2:
// 使用幫助函數(shù) `_keepHighest` 重構(gòu) `max` 使之成為 curry 函數(shù) // 無須改動(dòng): var _keepHighest = function(x,y){ return x >= y ? x : y; }; // 重構(gòu)這段代碼: var max = function(xs) { return reduce(function(acc, x){ return _keepHighest(acc, x); }, -Infinity, xs); }; var max = R.reduce(_keepHighest, -Infinity);
?? 3:
// 包裹數(shù)組的 slice 函數(shù)使之成為 curry 函數(shù) // [1,2,3].slice(0,1); var slice = R.curry(function(start, end, xs){ return xs.slice(start, end); });
六. 代碼組合(compose)
- 什么是Compose卢肃?
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
上面的代碼即為代碼組合的本質(zhì)疲迂。組合就是將兩個(gè)或兩個(gè)以上的函數(shù)進(jìn)行結(jié)合返回新的函數(shù)。
在組合的定義中莫湘,g 將先于f 執(zhí)行尤蒿,因此就創(chuàng)建了一個(gè)從右到左的數(shù)據(jù)流。
組合負(fù)符合數(shù)學(xué)中的結(jié)合律
compose(a,b,c) === compose(compose(a,b),c) === compose(a,compose(b,c))
結(jié)合律的一大好處是任何一個(gè)函數(shù)分組都可以被拆開來幅垮,然后再以它們自己的組合方式打包在一起腰池。
組合中數(shù)據(jù)的轉(zhuǎn)變?nèi)缦聢D:
- Compose有利于實(shí)現(xiàn)pointfree
pointfree 模式:函數(shù)無須表明要操作的數(shù)據(jù)的樣子。一等公民的函數(shù)军洼、curry巩螃、compose聯(lián)合使用有利于實(shí)現(xiàn)這種模式。
?? :
// 非 pointfree匕争,因?yàn)樘岬搅藬?shù)據(jù):word
var snakeCase = function (word) {
return word.toLowerCase().replace(/\s+/ig, '_');
};
// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
在pointfree的實(shí)現(xiàn)方式中不需要指明操作的數(shù)據(jù)為word避乏,而在非pointfree的代碼中則需要指明。pointfree 代碼可以幫我們減少很多不必要的命名甘桑。
在pointfree的實(shí)現(xiàn)方式中拍皮, 是通過管道將數(shù)據(jù)在接受單個(gè)參數(shù)的函數(shù)間傳遞。通過Curry跑杭,使得compose中的每一個(gè)函數(shù)都先接受數(shù)據(jù)铆帽,然后操作數(shù)據(jù),最后再把結(jié)果傳遞給下一個(gè)函數(shù)德谅。
- Compose的調(diào)試方式
在使用組合時(shí)爹橱,將多個(gè)函數(shù)組合在一起,除了最右邊的函數(shù)可以一次性接收兩個(gè)及兩個(gè)以上的參數(shù)窄做,其他的函數(shù)一次只能接收一個(gè)參數(shù)愧驱,因此經(jīng)常會(huì)出現(xiàn)下面的錯(cuò)誤慰技。
// wrong
var latin = compose(map, angry, reverse);
// right
var latin = compose(map(angry), reverse);
調(diào)試上面代碼的錯(cuò)誤可以使用下面這個(gè)不純的trace函數(shù)來追蹤代碼的執(zhí)行情況:
var trace = R.curry(function(tag, x){
console.log(tag, x);
return x;
});
??:
var dasherize = compose(join('-'), toLower, split(' '), replace(/\s{2,}/ig, ' '));
dasherize('The world is a vampire');
//debug
var dasherize = compose(join('-'), toLower, trace("after split"), split(' '), replace(/\s{2,}/ig, ' '));
// after split [ 'The', 'world', 'is', 'a', 'vampire' ]
- Compose使用舉例
const _ = require('ramda');
// 數(shù)據(jù)
const CARS = [
{name: "Ferrari FF", horsepower: 660, dollar_value: 700000, in_stock: true},
{name: "Spyker C12 Zagato", horsepower: 650, dollar_value: 648000, in_stock: false},
{name: "Jaguar XKR-S", horsepower: 550, dollar_value: 132000, in_stock: false},]
// ?? 1:
// 使用 _.compose()、_.prop() 和 _.head() 獲取第一個(gè) car 的 name
// answer
const nameOfFirstCar = _.compose(_.prop('name',_.head));
// ?? 2:
// 重寫下面這個(gè)函數(shù)
var isLastInStock = function(cars) {
var last_car = _.last(cars);
return _.prop('in_stock', last_car);
};
// answer
const isLastInStock = _.compose(_.prop('in_stock'), _.last);
// ?? 3:
// 使用_average重寫下面函數(shù)
var _average = function(xs) { return reduce(add, 0, xs) / xs.length; };
var averageDollarValue = function(cars) {
var dollar_values = map(function(c) { return c.dollar_value; }, cars);
return _average(dollar_values);
};
// answer
const averageDollarValue = _.compose(_average,_.map(_.prop('dollar_value')));
// ?? 4:
// 重構(gòu)下面的代碼
var availablePrices = function(cars) {
var available_cars = _.filter(_.prop('in_stock'), cars);
return available_cars.map(function(x){
return accounting.formatMoney(x.dollar_value);
}).join(', ');
};
// answer
var formatPrice = _.compose(accounting.formatMoney, _.prop('dollar_value'));
var availablePrices = _.compose(join(', '), _.map(formatPrice), _.filter(_.prop('in_stock')));
// ?? 5:
// 重構(gòu)下面的代碼
var fastestCar = function(cars) {
var sorted = _.sortBy(function(car){ return car.horsepower }, cars);
var fastest = _.last(sorted);
return fastest.name + ' is the fastest';
};
// answer
var append = _.flip(_.concat);
var fastestCar = _.compose(append(' is the fastest'),
_.prop('name'),
_.last,
_.sortBy(_.prop('horsepower')));
七. 聲明式代碼
-
什么是聲明式代碼组砚?
命令式代碼是一步步地指示要做怎么做吻商。聲明式代碼是告訴要做什么,而不是怎么做糟红。雖然命令式代碼并不錯(cuò)艾帐,但是命令式代碼硬編碼了一步接一步的執(zhí)行方式。聲明式代碼不指定執(zhí)行順序盆偿,所以更適合于并行執(zhí)行柒爸。
?? :// 命令式 var makes = []; for (i = 0; i < cars.length; i++) { makes.push(cars[i].make); } // 聲明式 var makes = cars.map(function(car){ return car.make; });
-
可用于重構(gòu)的等式
// map 的組合律 var law = compose(map(f), map(g)) == map(compose(f, g));
使用上面的等式進(jìn)行重構(gòu)可以將兩層循環(huán)合并成一層循環(huán)。
八. Hindley-Milner 類型簽名(type signatures)
-
什么是Hindley-Milner 類型簽名陈肛?
在 Hindley-Milner 系統(tǒng)中揍鸟,函數(shù)都寫成類似 a -> b 這個(gè)樣子兄裂,其中 a 和b 是任意類型的變量句旱。?? 1:
// match :: Regex -> String -> [String] // match :: Regex -> (String -> [String]) var match = curry(function(reg, s){ return s.match(reg); });
對(duì)于Hindley-Milner 類型簽名:
- 與普通代碼一樣,類型簽名中也使用變量晰奖,把變量命名為a 和b 只是一種約定俗成的習(xí)慣谈撒;
對(duì)于相同的變量名,其類型也一定相同匾南。 a -> b 可以是從任意類型的 a 到任意類型的 b啃匿,但是 a -> a 必須是同一個(gè)類型; - 可以將最后一個(gè)類型理解成返回值蛆楞;
- 將(a -> b)理解成一個(gè)類型為a的參數(shù)溯乒,返回類型為b的結(jié)果的函數(shù);
- 簽名可以把類型約束為一個(gè)特定的接口(interface)豹爹;這就是類型約束(type constraints)
以上面的規(guī)則對(duì)reduce進(jìn)行解釋:
?? 2:
// reduce :: (b -> a -> b) -> b -> [a] -> b
var reduce = curry(function(f, x, xs){
return xs.reduce(f, x);
});
首先reduce接收(b -> a -> b)這樣的一個(gè)函數(shù)作為參數(shù)1裆悄,函數(shù)的兩個(gè)參數(shù)為類型b 和a, b和a
的值來自于 reduce接收的第2個(gè)和第3個(gè)參數(shù),最終返回類型b 的結(jié)果值臂聋, 并可以看到結(jié)果值類型b和reduce的第一個(gè)參數(shù)(這個(gè)函數(shù))的返回類型相同光稼,則可以看出reduce的返回值則為reduce接收的一個(gè)參數(shù)的返回值。
?? 3:
// sort :: Ord a => [a] -> [a]
胖箭頭左邊指明 a 一定是個(gè) Ord 對(duì)象孩等。
- parametricity
一旦引入一個(gè)類型變量艾君,就會(huì)出現(xiàn)一個(gè)奇怪的特性叫parametricity。parametricity 是指此函數(shù)將會(huì)以一種統(tǒng)一的行為作用于所有的類型肄方。
??:
// fun:: [a] -> a
a 告訴我們它不是一個(gè)特定的類型冰垄,這意味著它可以是任意類型;那么我們的函數(shù)對(duì)每一個(gè)可能的類型的操作都必須保持統(tǒng)一权她。這就是 parametricity 的含義虹茶。