JS函數式編程【譯】3.1 Javascript的函數式庫

Javascript的函數式庫

聽說全部的函數式程序員都會寫本身的函數庫,函數式Javascript程序員也不例外。 隨着現在開源代碼分享平臺如GitHab、Bower和NPM的涌現,對這些函數庫進行分享、變得及補充變得愈來愈容易。 如今已經有不少Javascript的函數式變成苦,從小巧的工具集到龐大的模塊庫都有。 javascript

每個庫都宣揚着本身的函數式編程風格。從一本正經的數學風格到靈活鬆散的非正式風格,每個庫都不盡相同, 然而他們他們有一個共同的特色:都是經過抽象的Javascript函數式能力來增進代碼的重用行、可讀性和健壯性。 css

然而直到寫這本書的時候,尚未一個函數庫成爲事實上的標準。有人可能會說underscore.js是, 不過在後面的章節你會看到,可能避免使用underscore.js是明智的。 html

Underscore.js

Underscore在不少人眼裏已經成爲函數式Javascript庫的標準。它成熟穩定, 其建立者Jeremy Ashkenas也是Backbone.js和Coffeescript的建立者。 Underscore其實是對Ruby的Enumerable模塊的從新實現, 這也解釋了爲何Coffeescript也是受Ruby影響。 java

與jQuery類似,Underscore並不改變Javascript原生對象,而是用一個符號來定義本身的對象, 就是下劃線(underscore)字符「_」。因此使用Underscore會是這個樣子: node

var x = _.map([1,2,3], Math.sqrt); // Underscore的map函數
console.log(x.toString());

咱們已經見過Javascript數組原生的map()方法,它是這樣用的:程序員

var x = [1,2,3].map(Math.sqrt);

不一樣的是,用underscore時,數組對象和回調函數都是做爲參數傳入給underscore的map()方法(_.map)的, 而不是像數組原生的map()方法(Array.prototype.map)那樣只需傳遞迴調。 編程

不過underscore除了map()還有不少內建函數,他們都是很是好用的函數, 好比find()、invoke()、pluck()、sortBy()、groupBy()等等。 設計模式

var greetings = [{
  origin: 'spanish',
  value: 'hola'
}, {
  origin: 'english',
  value: 'hello'
}];

console.log(_.pluck(greetings, 'value'));
// 獲取一個對象的屬性.
// 返回: ['hola', 'hello']

console.log(_.find(greetings, function(s) {
  return s.origin ==
    'spanish';
}));
// 查找第一個回調函數返回真的元素
// 返回: {origin: 'spanish', value: 'hola'}

greetings = greetings.concat(_.object(['origin', 'value'], ['french', 'bonjour']));
console.log(greetings);
// _.object經過合併兩個數組來創建一個對象
// 返回: [{origin: 'spanish', value: 'hola'},
//{origin: 'english', value: 'hello'},
//{origin: 'french', value: 'bonjour'}]

而且它還提供了鏈式調用方法數組

var g = _.chain(greetings)
  .sortBy(function(x) {
    return x.value.length
  })
  .pluck('origin')
  .map(function(x) {
    return x.charAt(0).toUpperCase() + x.slice(1)
  })
  .reduce(function(x, y) {
    return x + ' ' + y
  }, '')
  .value(); // 應用這些函數
// 返回: 'Spanish English French'
console.log(g);
_.chain()方法的返回值被包在了一個擁有Underscore所有函數的對象裏。_.value方法用於把被包裹的對象提取出來。 包裹的對象對於把Underscore混合到面向對象編程中很是有用。

儘管underscore易於使用而且被社區改進,他仍是在遭受批評。underscore強迫你編寫過於冗長的代碼, 並鼓勵你使用錯誤的模式。underscore的結構並不完美,甚至不夠函數式! 數據結構

就在Brian Lonsdorf在YouTube上發表名爲「嘿,underscore,你作錯了」的講話不久以後, underscore在發行的1.7.0版本中明確地阻止了咱們擴展函數,好比map()、reduce()和filter()等等。

_.prototype.map = function(obj, iterate, [context]) {
  if (Array.prototype.map && obj.map === Array.prototype.map)
    return obj.map(iterate, context);
  // ...
};
你能夠在 www.youtube.com/watch?v=m3svKOdZij查看Brian Lonsdorf講話的視頻。

