當Buzzdecafe(Ramda庫的主要貢獻者)最近將Ramda介紹給世界時,有兩種大相徑庭的反應。那些習慣了函數式技術的人——不管是用JavaScript或其餘語言——大多會回答「酷」。他們可能對此感到興奮,或者只是隨便地注意到另外一個潛在的工具,但他們理解它的用途。node
第二種反應是:「哈?」npm
對於那些不習慣於函數式編程的人來講,ramda彷佛毫無用處。它的大部分主要功能已經被諸如Underscore和lodash之類的庫所實現了。編程
這些人是對的。若是你想用你一直使用的命令式和麪向對象風格來編寫代碼,Ramda沒有更多的功能提供給你。數組
然而,它確實提供了一種不一樣的編碼風格,這種風格在純函數式編程語言中被認爲是理所固然的:Ramda經過函數組合的方式使構建複雜邏輯變得簡單。請注意,任何具備compose函數的庫都容許您進行函數組合;但Ramda真正的要點是:「使其簡單化」。瀏覽器
讓咱們看看Ramda是如何運做的。緩存
Web框架老是拿「todolist」當作事例,所以咱們也用它作事例:想象這樣一個場景,篩選todolist以刪除全部已完成的項。bash
使用內置的數組原型方法,咱們能夠這樣作:數據結構
// Plain JS
var incompleteTasks = tasks.filter(function(task) {
return !task.complete;
});
複製代碼
使用LoDash,會簡單一點:框架
// Lo-Dash
var incompleteTasks = _.filter(tasks, {complete: false});
複製代碼
上面兩種狀況,咱們都會獲得一個通過過濾的任務列表。編程語言
如今使用Ramda,咱們能夠這樣作:
var incomplete = R.filter(R.where({complete: false});
複製代碼
注意到什麼東西不見了嗎?任務列表tasks沒有了。這個ramda代碼只是給了咱們一個函數。
爲了獲得結果,咱們仍然須要用任務列表tasks來調用它。
var incompleteTasks = incomplete(tasks);
複製代碼
這就是重點所在。
由於咱們如今有了一個函數,咱們能夠很容易地將它與其餘函數結合起來,而後再對數據進行操做。假設咱們有一個函數groupbyuser,它按用戶對todo項進行分組。而後咱們能夠簡單地建立一個新的函數:
var activeByUser = R.compose(groupByUser, incomplete);
複製代碼
上面代碼實現了選擇未完成的任務並按用戶分組。
若是不使用Ramda的compose,而是本身手動實現函數組合,則須要寫一個這樣的函數:
// (if created by hand)
var activeByUser = function(tasks) {
return groupByUser(incomplete(tasks));
};
複製代碼
使用Ramda的好處就是不用每次手動實現函數組合。組合是函數式編程的關鍵技術之一。讓咱們多考慮一些狀況。若是咱們須要按截止日期對每一個用戶的todolist進行排序呢?
var sortUserTasks = R.compose(R.map(R.sortBy(R.prop("dueDate"))), activeByUser);
複製代碼
觀察力強的讀者可能已經注意到咱們能夠將上述全部內容合併起來。既然咱們的compose函數容許兩個以上的參數,爲何不在一個步驟中完成全部這些工做呢?
var sortUserTasks = R.compose(
R.mapObj(R.sortBy(R.prop('dueDate'))),
groupByUser,
R.filter(R.where({complete: false})
);
複製代碼
若是您沒有其餘地方調用函數activebyuser和incomplete,這樣寫多是合理的。可是,它也會使調試變得更困難,而且不會增長代碼的可讀性。
事實上,我認爲咱們不該該把全部函數合併成一個函數。應該拆分可重用的部分。若是咱們這樣作,可能會更好:
var sortByDateDescend = R.compose(R.reverse, sortByDate);
var sortUserTasks = R.compose(R.mapObj(sortByDateDescend), activeByUser);
複製代碼
若是咱們肯定咱們只想先按最近的日期排序,那麼咱們能夠只單獨保留SortByDatedDescend函數。若是業務有按升序或降序對數據進行排序兩種需求,應該保留sortByDate和sortByDateDescend函數都在,方便後續的組合。
咱們這回尚未處理數據。這是怎麼回事?沒有數據的數據處理只是過程。耐心寫,當您使用函數式編程時,您所獲得的只是組成管道的函數。一個函數向下一個函數提供數據,下一個函數向下下個函數提供數據,依此類推,直到須要的結果從末尾流出。
到目前爲止,咱們已經構建瞭如下函數:
incomplete: [Task] -> [Task]
sortByDate: [Task] -> [Task]
sortByDateDescend: [Task] -> [Task]
activeByUser: [Task] -> {String: [Task]}
sortUserTasks: {String: [Task]} -> {String: [Task]}
複製代碼
咱們已經使用前面的函數來構建sortUserTasks,也能夠單獨使用這些函數。這裏面的activeByUser函數,其中的groupByUser函數,我尚未實現。咱們要怎樣編寫它呢?
如下是groupByUser函數的實現:
var groupByUser = R.partition(R.prop('username'));
複製代碼
從任務列表中選擇前五個元素,咱們可使用ramda的take函數,咱們能夠這樣作:
var topFiveUserTasks = R.compose(R.mapObj(R.take(5)), sortUserTasks);
複製代碼
咱們只須要返回的對象中屬性的子集,好比標題和截止日期。在這個數據結構中,用戶名顯然是多餘的,咱們不想傳遞給其餘系統。
咱們可使用Ramda的相似於SQL select函數的方法來實現這一點,該函數被稱爲project:
var importantFields = R.project(['title', 'dueDate']);
var topDataAllUsers = R.compose(R.mapObj(importantFields), topFiveUserTasks);
複製代碼
如今,咱們的todolist應用程序中,可能有下面這些函數:
var incomplete = R.filter(R.where({complete: false}));
var sortByDate = R.sortBy(R.prop('dueDate'));
var sortByDateDescend = R.compose(R.reverse, sortByDate);
var importantFields = R.project(['title', 'dueDate']);
var groupByUser = R.partition(R.prop('username'));
var activeByUser = R.compose(groupByUser, incomplete);
var topDataAllUsers = R.compose(R.mapObj(R.compose(importantFields,
R.take(5), sortByDateDescend)), activeByUser);
複製代碼
如今是將數據傳遞到函數中的時候了。這些函數都接受相同類型的數據,即一個todo項數組。咱們沒有具體描述這些項目的結構,但咱們知道它們必須至少具備如下屬性:
complete: Boolean
dueDate: String, formatted YYYY-MM-DD
title: String
userName: String
因此,若是咱們有一個任務數組,咱們如何使用它?以下:
var results = topDataAllUsers(tasks);
複製代碼
使用起來就是這麼簡單。 結果是一個對象,以下:
{
Michael: [
{dueDate: '2014-06-22', title: 'Integrate types with main code'},
{dueDate: '2014-06-15', title: 'Finish algebraic types'},
{dueDate: '2014-06-06', title: 'Types infrastucture'},
{dueDate: '2014-05-24', title: 'Separating generators'},
{dueDate: '2014-05-17', title: 'Add modulo function'}
],
Richard: [
{dueDate: '2014-06-22', title: 'API documentation'},
{dueDate: '2014-06-15', title: 'Overview documentation'}
],
Scott: [
{dueDate: '2014-06-22', title: 'Complete build system'},
{dueDate: '2014-06-15', title: 'Determine versioning scheme'},
{dueDate: '2014-06-09', title: 'Add `mapObj`'},
{dueDate: '2014-06-05', title: 'Fix `and`/`or`/`not`'},
{dueDate: '2014-06-01', title: 'Fold algebra branch back in'}
]
}
複製代碼
一樣,咱們也能夠將任務數組傳遞給incomplete函數,獲得一個篩選後的列表:
var incompleteTasks = incomplete(tasks);
複製代碼
結果以下:
[
{
username: 'Scott',
title: 'Add `mapObj`',
dueDate: '2014-06-09',
complete: false,
effort: 'low',
priority: 'medium'
}, {
username: 'Michael',
title: 'Finish algebraic types',
dueDate: '2014-06-15',
complete: false,
effort: 'high',
priority: 'high'
} /*, ... */
]
複製代碼
固然,您也能夠將任務數組傳遞給sortbydate、sortbydatedescend、importantfields、byuser或activebyuser。由於這些都在相似的類型上運行——任務數組——咱們能夠經過簡單的組合構建一個大型的工具集合。
如今又有了一個新需求,咱們的項目又要支持一個新特性,爲特定用戶篩選任務列表。擁有同上面相同的篩選,排序等功能。
var gloss = R.compose(importantFields, R.take(5), sortByDateDescend);
var topData = R.compose(gloss, incomplete);
var topDataAllUsers = R.compose(R.mapObj(gloss), activeByUser);
var byUser = R.use(R.filter).over(R.propEq("username"));
複製代碼
下面是使用方式:
var results = topData(byUser('Scott', tasks));
複製代碼
能夠,如:
var incomplete = R.filter(R.where({complete: false}));
複製代碼
咱們不先獲得複合函數,再操做,而是直接獲得數據結果:
var incompleteTasks = R.filter(R.where({complete: false}), tasks);
複製代碼
全部其餘主要函數也是如此:只需在調用結束時添加一個tasks參數,就能夠返回數據。
Ramda的一個主要特性。就是全部函數都是自動柯里化的。這意味着,若是您沒有提供函數指望的全部參數,將返回一個新的函數,此函數緩存了已經傳遞的參數,指望剩餘的參數。上面的代碼中,就是使用了柯里化這一特性,好比R.filter期待兩個參數,咱們只傳遞給它一個,那麼它就返回一個新函數,指望再傳遞給新函數一個參數,才執行獲得篩選出的最終數據。
自動柯里化特性,加上Ramda這種函數優先,數據最後的API設計風格,使Ramda很是適合編寫函數式編程風格。
node環境使用npm安裝:
npm install ramda
var R = require('ramda')
複製代碼
瀏覽器環境:
<script src="path/to/yourCopyOf/ramda.js"></script>
複製代碼
或
<script src="path/to/yourCopyOf/ramda.min.js"></script>
複製代碼
或使用一些CDN連接。
英文原文地址:fr.umio.us/why-ramda/