前不久,公司後端同事找到我,邀請我在月會上分享函數式編程,我說你仍是另請高明吧…… 我也不是謙虛,我一個前端頁面仔,怎麼去給以 Java 後端開發爲主的技術部講函數式編程呢?可是同事說你仍是試試吧。而後我就去先試着準備下。前端
因爲我最近在學函數式領域建模(Functional Domain Modeling),一開始我想講下 Scala,而後我找到了 Functional and Reactive Domain Modeling 這本書。可是轉念一想,我都沒寫事後端,對後端的業務場景根本不瞭解,看本書就去講,有些不尊重聽衆智商了。最後我打算在 Java 裏找些接地氣的內容來分享,而後我就去學了 Java,發現了 Java 裏面有 Stream 這麼好的函數式一級語言支持。express
最終通過謹慎考慮後,我仍是沒有去分享。我沒有業務上的理解,僅僅去講些語法特性和奇技淫巧,有些班門弄斧了。我在學習 Stream 的過程當中有一些發現,如今把這些發現分享出來。編程
本文代碼在我上一篇文章中出現過,但此次解釋更詳細。後端
本文無心拋出 JavaScript 和 Java 誰比誰好這種無聊的論斷,我僅想指出,JS 這種弱類型語言能夠避免一些語法噪音,讓初學者在學習一些 FP 概念時,快速直抵核心,理解本質。數組
在學 Java Stream 時,要先學 lambda expression, method reference, 而後學 Interface 和 Functional Interface, 學這些僅僅爲了支持 lambda 傳參。而用 JS 的話,函數順手就寫,順手就傳,不用那麼多準備儀式。app
再次聲明一下,Java 這種面向對象特性有其強大之處。這裏僅僅指出在學習理解一些 FP 概念上,JS 更簡單靈活。dom
Stream 能夠拆解成三個部分:函數式編程
如上圖,Stream 可分爲數據源,管道數據操做,數據消費三個部分。源數據生成後,流經管道,最終在管道終端被消費。最終的消費多是數據流被聚合成新的數據集,也多是逐個執行反作用(forEach
)。函數
下面用 JS 代碼解釋這三個部分。學習
數據源能夠理解成是一個 generator 函數被反覆執行,生成數據流。什麼是 generator 呢?最簡單的 generator 能夠是這樣:
const getRandNum = () => Math.floor(Math.random() * 100);
複製代碼
每次執行 getRandNum
它都隨機生成一個 0 到 100 的整數,它知足咱們對 generator 行爲的期待。能夠看出 generator 其實就是一個動態生成數據的行爲,那若是數據源是靜態數據集,怎麼獲得這種動態 generator 呢?很簡單:
function getGeneratorFromList(list) {
let index = 0;
return function generate() {
if (index < list.length) {
const result = list[index];
index += 1;
return result;
}
};
}
// 例子:
const generate = getGeneratorFromList([1, 2, 3]);
generate(); // 1
generate(); // 2
generate(); // 3
generate(); // undefined
複製代碼
給 getGeneratorFromList
傳個數組,它會返回一個 generator,這個 generator 每被執行一次都吐出傳入數組當前遍歷到的元素。這裏只考慮數組,其它狀況很容易擴展。
數據源部分就講完了。其實很簡單。
數據源生成後,就進入管道了。管道里對原 generator 進行各類轉換,組合生成符合期待的 generator。注意上一句的描述,管道里僅僅是基於原 generator 函數生成新的 generator 函數,計算行爲並不會被觸發。
咱們給管道里塞入一些高階操做函數,這些高階操做函數接受前一個函數吐出的 generator,返回加入了新行爲的 generator。咱們先來定義一個 map
:
function map(mapping) {
return function(generate) {
return function mappedGenerator() {
const value = generate();
if (value !== undefined) {
return mapping(value);
}
};
};
}
複製代碼
map
函數連續返回了兩個函數。這裏先簡要解釋下爲何這麼作,可能會比較難懂,等你看了後面的代碼再回過頭來看會更容易理解些。當用戶調用 map
並將結果傳入管道時,map
返回了第二層函數。當管道組合被觸發時,第二層函數被執行,最終 generator 函數被返回,而後返回的 generator 被傳給管道里的下一個高階操做函數。
再來定義一個 filter
:
function filter(predicate) {
return function(generate) {
return function filteredGenerator() {
const value = generate();
if (value !== undefined) {
if (predicate(value)) {
return value;
}
return filteredGenerator();
}
};
};
}
複製代碼
當判斷條件不知足時,generator 會跳過當前值,持續遞歸,直到遍歷到符合條件的值纔將值返回出去。
map
和 filter
是最重要的高階操做函數,其它的操做函數就不展開解釋了。
在提供了高階操做函數以後,咱們要提供一個函數將這些高階函數組合起來。
function Stream(source) {
let initialGenerator;
if (Array.isArray(source)) {
initGenerator = getGeneratorFromList(source);
} else {
initialGenerator = source;
}
function pipe(...operators) {
return operators.reduce((prev, current) => current(prev), initialGenerator);
}
return { pipe };
}
複製代碼
本文就只考慮數據源是 generator 函數和數組兩個狀況了。這種考慮固然是不嚴謹的,但足以解釋 Stream 的實現。
pipe
函數將操做函數從左往右依次執行,並將上一個函數執行的結果傳給下一個函數。如此則完成了管道組合操做。
前面兩步完成後,就剩下如何將動態的 generator 轉換成靜態的數據集了(forEach 執行反作用本文就不考慮了)。這一步也比較簡單:
function toList(generate) {
const arr = [];
let value = generate();
while (value !== undefined) {
arr.push(value);
value = generate();
}
return arr;
}
複製代碼
這裏就只展現生成數組了,其它數據類型讀者可自行擴展。
至此,完整的 Stream 就實現了。固然,操做符支持上還不完整,但你能明白個人意思。
再定義一個 take
,而後測試下:
function take(n) {
return function(generate) {
let count = 0;
return function() {
if (count < n) {
count += 1;
return generate();
}
};
};
}
Stream(getRandNum).pipe(
filter(x => x % 2 === 1),
take(10),
toList
);
// => 10 個隨機奇數
複製代碼
注意,getRandNum
永遠都不會返回 undefined
,那爲何 toList
沒有進入死循環?這是由於 take
給原 generator 加入了新的行爲,讓它只能返回 10 個有效值。這也是惰性求值的魅力。
更多高階操做符以下:
function skipWhile(predicate) {
return function(generate) {
let startTaking = false;
return function skippedGenerator() {
const value = generate();
if (value !== undefined) {
if (startTaking) {
return value;
} else if (!predicate(value)) {
startTaking = true;
return value;
}
return skippedGenerator();
}
};
};
}
function takeWhile(predicate) {
return function(generate) {
return function() {
const value = generate();
if (value !== undefined) {
if (predicate(value)) {
return value;
}
}
};
};
}
複製代碼
Java 裏面的 Stream 底層實現我並無學習,我只學了下 API 用法。可是從 Stream API 的行爲特性來推斷,其底層實現應該和本文展現的 JS 實現思想是相通的。
前幾天在知乎上偶然發現有人未經我受權把我去年發表的《如何在 JS 裏面消滅 for 循環》轉載了。我看了下評論,比在掘金被罵的還慘。沒想到在掘金被罵了一輪還要在知乎上再被罵一輪……
若是你寫 Java,Java 8 給你提供了 Stream 你不用,恰恰要用 for 循環,還攻擊用前者的人是在玩語法遊戲,是否是很傻?