在範疇論的形式裏,map是一個同態函子接口(詳見第五章《範疇輪》)。咱們應該可以把map定義爲函子, 不管咱們是否須要這樣。因此說underscore不是很函數式。

而且因爲Javascript不具備內建的不可變數據,函數式庫應該十分當心地避免輔助函數改變傳入的對象。 下面展現了一個針對這個問題的例子。代碼中你會了一個新的選線列表,其中有一個選擇項被設爲了默認項。 實際上原來的列表被修改了。

function getSelectedOptions(id, value) {
  options = document.querySelectorAll('#' + id + ' option');
  var newOptions = _.map(options, function(opt) {
    if (opt.text == value) {
      opt.selected = true;
      opt.text += ' (this is the default)';
    } else {
      opt.selected = false;
    }
    return opt;
  });
  return newOptions;
}
var optionsHelp = getSelectedOptions('timezones', 'Chicago');

咱們應當插入一行「opt = opt.cloneNode()」,讓回調函數對傳入的列表中每個元素創建一份拷貝。 underscore的map()函數爲了獲得性能而破壞了函數式的風水。原生的Array.prototype.map()不要求這些, 由於它會創建一個拷貝,然而它沒法做用於nodelist集合。

我插幾句。這一段真是翻譯得不太對勁。首先,我從哪翻譯出來個「風水」?呃~原文就是「feng shui」啊。 人家好不容易用了我們的詞我怎能不照字面翻譯過來?我估計做者說的風水是指函數式那一整套系統的一個感受吧。 反正是個說不太清的有機總體。原文是這樣的:「Underscore's map() function cheats to boost performance, but it is at the cost of functional feng shui」,我並無嚴格按字面翻譯是以爲這樣說好理解一點, 不過可能不太準確。另外,做者說數組原生的map會創建拷貝,意思是沒有反作用,可是你能夠建個對象數組試試, 在map的回調中改變傳入對象某屬性的值,原數組也發生了變化。做者鬧錯了吧……

Underscore也許並無要追求函數式編程數學上的正確性,不過它也歷來沒有想要把Javascript擴展或者轉變爲一個純函數語言。 它把本身定義爲一個提供一大堆有用的函數式編程輔助函數的Javascript庫。 也許它比那些僞造得看起來像函數式輔助函數的玩意兒要好些,不過它也不是一個嚴肅的函數式庫。

那麼有沒有更好的庫呢?一個創建在數學之上的庫?

Fantasy Land

有時,真實世界比小說更離奇。

Fantasy Land是一個函數式基礎庫的集合,也是一份關於如何在Javascript中實現「代數結構」的規格。 更確切地說,Fantasy Land闡述了通常代數結構(簡稱代數)的互操做性:monads、monoids、setoids、 函子(functors)、鏈(chains)等等。這些名字可能聽起來很嚇人,不過他們只是一系列值、 一系列操做以及一些必需要遵照的規定。換句話說,他們只不過是對象。

下圖展現了他們是如何工做的。每個代數是一個單獨的Fantasy Land規格, 它可能依賴於另外一個須要實現的代數。

這裏列出一些代數的規格:

  • Setoids:
    • 實現自反性(reflexivity)、對稱性(reflexivity)和傳遞性(transitivity)
    • 定義equals()方法
  • Semigroups
    • 實現結合律
    • 定義concat()方法
  • Monoid
    • 實現右單位元(right identity)和左單位元(left identity)
    • 定義empty()方法
  • 函子(functor)
    • 實現單位元和組合定律
    • 定義map()方法

這個列表還有不少內容

咱們不須要知道麼一個代數的確切含義是什麼,可是它的確頗有幫助,尤爲是當你編寫符合這些這些規則的本身的庫的時候。 這不僅是抽象的玩意兒,它對一個叫作範疇論的高度抽象的東西的含義進行了歸納。第五章將對範疇論進行全面的解釋。

Fantasy Land不僅告訴了咱們如何實現函數式編程,它還提供了一個Javascript的函數式模塊集。 然而裏面有不少不完整的東西,而且文檔也很不完善。不過Fantasy Land不是對這個開源規格的惟一實現。 還有一個實現的庫叫作Bilby.js。

Bilby.js

