天天看點

函數式程式設計第一步——流程控制

  失落迷茫了好一段日子。終于我用接觸2個月的技術Nodejs成功的混到一份工作。嚴格來說隻學習了3天(白天睡覺,晚上通宵學習),後面的時間都是在配置環境。總的來說,函數式程式設計是有應用的市場的,而且學習門檻也不是太高。就算從來沒聽說過函數式程式設計的人也會知道javascript,也會使用jquery。雖然很多是把它當作過程式的來用,來看待。這也是在于它的文法看起來太像C,太像過程式的語言。

  之前一直想寫一些關于函數程式設計文章來記錄我學習的曆程。之前寫了一篇使用F#的,不過大家好像對F#比較排斥。以後我從工作出發寫nodejs的吧。

  好了。廢話不多說我們先從一個具體的項目來分析函數式程式設計吧。

  用webstorm建立一個express項目,這是nodejs下用來做web伺服器的庫。會生成類似下面這個結構的檔案。

  • /bin/www : 項目的啟動檔案,配置了監聽的端口,當然程式入口還是app.js
  • /node_modules/ : 通過npm包管理中間件都在這,包括session,模闆,日志等中間件,你自己安裝的中間件也在這
  • /public/  : 暴露的檔案夾,從名字就可以看出,圖面前端js腳本和css會在這裡
  • /routes/ : 路由,相當于控制器
  • /views/ : 模闆檔案
  • /app.js : 約定俗成的項目入口
  • /package.json : 配置你項目依賴的包,使用npm指令 npm install -d 會自動安裝裡面記錄的中間件,非常友善。由于nodejs的中間件不完全是腳本組成的,也會包含C寫的編譯檔案,各環境下不盡相同,是以通過npm,本地下載下傳編譯是非常重要的

  總的來說檔案結構隻是約定俗成,或是按人們習慣來用的。不像java、C之類的會有main函數作為入口。任何檔案都能當作啟動入口。nodejs也不僅限于開發web伺服器,加上各種奇葩的中間件的運用,會讓項目變成各種形态。這是一個自由度非常高的開發平台。

   我們先寫一個簡單的demo。由于js的文法太過糾結,我們使用另外一種語言coffeescript,他是一個nodejs的庫。能自己運作在nodejs上,也能編譯成js檔案。這裡我們隻是用做文法糖,仍然編譯成js檔案。我會貼出兩種代碼來适應不同的需要。

coffeescript

fna = ->
  console.log("I am 'a'")

fnb = ->
  console.log "I am 'b'"

fna()
fnb()      

javscript

// Generated by CoffeeScript 1.7.1
(function() {
  var fna, fnb;

  fna = function() {
    return console.log("I am 'a'");
  };

  fnb = function() {
    return console.log("I am 'b'");
  };

  fna();

  fnb();

}).call(this);

//# sourceMappingURL=test.map      

  這裡我編寫了兩個函數,并依次調用它們。coffeescript會嚴格申明變量和閉包,不會讓其污染全局變量。代碼精簡不少,看起來也更像是函數式程式設計了。輸入結果顯而易見。

console.log

i am 'a'
i am 'b'      

  nodejs是異步執行的。如果這是兩個有關聯的函數呢?

coffeescript

fna = ->
  console.log("這是母雞")

fnb = ->
  console.log "母雞下蛋"

fna()
fnb()      

javascript

// Generated by CoffeeScript 1.7.1
(function() {
  var fna, fnb;

  fna = function() {
    return console.log("這是母雞");
  };

  fnb = function() {
    return console.log("母雞下蛋");
  };

  fna();

  fnb();

}).call(this);

//# sourceMappingURL=test.map      

  單從結果來看,好像沒有什麼問題。

console.log

這是母雞
母雞下蛋      

  在實際項目中,我們并不知道兩個函數内部到底幹了什麼,就像蝴蝶效應,任何改動都可能讓結果發生變動。

coffeescript

fna = ->
  setTimeout ->
    console.log("這是母雞")
  , 100

fnb = ->
  console.log "母雞下蛋"

fna()
fnb()      

javascript

// Generated by CoffeeScript 1.7.1
(function() {
  var fna, fnb;

  fna = function() {
    return setTimeout(function() {
      return console.log("這是母雞");
    }, 100);
  };

  fnb = function() {
    return console.log("母雞下蛋");
  };

  fna();

  fnb();

}).call(this);

//# sourceMappingURL=test.map      

console.log

母雞下蛋
這是母雞      

  現在就不是我們想要的結果了。其實這種異步方式也很好了解,它隻管函數調用,而不管函數結果。在同步程式設計中,前一步操作會阻塞後一步操作,母雞下蛋的操作會等着這隻母雞出結果。而異步程式設計中,不會阻塞後面的任務進行,就像指揮官給手下發派任務,手下都會去執行各自的任務,但什麼時候完成任務就不好說了。這樣做的好處就是在執行耗時任務的時候,其他的任務也能繼續執行,或者同時執行多個耗時任務。但是有利有弊,在流程控制上會比較糾結。正常做法是用回調函數,就像有人說過,世上本來沒有回調,用的人多了也就有了回調函數。

