補充一個替代 for 循環的新姿式

本文英文版發表在 Lei's Blogjavascript

我也沒想到我還在這個問題上死磕……java

最近複習之前的知識點,發現以前鑽的不夠深,而後繼續開了下腦洞,而後就有了今天要寫的內容。git

可能有不熟悉背景的朋友,這裏我簡單介紹下。我以前在掘金寫了一篇很引戰的文章《如何在 JS 代碼中消滅 for 循環》。這個標題嚴謹推敲一下是不合適的,但已經成歷史了,我就不改了。後來我又解釋了爲何要在特定場合下避免 for 循環,這裏就不贅述了。接下來我要寫的代碼也包含大量 for 循環,這和我以前的解釋並不衝突。github

去年學習 Haskell 的時候發現 Haskell 的一種寫法很簡潔和優雅,很難用 JS 來實現。那段代碼長這樣:安全

sum (takeWhile (<10000) (filter odd (map (^2) [1..])))
複製代碼

我當時就想在 JS 裏面怎麼用高階函數實現這段代碼。固然我是想不出來的,而後我 Google 搜了一下,還真有人寫了。Lazy Evaluation in JavaScript with Generators, Map, Filter, and Reduceapp

我基於這篇文章,加了一個 takeWhile 方法,而後用 JS 翻譯了上面那段 Haskell 代碼。而後我寫了一篇文章,逐塊解釋代碼,發表在掘金(後來刪了)。前時間我把這篇文章從新分享出來,而後就有人質疑我從哪抄的代碼。異步

我確實一開始沒有標明出處,只有認了。同時別人的監督也促使我檢討我爲何不基於別人的成果擴展些本身的東西?函數

我接下來要基於原代碼從兩個方向擴展。oop

一是將 class 改寫成工廠函數。這裏不想再引發爭議,我不是提倡不要用 class。在 Node 開發中,爲了性能是要用 class 的。這裏折騰改寫出於兩個緣由:性能

  1. 經過改寫完全弄懂代碼。
  2. 我的偏好上我更喜歡工廠函數。擴展性和安全性上,工廠函數更優(歡迎證實我是錯的)。

二是將原文 Lazy 函數改寫成接受任何 iterable(原文只接受無限 iterator),並加上經常使用的列表操做高階函數。這一步完成後,咱們其實就能用 Lazy 來實現大數據集遍歷了。我以前解決這個問題是用的 Transducer。惰性求值和 transduce 原理不同,但目的有重合。

我首先用工廠函數改寫原版:

const Lazy = iterator => {
  const next = iterator.next.bind(iterator)

  const map = f => {
    const modifiedNext = () => {
      const item = next()
      const mappedValue = f(item.value)
      return {
        value: mappedValue,
        done: item.done,
      }
    }
    const newIter = { ...iterator, next: modifiedNext }
    return Lazy(newIter)
  }

  const filter = predicate => {
    const modifiedNext = () => {
      while (true) {
        const item = next()
        if (predicate(item.value)) {
          return item
        }
      }
    }
    const newIter = { ...iterator, next: modifiedNext }
    return Lazy(newIter)
  }

  const takeWhile = predicate => {
    const result = []
    let value = next().value
    while (predicate(value)) {
      result.push(value)
      value = next().value
    }
    return result
  }

  return Object.freeze({
    map,
    filter,
    takeWhile,
    next,
  })
}

const numbers = function*() {
  let i = 1
  while (true) {
    yield i++
  }
}

Lazy(numbers())
  .map(x => x ** 2)
  .filter(x => x % 2 === 1)
  .takeWhile(x => x < 10000)
  .reduce((x, y) => x + y)
// => 16650
複製代碼

以上代碼在我博客中有逐步解釋。

接着擴展 Lazy 函數,讓它接受任何 Iterable 和自定義 Generator:

/* * JS 中判斷 generator 有些麻煩,我用了這個庫: * https://github.com/ljharb/is-generator-function */

const Lazy = source => {
  if (typeof source[Symbol.iterator] !== 'function' && !isGeneratorFunction(source))
    throw new Error('The source input must be an iterable or a generator function')

  const iterator = isGeneratorFunction(source)
    ? source()
    : source[Symbol.iterator]();

  const _lazy = it => {
    const next = it.next.bind(it)

    const map = f => {
      const modifiedNext = () => {
        const item = next()
        const mappedValue = f(item.value)
        return {
          value: mappedValue,
          done: item.done,
        }
      }
      const newIter = { ...it, next: modifiedNext }
      return _lazy(newIter)
    }

    const filter = predicate => {
      const modifiedNext = () => {
        let item = next()
        /* * 注意這裏的改動,這裏爲了處理有限數據集, * 不能再用死循環了 */
        while (!item.done) {
          if (predicate(item.value)) {
            return item
          }
          item = next()
        }
        /* * 若是上面的循環完成,還沒匹配值, * 通知 iterator 及時結束 */
        return { value: null, done: true }
      }
      const newIter = { ...it, next: modifiedNext }
      return _lazy(newIter)
    }

    const takeWhile = predicate => {
      const result = []
      let value = next().value
      while (predicate(value)) {
        result.push(value)
        value = next().value
      }
      return result
    }

    const take = n => {
      const values = []
      let item = next()
      for (let i = 0; i < n; i++) {
        /* * 若是數據集長度比 n 要小, * 遍歷提早完成,要及時 break 循環 */
        if (item.done) break
        values.push(item.value)
        item = next()
      }

      return values
    }

    /* * ZipWith 把兩個包在 Lazy 函數中的 Iterable * 按指定回調函數拼接起來 */
    const zipWith = (fn, other) => {
      const modifiedNext = () => {
        const first = next()
        const second = other.next()
        /* * 只要有一個 Iterable 遍歷完成, * 告訴外層 Iterator 結束 */
        if (first.done || second.done) {
          return { value: null, done: true }
        }
        return {
          value: fn(first.value, second.value),
          done: false,
        }
      }
      const newIter = { ...it, next: modifiedNext }
      return _lazy(newIter)
    }

    return Object.freeze({
      map,
      filter,
      takeWhile,
      next,
      take,
      zipWith,
    })
  }
  return _lazy(iterator)
}
複製代碼

來試驗一下:

Lazy([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
  .map(x => x * 2)
  .filter(x => x % 3 === 0)
  .take(10) // => [6, 12, 18]

Lazy([1, 2, 3, 4, 5, 6, 7, 8])
  .zipWith((x, y) => x + y, Lazy([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]))
  .map(x => x + 1)
  .take(10) // => ​​​​​[ 3, 5, 7, 9, 11, 13, 15, 17 ]​​​​​
複製代碼

注意本文只是提供了一個 proof of concept, 性能問題和其它潛在的問題我還沒驗證過,因此不建議在生產中直接使用本文代碼。

另外,有一個叫 lazy.js 的庫提供了相似的功能。感興趣的朋友能夠參考 lazy.js 的 API,基於本文代碼繼續擴展。好比,給 Lazy 加上異步迭代的功能就能夠很簡單作到。歡迎折騰反饋。

相關文章
相關標籤/搜索