用 JS 代碼解釋 Java Stream

引言

前不久,公司後端同事找到我,邀請我在月會上分享函數式編程,我說你仍是另請高明吧…… 我也不是謙虛,我一個前端頁面仔,怎麼去給以 Java 後端開發爲主的技術部講函數式編程呢?可是同事說你仍是試試吧。而後我就去先試着準備下。前端

因爲我最近在學函數式領域建模(Functional Domain Modeling),一開始我想講下 Scala,而後我找到了 Functional and Reactive Domain Modeling 這本書。可是轉念一想,我都沒寫事後端,對後端的業務場景根本不瞭解,看本書就去講,有些不尊重聽衆智商了。最後我打算在 Java 裏找些接地氣的內容來分享,而後我就去學了 Java,發現了 Java 裏面有 Stream 這麼好的函數式一級語言支持。express

最終通過謹慎考慮後,我仍是沒有去分享。我沒有業務上的理解,僅僅去講些語法特性和奇技淫巧,有些班門弄斧了。我在學習 Stream 的過程當中有一些發現,如今把這些發現分享出來。編程

本文代碼在我上一篇文章中出現過,但此次解釋更詳細。後端

JavaScript 更適合用來學習一些 FP 概念

本文無心拋出 JavaScript 和 Java 誰比誰好這種無聊的論斷,我僅想指出,JS 這種弱類型語言能夠避免一些語法噪音,讓初學者在學習一些 FP 概念時,快速直抵核心,理解本質。數組

在學 Java Stream 時,要先學 lambda expression, method reference, 而後學 Interface 和 Functional Interface, 學這些僅僅爲了支持 lambda 傳參。而用 JS 的話,函數順手就寫,順手就傳,不用那麼多準備儀式。app

再次聲明一下,Java 這種面向對象特性有其強大之處。這裏僅僅指出在學習理解一些 FP 概念上,JS 更簡單靈活。dom

理解 Stream

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 會跳過當前值,持續遞歸,直到遍歷到符合條件的值纔將值返回出去。

mapfilter 是最重要的高階操做函數,其它的操做函數就不展開解釋了。

在提供了高階操做函數以後,咱們要提供一個函數將這些高階函數組合起來。

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 循環,還攻擊用前者的人是在玩語法遊戲,是否是很傻?

相關文章
相關標籤/搜索