Bilby是個啥?它可不是夢幻大陸(Fantasy Land)上的神話生物,而是地球上的介於老鼠和兔子之間的一種怪異而可愛的動物, 中文名是兔耳袋狸。儘管如此,bilby.js庫聽從Fantasy Land的規格。

實際上,bilby是一個嚴肅的函數庫。如它的文檔中所描述:嚴肅,意味着它應用範疇論來實現高度抽象代碼; 函數式,意味着它可使程序引用透明。Wow,它還真夠嚴肅的。文檔的地址是 http://bilby.brianmckenna.org/。 它提供瞭如下內容:

  • 特定多態(ad-hoc polymorphism)的不可變多元方法(multi-methods)
  • 函數式數據結構
  • 函數式語言的操做符重載
  • 自動化規格測試(ScalaCheck, QuickCheck)

目前,Bilby.js這個已經很成熟的的庫符合了Fantasy Land關於代數結構的規格。 要寫徹底函數式語言的代碼,Bilby.js是一個優秀的資源。

咱們來看個例子

// bilby的環境是多元方法的不可變結構
var shapes1 = bilby.environment()
  // 定義方法
  .method(
    'area', // 方法的名稱
    function(a){return typeof(a) == 'rect'}, // 斷言
    function(a){return a.x * a.y} // 實現
  )
  // 定義屬性,相似於定義一個方法,裏面只有總返回true的斷言
  .property(
     'name',   // 名稱
     'shape'); // 函數
// 如今咱們能夠把它重載
var shapes2 = shapes1
  .method(
    'area', function(a){return typeof(a) == 'circle'},
    function(a){return a.r * a.r * Math.PI} );
var shapes3 = shapes2
  .method(
    'area', function(a){return typeof(a) == 'triangle'},
    function(a){return a.height * a.base / 2} );
// 如今咱們能夠像這樣作點什麼
var objs = [{type:'circle', r:5}, {type:'rect', x:2, y:3}];
var areas = objs.map(shapes3.area);
// 或者這樣
var totalArea = objs.map(shapes3.area).reduce(add);

這就是範疇論和特定多態的實踐。再囉嗦一次:範疇論將會在第五章全面講解。

範疇論是最近興起的一個數學分支,函數式程序員用它來最大程度抽象代碼。但這有一個主要的缺點: 範疇論難以被概念化且難以快速上手。

事實上,Bilby和Fantasy Land真的讓Javascript之上的函數式編程成爲了可能。 儘管能夠看到計算機科學發生着突飛猛進的變化,可是這個世界仍未準備好迎接Bilby和Fantasy Land 所推進的頑固的函數式編程風格。

也許在函數式Javascript的恐慌地帶的如此壯麗的一個庫並非咱們想要的。 畢竟咱們的出發點是尋找用於補充Javascript的函數式技術,而不是創建函數式編程信條。 如今讓咱們把注意力轉向另外一個新庫:Lazy.js。

Lazy.js

Lazy是一個實用的庫,它更大程度上是沿着Underscore的路線,不過它有惰性求值策略。正因如此, Lazy讓即刻解釋的語言本不可能完成的函數式計算變成了可能。它還會顯著提高性能。

Lazy庫還很年輕,可是在它背後有旺盛的社區熱度和強勁的動力。

Lazy的主意是,咱們可以迭代的全部東西都是一個序列。因爲這個庫用方法執行的前後來控制順序, 不少很酷的事情就能夠實現了:異步循環(並行編程)、無限序列、函數式響應式編程等等。

下面的例子展現了一下各類情形的代碼:

// 得到一首歌歌詞的前三行
var lyrics = "我徘徊在海之濱山之巔\n越此城鎮越彼鄉園\n ...
// 若是沒有惰性,整個歌詞會先根據換行來分割
console.log(lyrics.split('\n').slice(0, 3));
// 有了惰性,能夠只文本分割出來前三行
// 歌詞甚至能夠無限的長!
console.log(Lazy(lyrics).split('\n').take(3));
// 前十個能被3整除的平方數
var oneTo1000 = Lazy.range(1, 1000).toArray();
var sequence = Lazy(oneTo1000)
  .map(function(x) { return x * x; })
  .filter(function(x) { return x % 3 === 0; })
  .take(10)
  .each(function(x) { console.log(x); });
