從鏈式調用到管道組合

寫在前面的

在開始正文以前我想談些與此文相關性很低的話題。對這部分不感興趣的讀者可直接跳過。javascript

在我發表上一篇文章或許咱們在 JavaScript 中不須要 this 和 class以後,看到一種評論比較有表明性。此評論認爲咱們應該以 MDN 文檔爲指南。MDN 推薦的寫法,應當是無需質疑的寫法。java

我不這麼看。編程

MDN 文檔不是用來學習的,它是你在不肯定某個具體語法時的參考指南。它是 JavaScript 使用說明書。說明書通常都不帶主觀思辨,它沒法提供指引。就好比你光是把市面上的全部調味料買來,看它們的說明書,你仍是學不會怎麼作菜的……數組

很碰巧我看的比較多的一些 JS 教程,都比較主觀,與 MDN 有不少誤差。好比 Kyle Simpson 認爲 JS 裏面根本沒有繼承,提供 new 操做符以及 class 語法糖是在誤導開發者。JS 原型鏈的正確用法應該是代理而不是繼承。(我贊成他)promise

更明顯的例子是 Douglas Crockford,他認爲 JS 中處理異步編程的主流方案—— callback hell, promise, async/await 全都錯了。你在看他的論述以前有十足把握判定他在胡說嗎?他在 How JavaScript Works 裏面論述了他對事件編程(Eventual Programming)的見解,並寫了個完整的庫,提供他的解決方案。app

批判和辯證地看問題,咱們才能進步。異步

引言

我以前有兩篇文章寫過 JS 裏面惰性求值的實現,但都是淺嘗輒止,沒有過橫向擴展的打算。相關工做已經有人作了(如 lazy.js),我再作意義就不大了。這周在 GitHub 上看到有人寫了個相似的庫,用原生 Generator/Iterator 實現,人氣還很高。我一看仍是有人在寫,我也試試吧。而後我就用 Douglas Crockford 倡導的一種編程風格去寫這個庫,想驗證下這種寫法是否可行。async

Crockford 倡導的寫法是,不用 this 和原型鏈,不用 ES6 Generator/Iterator,不用箭頭函數…… 數據封裝則用工廠函數來實現。異步編程

Douglas Functions

首先,若是不用 ES6 Generator 的話,咱們得本身實現一個 Generator,這個比較簡單:函數

function getGeneratorFromList(list) {
  let index = 0;
  return function next() {
    if (index < list.length) {
      const result = list[index];
      index += 1;
      return result;
    }
  };
}

// 例子:
const next = getGeneratorFromList([1, 2, 3]);
next(); // 1
next(); // 2
next(); // 3
next(); // undefined
複製代碼

ES6 給數組提供了 [Symbol.Iterator] 屬性,給數據賦予了行爲,很方便咱們進行惰性求值操做。而拋棄了 ES6 提供的這個便利以後,咱們就只有手動將數據轉換成行爲了。來看看怎麼作:

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;
}
複製代碼

若是給 Sequence 傳入原生數組的話,它會將數組傳給 getGeneratorFromList,生成一個 Generator,這樣就完成了數據到行爲的轉換

最核心的這兩個功能寫完以後,咱們來實現一個 map

function createMapIterable(mapping, { next }) {
  function map() {
    const value = next();
    if (value !== undefined) {
      return mapping(value);
    }
  }
  return { next: map };
}

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;

  function map(mapping) {
    return Sequence(createMapIterable(mapping, iterable));
  }

  return {
    map,
  };
}
複製代碼

map 寫完後,咱們還須要一個函數幫咱們把行爲轉換回數據

function toList(next) {
  const arr = [];
  let value = next();
  while (value !== undefined) {
    arr.push(value);
    value = next();
  }
  return arr;
}
複製代碼

而後咱們就有一個半完整的惰性求值的庫了,雖然如今它只能 map

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;

  function map(mapping) {
    return Sequence(createMapIterable(mapping, iterable));
  }

  return {
    map,
    toList: () => toList(iterable.next);
  };
}

// 例子:
const double = x => x * 2 // 箭頭函數這樣用是沒問題的啊啊啊,破個例吧
Sequence([1, 3, 6])
  .map(double)
  .toList() // [2,6,12]
複製代碼

再給 Sequence 加個 filter 方法就差很少完整了,其它方法再擴展很簡單了。

