从链式调用到管道组合

写在前面的

在开始正文以前我想谈些与此文相关性很低的话题。对这部分不感兴趣的读者可直接跳过。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

相关文章
相关标签/搜索