不要害怕函數式編程
函數式編程是時髦的編程范式。最初移交給計算機科學學術界,由于在分布式系統上很大的實用性, 有了函數式編程最近的復興。 (也可能因為像Haskell這樣的給他們一定的聲望的函數式語言很難把握)。
當一個系統的性能和完整性都很關鍵的時候,嚴格的函數式編程語言被特別使用。即——你期望每次你的程序恰恰需要做什么并且需要在它的任務在成百上千的計算機網絡的環境中操作。
例如,Clojure為非死book等公司所使用的大量內容傳送網絡Akamai提供支持,而推ter則以Scala為其最具性能密集型組件,而Haskell則被AT&T用于其網絡安全系統。
這些語言對大多數前端Web開發人員有一個陡峭的學習曲線; 然而,更多的平易近人的語言結合了功能編程的特性,最突出的是Python,它的核心庫,函數map和reduce(我們將討論一下),以及諸如Fn.py的庫,以及JavaScript ,再次使用收集方法,但也與像Underscore.js和Bacon.js的庫。
函數式編程可能會讓人望而生畏,但請記住,這不是博士,數據科學家和宇航員。對于大多數人來說,采用功能性風格的真正好處是我們的程序可以被分解成更小、更簡單,更可靠和更容易理解。如果你是一個前端開發人員處理數據,特別是如果你使用D3格式化,數據可視化,拉斐爾或類似的,那么函數式編程將是你的兵器庫里的重要武器。
找到一個一致的定義,函數式編程是艱難的,大多數的文獻依賴預感的狀態如“函數作為一類對象,”和“消除副作用。“以防不彎曲你的大腦進入海里,更多的理論水平,函數式編程通常是解釋的演算(一些實際上認為,函數式編程基本上是數學)——但是你可以放松。從更加務實的角度來看,一個初學者只需要了解兩個概念為了使用它為日常應用程序(不需要微積分!)。
首先,數據在函數式程序應該是不可變的,這聽起來很嚴重,但只是意味著它永遠不應該改變。起初,這可能看上去很奇怪(畢竟,誰需要一個程序不會改變什么?),但在實踐中,您只需創建新的數據結構,而不是修改已經存在的。例如,如果您需要操作一些數據在數組中,然后你會創建一個新數組與更新的值,而不是修改原始數組。簡單!
其次,功能程序應該是無狀態的,這基本上意味著他們應該執行每個任務,好像第一次沒有的知識可能會或可能不會發生在程序的執行(你可能會說,一個無狀態的程序是無知的過去)。加上不變性,這有助于我們認為每個函數如果操作在真空中,幸福地無知的其他應用程序除了其他功能。在更具體的術語中,這意味著您的功能將只在數據作為參數傳遞,不會依賴外部值來執行他們的計算。
不變性和無狀態性是函數式編程的核心和理解很重要,但是不要擔心,如果他們還不很有意義。你會熟悉這些原則的文章中,我承諾,美,精度和函數式編程的力量將把應用程序變成明亮,閃閃發光,data-chomping彩虹。現在,開始簡單的函數,返回數據(或其他功能),然后結合這些基本構建塊來執行更復雜的任務。
例如,讓我們看一個API響應:
var data = [
{
name: "Jamestown",
population: 2047,
temperatures: [-34, 67, 101, 87]
},
{
name: "Awesome Town",
population: 3568,
temperatures: [-3, 4, 9, 12]
}
{
name: "Funky Town",
population: 1000000,
temperatures: [75, 75, 75, 75, 75]
}
];
如果我們想使用圖表或圖形庫比較平均溫度人口規模,我們需要編寫一些JavaScript使前幾個修改數據的可視化格式正確。我們的圖形庫希望x和y坐標的數組,如下所示:
[
[x, y],
[x, y]
…etc
]
這里x是平均溫度,y是人口規模。
沒有函數式編程(或不使用所謂的“命令式”風格),我們的程序可能看起來像這樣:
var coords = [],
totalTemperature = 0,
averageTemperature = 0;
for (var i=0; i < data.length; i++) {
totalTemperature = 0;
for (var j=0; j < data[i].temperatures.length; j++) {
totalTemperature += data[i].temperatures[j];
}
averageTemperature = totalTemperature / data[i].temperatures.length;
coords.push([averageTemperature, data[i].population]);
}
即使在一個人為的例子,這已經變得困難。看看我們能做得更好。
編程功能風格時,你總是在尋找簡單、可重復的行為可以被抽象成一個函數。我們可以構建更復雜的功能通過調用這些函數序列(也稱為“組合”功能)——更多詳情。與此同時,讓我們看看我們采取的步驟在轉變的過程中需要的初始API應對結構可視化圖書館。在基本層面上,我們將我們的數據上執行以下操作:
-
添加每個數字到列表中
-
計算一個平均值
-
從一個列表對象中檢索單個屬性
我們會為每一個寫一個函數三個基本動作,然后從這些功能組成我們的節目。函數式編程可能會讓人有些迷惑,和你可能會陷入舊命令式的習慣。為了避免那樣,這里有一些簡單的基本規則,以確保你遵循最佳實踐:
-
所有的方法必須接收至少一個參數
-
所有方法必須返回數據或者另一個方法
-
沒有循環
好的,讓我們添加列表中的每個數字。記住這些規則,讓我們確保我們的函數接受一個參數(數字添加)的數組,并返回一些數據。
function totalForArray(arr) {
return total;
}
目前為止一切都很順利。但是我們如何訪問列表中的每一項如果我們不循環嗎?向你的新朋友問好,遞歸!這是有點棘手,但基本上,當你使用遞歸,你創建一個函數調用本身,除非特定的條件滿足,在這種情況下,返回一個值。看一個例子可能是簡單的:
function totalForArray(currentTotal, arr) {
currentTotal += arr[0];
var remainingList = arr.slice(1);
if(remainingList.length > 0) {
return totalForArray(currentTotal, remainingList);
}
else {
return currentTotal;
}
}
警告:遞歸將使程序更具可讀性,它在功能性編程風格至關重要。然而,在一些語言(包括JavaScript),你就會遇到問題,當您的程序使得大量遞歸調用單個操作(在撰寫本文時,“大”是在Chrome中10000個調用,在Firefox中50000個調用并且在node.js中50000個調用)。細節超出了本文的范圍,但要點是,至少在ECMAScript6發布之前,JavaScript不支持所謂的“尾遞歸,”,是一種更有效的遞歸。這是一個高級主題,不會經常出現,但它是值得了解的。
請記住,我們需要從溫度數組中計算總共的溫度而不是計算溫度平均值,我們可以簡單地寫這個:
var totalTemp = totalForArray(0, temperatures);
如果你純粹主義者,你可能會說,我們totalForArray功能可以進一步分解。例如,把兩個數字加在一起的任務可能會出現在您的應用程序的其他部分,隨后應該是自己的函數。
function addNumbers(a, b) {
return a + b;
}
現在,我們totalForArray功能看起來是這樣的:
function totalForArray(currentTotal, arr) {
currentTotal = addNumbers(currentTotal, arr[0]);
var remainingArr = arr.slice(1);
if(remainingArr.length > 0) {
return totalForArray(currentTotal, remainingArr);
}
else {
return currentTotal;
}
}
太好了!返回一個值從一個數組在函數式編程中相當普遍,以至于它有一個特別的名字,“減少”,你就會更常聽到一個動詞,當你“減少單個值數組。“JavaScript有一種特殊的方法來執行常見的任務。Mozilla開發人員網絡提供了一個完整的解釋,但對我們來說就是這么簡單:
var totalTemp = temperatures.reduce(function(previousValue, currentValue){
return previousValue + currentValue;
});
但是,嘿,因為我們已經定義了一個addNumber函數,我們可以用這個代替。
var totalTemp = temperatures.reduce(addNumbers);
事實上,因為總計數組太酷了,讓我們把它變成自己的函數,這樣我們可以再次使用它而不需要記住所有的令人困惑的東西關于減少和遞歸。
function totalForArray(arr) {
return arr.reduce(addNumbers);
}
var totalTemp = totalForArray(temperatures);
啊,現在是一些可讀的代碼!你知道方法,如減少最常見的函數式編程語言。這些輔助方法對數組代替循環執行的行為通常被稱為“高階函數”。
始終,我們列出的第二個任務是計算平均。這是非常容易的。
function average(total, count) {
return total / count;
}
我們如何得到整個數組的平均嗎?
function averageForArray(arr) {
return average(totalForArray(arr), arr.length);
}
var averageTemp = averageForArray(temperatures);
希望你開始看到如何結合函數來執行更復雜的任務。這是可能的因為我們遵守規則制定在本文的開始——即,我們的函數必須接受參數和返回數據。非常棒。
最后,我們想要獲取一個屬性從一個對象數組。而不是顯示你更多的遞歸的例子,我開門見山,知道你在另一個內置的JavaScript方法:地圖。這種方法是當你有一個數組和一個結構,需要將它映射到另一個結構,像這樣:
var allTemperatures = data.map(function(item) {
return item.temperatures;
});
這是很酷,但把一個屬性從一個對象集合是你將會做所有的時間,所以讓我們做一個函數。
function getItem(propertyName) {
return function(item) {
return item[propertyName];
}
}
檢查一下:我們做了一個函數,它返回一個函數!現在我們可以通過它來映射方法是這樣的:
var temperatures = data.map(getItem('temperature'));
如果你喜歡細節,我們可以這樣做的原因是,在JavaScript中,函數是“一流的對象,這基本上意味著你可以通過在功能就像任何其他值。雖然這是一個許多編程語言的特性,這是一個可以使用任何語言,要求在函數式風格。順便說一句,這也是為什么你能做類似 $(“#my-element”).on('click', function(e) ...) 。在方法的第二個參數是一個函數,當你將函數作為參數進行傳遞,你使用它們就像在命令式語言使用值。很整潔。
最后,讓我們用調用的函數映射到使事情更加可讀。
function pluck(arr, propertyName) {
return arr.map(getItem(propertyName));
}
var allTemperatures = pluck(data, 'temperatures');
好了,現在我們有一個工具包的通用函數我們可以用在我們的應用程序的任何地方,甚至在其他項目。我們可以總結項目一個數組,數組的平均值,使新的對象數組通過采集屬性列表。最后但并非最不重要,讓我們回到我們最初的問題:
var data = [
{
name: "Jamestown",
population: 2047,
temperatures: [-34, 67, 101, 87]
},
…
];
我們需要像上面的數組對象轉換成一個數組的x,y對,是這樣的:
[
[75, 1000000],
…
];
這里x是平均溫度,y是總人口。首先,讓我們分離,我們需要的數據。
var populations = pluck(data, 'population');
var allTemperatures = pluck(data, 'temperatures');
現在,讓我們做一個數組的平均值。記住,我們通過映射函數將數組中的每一項被稱為;因此,通過函數的返回值將被添加到一個新數組,這最終將分配給新數組averageTemps變量。
var averageTemps = allTemperatures.map(averageForArray);
目前為止一切都很順利。但是現在我們有兩個數組:
// populations
[2047, 3568, 1000000]
// averageTemps
[55.25, 5.5, 75]
顯然,我們想要的只有一個數組,讓我們寫一個函數將它們結合起來。我們的函數應該確保項目在第一個數組索引0搭配項目在第二個數組的索引0,等等為索引1到n(n是數組中的總項數)。
function combineArrays(arr1, arr2, finalArr) {
finalArr = finalArr || [];
finalArr.push([arr1[0], arr2[0]]);
var remainingArr1 = arr1.slice(1),
remainingArr2 = arr2.slice(1);
if(remainingArr1.length === 0 && remainingArr2.length === 0) {
return finalArr;
}
else {
return combineArrays(remainingArr1, remainingArr2, finalArr);
}
};
var processed = combineArrays(averageTemps, populations);
或者,因為俏皮話是有趣的:
var processed = combineArrays(pluck(data, 'temperatures').map(averageForArray), pluck(data, 'population'));
讓我們得到真正的
最后,讓我們來看一個真實世界的例子,這一次增加我們與強調的功能直到之后。js JavaScript庫,提供了許多函數式編程的幫手。我們會把數據從一個平臺的沖突和災難信息,我一直在做CrisisNET命名,并且我們將使用神奇的D3庫數據的可視化。
我們的目標是讓人們來CrisisNET的主頁快速快照的類型的信息系統。為了說明這一點,我們可以計算從API文檔的數量分配給一個特定的類別,如“暴力”或“武裝沖突。“通過這種方式,用戶可以看到多少信息是他們最感興趣的主題上可用。
泡沫圖表或許是不錯的選擇,因為他們通常用來代表大型群體的相對大小。幸運的是,D3有一個內置的可視化包命名這一目的。所以,讓我們創建一個圖形與包裝顯示給定類別的次數的名字出現在回應CrisisNET的API。
在我們繼續之前,注意,D3認股權證自己的教程是一個復雜的庫(或許多教程)。由于本文重點是函數式編程,我們不會花很多時間在D3是如何工作的。不過別擔心,如果你不熟悉圖書館,你應該能夠復制粘貼代碼片段特定D3和深入細節。斯科特?莫里的D3教程是一個偉大的資源,如果你對學習更感興趣。
沿著,首先確保我們有一個DOM元素,這D3有地方放圖生成與我們的數據。
<div id="bubble-graph"></div>
現在,讓我們創建一個表格并且把它添加到dom。
var diameter = 960,
format = d3.format(",d"),
color = d3.scale.category20c(),
var bubble = d3.layout.pack().sort(null).size([diameter, diameter]).padding(1.5);
var svg = d3.select("#bubble-graph").append("svg").attr("width", diameter).attr("height", diameter).attr("class", "bubble");
pack 對象需要一個對象數組的格式:
{
children: [
{
className: ,
package: "cluster",
value:
}
]
}
CrisisNET的數據api返回如下格式:
{
data: [
{
summary: "Example summary",
content: "Example content",
…
tags: [
{
name: "physical-violence",
confidence: 1
}
]
}
]
}
我們可以看到,每個文檔都有一個標簽屬性,屬性包含一個條目數組。每個標簽項有一個名稱屬性,它是我們所追求的。我們需要找到每一個獨特的標記名CrisisNET API的響應和計數,標記名稱出現的次數。先隔離的信息我們需要用勇氣我們前面創建的函數。
var tagArrays = pluck(data, 'tags');
給予一個像下面這樣的數組:
[
[
{
name: "physical-violence",
confidence: 1
}
],
[
{
name: "conflict",
confidence: 1
}
]
]
然而,我們真正想要的是一個數組,每個標記。所以,讓我們從Underscore.js里使用一個名叫flatten的方法。這將把值從任何嵌套的數組和數組給我們一層深。
var tags = _.flatten(tagArrays);
現在,我們的數組比較容易處理了:
[
{
name: "physical-violence",
confidence: 1
},
{
name: "conflict",
confidence: 1
}
]
我們可以用勇氣再次讓我們真正想要的東西,這是一個簡單的只有標記名稱列表。
var tagNames = pluck(tags, 'name');
[
"physical-violence",
"conflict"
]
啊,那是更好的。
現在我們的任務相對簡單的計算每個標記的次數的名字出現在我們的列表,然后轉換成所需的結構D3包我們前面創建的布局。正如你可能已經注意到的,數組是一個非常受歡迎的數據結構在函數式編程中,大部分的工具是設計時考慮到數組。作為第一步,然后,我們將創建一個數組:
[
[ "physical-violence", 10 ],
[ "conflict", 27 ]
]
這里,數組中的每一項的標記名稱索引0和標簽的總計數指數1。我們想要為每個獨特的標記名稱只有一個數組,我們首先創建一個數組中的每個標記名稱只出現一次。幸運的是,一個下劃線。js方法存在只是為了這個目的。
var tagNamesUnique = _.uniq(tagNames);
讓我們也擺脫任何false-y(false,null,”“,等等)值使用另一個方便的下劃線。js函數。
tagNamesUnique = _.compact(tagNamesUnique);
從這里,我們可以編寫一個函數,生成數組使用另一個內置的JavaScript收集方法,名叫過濾器,過濾數組基于一個條件。
function makeArrayCount(keys, arr) {
return keys.map(function(key) {
return [
key,
arr.filter(function(item) { return item === key; }).length
]
});
}
我們現在可以輕松地創建包需要的數據結構映射列表的數組。
var packData = makeArrayCount(tagNamesUnique, tagNames).map(function(tagArray) {
return {
className: tagArray[0],
package: "cluster",
value: tagArray[1]
}
});
最后,我們可以將我們的數據傳遞給D3和生成SVG DOM節點,每個獨特的標記名稱,一個圓的大小相對于總次數的標記名稱出現在CrisisNET的API的回應。
function setGraphData(data) {
var node = svg.selectAll(".node")
.data(bubble.nodes(data)
.filter(function(d) { return !d.children; }))
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.append("circle")
.attr("r", function(d) { return d.r; })
.style("fill", function(d) { return color(d.className); });
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.style("font-size", "10px")
.text(function(d) { return d.className } );
}
把它們放在一起,這是setGraphData makeArray函數上下文,包括調用CrisisNET使用jQuery API(你需要得到一個API密匙)。我也發布了一個完全工作示例在GitHub上。
function processData(dataResponse) {
var tagNames = pluck(_.flatten(pluck(dataResponse.data, 'tags')), 'name');
var tagNamesUnique = _.uniq(tagNames);
var packData = makeArrayCount(tagNamesUnique, tagNames).map(function(tagArray) {
return {
className: tagArray[0],
package: "cluster",
value: tagArray[1]
}
});
return packData;
}
function updateGraph(dataResponse) {
setGraphData(processData(dataResponse));
}
var apikey = // Get an API key here: http://api.crisis.net
var dataRequest = $.get('http://api.crisis.net/item?limit=100&apikey=' + apikey);
dataRequest.done( updateGraph );
這是個很深入,所以祝賀堅持它!正如我提到的,這些概念可能是一個挑戰,但抵制誘惑敲定為你的余生循環。
使用函數式編程技術在幾周內,你會很快建立一套簡單、可重用的功能,將極大地提高應用程序的可讀性。加上,你可以操作數據結構更迅速,敲出過去30分鐘的令人沮喪的調試幾行代碼。一旦你的數據格式正確,你將會花更多的時間在有趣的部分:使可視化看起來太棒了!
來自:http://mp.weixin.qq.com/s/Q56krgED0NYNNGmEqFP16w