function createFilterIterable(predicate, { next }) {
  function filter() {
    const value = next();
    if (value !== undefined) {
      if (predicate(value)) {
        return value;
      }
      return filter();
    }
  }
  return {next: filter};
}

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;

  function map(mapping) {
    return Sequence(createMapIterable(mapping, iterable));
  }

  function filter(predicate) {
    return Sequence(createFilterIterable(predicate, iterable));
  }

  return {
    map,
    filter,
    toList: () => toList(iterable.next);
  };
}

// 例子:
Sequence([1, 2, 3])
  .map(triple)
  .filter(isEven)
  .toList() // [6]
複製代碼

看樣子接着上面的例子繼續擴展就沒問題了。

問題

我繼續寫了十幾個函數,如 take, takeWhile, concat, zip 等。直到寫到我不知道接着寫哪些了,而後我去參考了下 lazy.js 的 API,一看倒吸一口涼氣。lazy.js 快 200 個 API 吧(沒數過,目測),寫完代碼還要寫文檔。我實在不想這麼折騰了。更嚴重的問題不在於工做量,而是這麼龐大的 API 數量讓我意識到我這種寫法的問題。

在使用工廠函數實現鏈式調用的時候,每次調用都返回了一個新的對象,這個新對象包含了全部的 API。假設有 200 個 API,每次調用都是隻取了其中一個,剩下 199 個全扔掉了…… 內存再夠用也不能這麼玩吧。我有強迫症,受不了這種浪費。

結論就是,若是想實現鏈式調用,仍是用原型鏈實現比較好。

然而鏈式調用自己就沒問題了嗎?雖然用原型鏈實現的鏈式調用能省去後續調用的對象建立,可是在初始化的時候也無可避免浪費內存。好比,原型鏈上有 200 個方法,我只調用其中 10 個,剩下的那 190 個都不須要,但它們仍是會在初始化時建立。

我想到了 Rx.js 在版本 5 升級到版本 6 的 API 變更。

// rx.js 5 的寫法:
Source.startWith(0)
  .filter(predicate)
  .takeWhile(predicate2)
  .subscribe(() => {});

// rx.js 6 的寫法:
import { startWith, filter, takeWhile } from 'rxjs/operators';

Source.pipe(
  startWith(0),
  filter(predicate),
  takeWhile(predicate2)
).subscribe(() => {});
複製代碼

RxJS 6 裏面採用了管道組合替代了鏈式調用。這樣子改動以後,想用什麼操做符就引用什麼,沒有多餘的操做符初始化,也利於 tree shaking。那麼咱們就模仿 Rxjs 6 的 API 改寫上面的 Sequence 庫吧。

用管道組合實現惰性求值

操做符的實現和上面沒太大區別,主要區別在操做符的組合方式變了:

function getGeneratorFromList(list) {
  let index = 0;
  return function generate() {
    if (index < list.length) {
      const result = list[index];
      index += 1;
      return result;
    }
  };
}

function toList(sequence) {
  const arr = [];
  let value = sequence();
  while (value !== undefined) {
    arr.push(value);
    value = sequence();
  }
  return arr;
}

// Sequence 函數自己很是輕量,操做符按需引入
function Sequence(list) {
  const initSequence = getGeneratorFromList(list);
  
  function pipe(...args) {
    return args.reduce((prev, current) => current(prev), initSequence);
  }
  return { pipe };
}

function filter(predicate) {
  return function(sequence) {
    return function filteredSequence() {
      const value = sequence();
      if (value !== undefined) {
        if (predicate(value)) {
          return value;
        }
        return filteredSequence();
      }
    };
  };
}

function map(mapping) {
  return function(sequence) {
    return function mappedSequence() {
      const value = sequence();
      if (value !== undefined) {
        return mapping(value);
      }
    };
  };
}

function take(n) {
  return function(sequence) {
    let count = 0;
    return function() {
      if (count < n) {
        count += 1;
        return sequence();
      }
    };
  };
}

function skipWhile(predicate) {
  return function(sequence) {
    let startTaking = false;
    return function skippedSequence() {
      const value = sequence();
      if (value !== undefined) {
        if (startTaking) {
          return value;
        } else if (!predicate(value)) {
          startTaking = true;
          return value;
        }
        return skippedSequence();
      }
    };
  };
}

function takeUntil(predicate) {
  return function(sequence) {
    return function() {
      const value = sequence();
      if (value !== undefined) {
        if (predicate(value)) {
          return value;
        }
      }
    };
  };
}

Sequence([2, 4, 6, 7, 9, 11, 13]).pipe(
  filter(x => x % 2 === 1),
  skipWhile(y => y < 10),
  toList
); // [11,13]
複製代碼

參考:

Let’s experiment with functional generators and the pipeline operator in JavaScript

相關文章
相關標籤/搜索