《JS函數(shù)式編程指南》Part 1筆記

《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ù)的好處

  1. 純函數(shù)總能根據(jù)輸入來做緩存。
    實(shí)現(xiàn)緩存的一種典型方式是memoize技術(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];
      };
    }
    
    對(duì)于含有類似請(qǐng)求等不純的函數(shù)桩警,通過包裹一層新的函數(shù)的延遲執(zhí)行的方式把不純的函數(shù)變成純函數(shù)。
  2. 可移植性/自文檔化(Portable/Self-Documenting)
    純函數(shù)的依賴都在參數(shù)中指明昌妹,更易于觀察理解捶枢。
    ??:
     // 不純的
     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);
       }; 
    
    在JS中,可移植性意味著把函數(shù)序列化并通過socket發(fā)送飞崖,也可以意味著代碼可以在web worker 在運(yùn)行烂叔。
    命令式編程中的方法和過程都深深的和其運(yùn)行環(huán)境相關(guān),功能通過狀態(tài)固歪、依賴和有效作用達(dá)成蒜鸡。而純函數(shù)正好相反,與環(huán)境無關(guān)牢裳,因此可以移植逢防。
  3. 可測(cè)試性(Testable)
    只需要簡(jiǎn)單的給函數(shù)一個(gè)輸入,然后斷言輸出就好了贰健。
  4. 合理性(Reasonable)
    如果一段代碼可以替換成它所執(zhí)行的所得的結(jié)果胞四,而且是在不改變整個(gè)程序行為的前提下替換的,則稱這段代碼是引用透明的(referential transparency)伶椿。
    由于純函數(shù)總是相同的輸入得到相同的輸出辜伟,所以純函數(shù)也是引用透明的氓侧。這也是純函數(shù)的很大的一個(gè)優(yōu)點(diǎn)。
  5. 并行代碼
    我們可以并行運(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)

  1. 什么是Curry独郎?

    Curry:只傳遞函數(shù)的一部分參數(shù)來調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)枚赡。
    可以一次性地調(diào)用curry函數(shù)氓癌,也可以每次只傳一個(gè)參數(shù)分多次調(diào)用。
    Ramda 函數(shù)本身都是自動(dòng)柯里化;

  2. 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)

  1. 什么是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:

image.png

  1. 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ù)德谅。

  1. 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' ] 
  1. 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')));

七. 聲明式代碼

  1. 什么是聲明式代碼组砚?

    命令式代碼是一步步地指示要做怎么做吻商。聲明式代碼是告訴要做什么,而不是怎么做糟红。雖然命令式代碼并不錯(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; }); 
    
  2. 可用于重構(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)

  1. 什么是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ì)象孩等。

  1. parametricity
    一旦引入一個(gè)類型變量艾君,就會(huì)出現(xiàn)一個(gè)奇怪的特性叫parametricity。parametricity 是指此函數(shù)將會(huì)以一種統(tǒng)一的行為作用于所有的類型肄方。
    ??:
 // fun:: [a] -> a

a 告訴我們它不是一個(gè)特定的類型冰垄,這意味著它可以是任意類型;那么我們的函數(shù)對(duì)每一個(gè)可能的類型的操作都必須保持統(tǒng)一权她。這就是 parametricity 的含義虹茶。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末冀瓦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子写烤,更是在濱河造成了極大的恐慌翼闽,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洲炊,死亡現(xiàn)場(chǎng)離奇詭異感局,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)暂衡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門询微,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狂巢,你說我怎么就攤上這事撑毛。” “怎么了唧领?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵藻雌,是天一觀的道長。 經(jīng)常有香客問我斩个,道長胯杭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任受啥,我火速辦了婚禮做个,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘滚局。我一直安慰自己居暖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布藤肢。 她就那樣靜靜地躺著太闺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谤草。 梳的紋絲不亂的頭發(fā)上跟束,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音丑孩,去河邊找鬼冀宴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛温学,可吹牛的內(nèi)容都是我干的略贮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼逃延!你這毒婦竟也來了览妖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤揽祥,失蹤者是張志新(化名)和其女友劉穎讽膏,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拄丰,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡府树,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了料按。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奄侠。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖载矿,靈堂內(nèi)的尸體忽然破棺而出垄潮,到底是詐尸還是另有隱情,我是刑警寧澤闷盔,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布弯洗,位于F島的核電站,受9級(jí)特大地震影響馁筐,放射性物質(zhì)發(fā)生泄漏涂召。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一敏沉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炎码,春花似錦盟迟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至歉闰,卻和暖如春辖众,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背和敬。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工凹炸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昼弟。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓啤它,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子变骡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 上周8班作文為《瞧塌碌,這一家子》渊胸。 從孩兒們的寫作效果來看,貼近家庭實(shí)際台妆。語言或質(zhì)樸蹬刷,或俏皮幽默,讓人忍俊不禁频丘,結(jié)構(gòu)...
    辰辰2008閱讀 370評(píng)論 0 2
  • PHP基本語法 變量相關(guān)的函數(shù) isset() 判斷變量是否被定義 empty() 判斷變量是否為空值办成, unse...
    阿爾方斯閱讀 203評(píng)論 0 0