coffeescript

fna = (next) ->
  setTimeout ->
    console.log("這是母雞")
    next()
  , 1000

fnb = ->
  console.log "母雞下蛋"

fna ->
  fnb()      

javascript

// Generated by CoffeeScript 1.7.1
(function() {
  var fna, fnb;

  fna = function(next) {
    return setTimeout(function() {
      console.log("這是母雞");
      return next();
    }, 1000);
  };

  fnb = function() {
    return console.log("母雞下蛋");
  };

  fna(function() {
    return fnb();
  });

}).call(this);

//# sourceMappingURL=test.map      

conslole.log

這是母雞
母雞下蛋      

  這中方法雖然解決了關聯函數的流程控制問題,但是也有新的問題。邏輯複雜的時候,回調嵌套就會越來越深。

coffeescript

fna = (next) ->
  setTimeout ->
    console.log("這是母雞")
    next()
  , 1000

fnb = (next) ->
  setTimeout ->
    console.log "母雞下蛋"
    next()
  , 100

fnc = ->
  console.log "蛋孵出了雞"

fna ->
  fnb ->
    fnc()      

javascript

// Generated by CoffeeScript 1.7.1
(function() {
  var fna, fnb, fnc;

  fna = function(next) {
    return setTimeout(function() {
      console.log("這是母雞");
      return next();
    }, 1000);
  };

  fnb = function(next) {
    return setTimeout(function() {
      console.log("母雞下蛋");
      return next();
    }, 100);
  };

  fnc = function() {
    return console.log("蛋孵出了雞");
  };

  fna(function() {
    return fnb(function() {
      return fnc();
    });
  });

}).call(this);

//# sourceMappingURL=test.map      

console.log

這是母雞
母雞下蛋
蛋孵出了雞      

  幸好有中間件解決這個問題。async 中間件有各種流程控制方法。其中series就能很優美的實作這個邏輯。你所要做的就是每個函數裡加上一個回調next執行下一步操作,第一個參數是err,第二個參數能追加一個結果,在async最後的回調中傳回出來。

coffeescript

async = require "async"
fna = (next) ->
  setTimeout ->
    console.log "這是母雞"
    next(null, 1)
  , 1000

fnb = (next) ->
  setTimeout ->
    console.log "母雞下蛋"
    next(null, 2)
  , 2000

fnc = (next) ->
  setTimeout ->
    console.log "蛋孵出了雞"
    next(null, 3)
  , 100


async.series [
  fna
  fnb
  fnc
]
,  (err, results) ->
   console.log results      

javascript

// Generated by CoffeeScript 1.7.1
(function() {
  var async, fna, fnb, fnc;

  async = require("async");

  fna = function(next) {
    return setTimeout(function() {
      console.log("這是母雞");
      return next(null, 1);
    }, 1000);
  };

  fnb = function(next) {
    return setTimeout(function() {
      console.log("母雞下蛋");
      return next(null, 2);
    }, 2000);
  };

  fnc = function(next) {
    return setTimeout(function() {
      console.log("蛋孵出了雞");
      return next(null, 3);
    }, 100);
  };

  async.series([fna, fnb, fnc], function(err, results) {
    return console.log(results);
  });

}).call(this);

//# sourceMappingURL=test.map      

console.log

這是母雞
母雞下蛋
蛋孵出了雞
[ 1, 2, 3 ]      

更好的封裝,應該是這個樣子。

coffeescript

async = require "async"
fna = (next) ->
  setTimeout ->
    console.log "這是母雞"
    next()
  , 1000

fnb = (next) ->
  setTimeout ->
    console.log "母雞下蛋"
    next()
  , 2000

fnc = (next) ->
  setTimeout ->
    console.log "蛋孵出了雞"
    next()
  , 100


async.series [
  (next) ->
    fna ->
      next null, 1
  (next) ->
    fnb ->
      next null, 2
  (next) ->
    fnc ->
      next null, 3
]
,  (err, results) ->
   console.log results      

javascript

// Generated by CoffeeScript 1.7.1
(function() {
  var async, fna, fnb, fnc;

  async = require("async");

  fna = function(next) {
    return setTimeout(function() {
      console.log("這是母雞");
      return next();
    }, 1000);
  };

  fnb = function(next) {
    return setTimeout(function() {
      console.log("母雞下蛋");
      return next();
    }, 2000);
  };

  fnc = function(next) {
    return setTimeout(function() {
      console.log("蛋孵出了雞");
      return next();
    }, 100);
  };

  async.series([
    function(next) {
      return fna(function() {
        return next(null, 1);
      });
    }, function(next) {
      return fnb(function() {
        return next(null, 2);
      });
    }, function(next) {
      return fnc(function() {
        return next(null, 3);
      });
    }
  ], function(err, results) {
    return console.log(results);
  });

}).call(this);

//# sourceMappingURL=test.map      

到這裡coffeescript還可以一戰,js你已經完全看不懂了對不對?

  今天就寫到這裡了,我接觸到的範圍也不廣,以後大家有什麼關于函數式程式設計的問題可以告知,大家一起解決。