// 對無限序列的異步循環
var asyncSequence = Lazy.generate(function(x) {
    return x++
  })
  .async(100) // 每兩個元素間隔0.100秒
  .take(20) // 只計算前20項
  .each(function(e) { // 開始對序列進行循環
    console.log(new Date().getMilliseconds() + ": " + e);
  });

更多例子參見第四章。

不過Lazy庫的這個主意並不能保證它徹底的正確性。它還有一個前輩,Bacon.js,他們的工做方式差很少。

Bacon.js

這是Bacon.js的logo:

這個大鬍子牛仔是一個函數式響應式編程的庫。函數式響應式編程的意思是: 函數式設計模式用於展現響應式的常常變化的值,好比鼠標在屏幕上的位置,或者公司股票的價格。 跟Lazy經過須要時才計算值而避免建立無限循環的序列的方法相同, Bacon能夠避免實時計算隨時在發生變化的值,直到須要這個值的最後一秒。

在Lazy中被稱爲序列的東西在Bacon中是事件流和屬性,由於這樣更適合於使用事件(onmouseover, onkeydown等等)以及響應屬性(滾動位置、鼠標位置、toggle等等)。

Bacon.fromEventTarget(document.body, "click")
  .onValue(function() { alert("Bacon!") });

Bacon比Lazy稍老一些,可是它的功能集差很少是Lazy的一半,社區熱度差很少。

其餘的一些庫

Javascript函數式編程的庫實在太多了,沒法在本書中一一展現。咱們再來簡單看幾個吧。

  • Functional
    • 這也許是Javascript的第一個函數式編程庫,它包括了全面的高階函數支持和string lambdas。
  • wu.js
    • 因其curryable()函數而飽受讚譽的wu.js庫是一個很優秀的函數式編程庫。它是第一個(據我所知) 實現了惰性求值的庫,這影響了Bacon.js、Lazy.js等庫。
    • 是的,它的名字來源於臭名昭著的搖滾組合「Wu-Tang Clan」
      "Wu-Tang Clan"實際就是武當派。是一個黑人說唱搖滾組合,有不少功夫文化的內容。
  • sloth.js
    • 和Lazy.js很像,可是更小
  • stream.js
    • 支持無限流,其它沒什麼
    • 特別小
  • Lo-Dash.js
    • 就像名字所暗示的那樣,它是受underscore.js的啓發
    • 高度優化
  • Sugar
    • Sugar是Javascript函數式編程技術的支持庫,和Underscore相像,可是在實現上有一些關鍵的不一樣。
    • underscore中的 _.pluck(myObjs, 'value')在Suger中僅僅是myObjs.map('value')。 意思是他修改了Javascript原生的對象,因此它在跟其它庫混用的時候會有些風險,好比Prototype。
  • from.js
    • 一個新的函數式庫,Javascript的LINQ(語言集成查詢)引擎,支持.net所提供的大多數LINQ函數。
    • 100%惰性求值,並支持lambda表達式
    • 很年輕,可是文檔很出色
  • JSLINQ
    • 另外一個Javascript的LINQ引擎
    • 比from.js更老也更成熟
  • Boiler.js
    • 另外一個讓Javascript擴展的函數式方法更加原生的工具庫,包括:字符串、數字、對象、集合和數組
  • Folktale
    • 像Bilby.js那樣,Folktable是一個對Fantasy Land實現的新庫。而且像他的祖先那樣, Folktable也是一個Javascript函數式編程庫的集合。它還很年輕,但有光明的前景。
  • jQuery
    • 在這裏看到jQuery很吃驚嗎?儘管jQuery不是一個用於函數式編程的工具,但它本身是函數式的。 jQuery應該是根植於函數式編程的使用最普遍的庫。
    • jQuery對象實際是一個monad。jQuery使用了monad的規則來實現方法鏈式調用:
      $('#mydiv').fadeIn().css('left': 50).alert('hi!');
      關於這個的詳細解釋能夠在第七章《Javascript的函數式和麪向對象編程》中找到
    • 它的一些函數是高階的
      $('li').css('left': function(index){return index*50});
    • jQuery1.8以上的deferred.then實現了函數式概念Promise
    • jQuery是一個抽象層,主要是面向DOM。它不是一個框架或工具集, 只是一個使用抽象來提升代碼複用和減小丑陋代碼的方式。而函數式編程不全都是關於這些的嗎?
相關文章
相關標籤/搜索