For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-function-pipe-flow.md.
  • 简体中文
  • Function.pipe and flow ?

    中文标题:Function.pipe 和 flow

    提案概览
    提案速览

    该提案针对 JavaScript 中常见的串联函数应用与组合需求。它提议在 Function 构造函数上添加静态方法——Function.pipe/pipeAsyncFunction.flow/flowAsyncFunction.compose/composeAsync——将这些流程标准化。提案还探讨了这些辅助函数与管道运算符以及 fp-ts、lodash.flow 等现有用户态工具之间的关系。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    Function.pipe 和 flow(用于 JavaScript)

    ECMAScript Stage-0 提案(已撤回)。J. S. Choi,2021 年。

    ⚠️ 本提案已撤回。 在 7 月 21 日的全体会议上,proposal-function-pipe-flow 被正式提交给委员会,但未通过 Stage 1。委员会普遍认为其用例要么可以很容易地通过用户态函数解决,例如:

    function pipe (input, ...fnArray) {
      return fnArray.reduce((value, fn) => fn(value), input);
    }

    ……要么也可以由 pipe operator 解决。其提案推动者随后将其从考虑中撤回。(最终,在管道运算符获得用户之后,管道运算符的痛点可能足以成为重新推动该提案的动力,但这在很长一段时间内不会发生。)

    原始提案

    序列函数应用和函数组合在 JavaScript 中是有用且常见的。开发者经常把代码拆分成许多较小的一元回调,然后用某个初始输入依次调用这些回调——或者把它们组合成更大的回调,以便之后依次调用这些函数。

    为此,他们通常将这些回调与辅助函数结合使用:pipe(序列函数应用)以及 flow 和/或 compose(函数组合)。将这些元函数标准化将会很有用。

    这里的问题空间是序列回调的应用与组合。JavaScript 社区中的许多开发者早已在序列地应用或组合回调。

    取自 StoplightIO Prism v4.5.0 packages/http/src/validator/validators/body.ts。

    return pipe(
      specs,
      A.findFirst(spec => !!typeIs(mediaType, [spec.mediaType])),
      O.alt(() => A.head(specs)),
      O.map(content => ({ mediaType, content }))
    );

    取自 strapi@3.6.8 packages/strapi-admin/services/permission/permissions-manager/query-builers.js:

    const transform = flow(flattenDeep, cleanupUnwantedProperties);

    取自 semantic-ui-react@v2.0.4/docs/static/utils/getInfoForSeeTags.js:

    const getInfoForSeeTags = _.flow(
      _.get('docblock.tags'),
      _.filter((tag) => tag.title === 'see'),
      _.map((tag) => { /* … */ }),
    )

    如果本提案获准进入 Stage 1,我们将探索序列地应用和组合一元回调的各种方向。此外,我们还会尽可能多地收集真实世界用例,并围绕它们来塑造我们的设计。

    (管道运算符的提案已经在一定程度上探索了这个问题空间。然而,与本提案不同,管道运算符可以“应用”任何类型的表达式操作,而不仅仅是一元函数调用。其代价是管道运算符涉及新语法;此外,pipe 函数对于一元函数调用更为简洁,并且可以处理动态回调数组。在 Stage 1 中,我们将继续研究与管道运算符以及其他“数据流”提案的跨领域问题和重叠部分。)

    解决方案

    我们可以添加以下静态函数的各种组合:

    • Function.pipeFunction.pipeAsync(用于序列应用)。
    • Function.flow 和 Function.flowAsync(用于从左到右的序列组合)。
    • Function.composeFunction.composeAsync(用于从右到左的序列组合)。

    (LTR = 从左到右。RTL = 从右到左。)

    `pipe`、`flow` 和 `compose` 共有八种可能的组合。
    • 选项 #0:维持现状

    • 选项 #1:LTR flow

      Function.flow(f, g, h);
      Function.flowAsync(f, g, h);
    • 选项 #2:RTL compose

      Function.compose(h, g, f);
      Function.composeAsync(h, g, f);
    • 选项 #3:LTR flow 与 RTL compose

      Function.flow(f, g, h);
      Function.flowAsync(f, g, h);
      Function.compose(h, g, f);
      Function.composeAsync(h, g, f);
    • 选项 #4:Pipe

      Function.pipe(x, f, g, h);
      Function.pipeAsync(x, f, g, h);
    • 选项 #5:Pipe 与 LTR flow

      Function.pipe(x, f, g, h);
      Function.pipeAsync(x, f, g, h);
      Function.flow(f, g, h);
      Function.flowAsync(f, g, h);
    • 选项 #6:Pipe 与 RTL compose

      Function.pipe(x, f, g, h);
      Function.pipeAsync(x, f, g, h);
      Function.compose(h, g, f);
      Function.composeAsync(h, g, f);
    • 选项 #7:Pipe、LTR flow 与 RTL compose

      Function.pipe(x, f, g, h);
      Function.pipeAsync(x, f, g, h);
      Function.flow(f, g, h);
      Function.flowAsync(f, g, h);
      Function.compose(h, g, f);
      Function.composeAsync(h, g, f);

    F# 管道运算符发生了什么?

    F#、Haskell 以及其他基于自动柯里化一元函数的语言都有一种隐式一元函数应用运算符。 pipe 提案推动组曾两次向 TC39 提交 F# 管道以争取进入 Stage 2,但两次都未成功, 原因来自多位 TC39 代表的反对,包括内存性能方面的担忧、对 await 的语法担忧, 以及对鼓励生态系统分裂/分叉的担忧。 (更多信息,请参阅 pipe 提案的 HISTORY.md。)

    鉴于这一现实,TC39 更有可能通过 Function.pipe 辅助函数,而不是类似的语法运算符。

    将辅助函数标准化并不妨碍之后将等效的运算符标准化。 例如,即使在 Math.pow 存在的情况下,TC39 还是将二元 ** 标准化了。

    未来,我们可能会尝试提出 F# 管道运算符, 但我们希望先尝试提出 Function.pipe, 以便尽快将其好处带给更广泛的 JavaScript 社区。

    Function.pipe

    Function.pipe 静态方法将一个回调序列应用于给定的输入值,并返回最后一个回调的结果。

    Function.pipe(input, ...fns);
    
    const { pipe } = Function;
    
    // f2(f1(f0(5))).
    pipe(5, f0, f1, f2);
    
    // 5.
    pipe(5);
    
    // undefined.
    pipe();

    以下真实世界示例原本使用了 fp-tspipe 函数。

    // From @gripeless/pico@1.0.1/source/inline.ts:
    return pipe(
      download(absoluteURL),
      mapRej(downloadErrorToDetailedError),
      chainFluture(responseToBlob),
      chainFluture(blobToDataURL),
      mapFluture(dataURL => `url(${dataURL})`)
    )
    
    // From StoplightIO Prism v4.5.0 packages/http/src/validator/validators/body.ts:
    return pipe(
      specs,
      A.findFirst(spec => !!typeIs(mediaType, [spec.mediaType])),
      O.alt(() => A.head(specs)),
      O.map(content => ({ mediaType, content }))
    );

    第一个回调应用于 input, 然后第二个回调应用于第一个回调的结果, 依此类推。 换句话说,函数管道从左到右进行。

    每个回调都应是一个一元函数。

    如果 Function.pipe 只接收一个参数,那么它将默认返回 input
    如果 Function.pipe 没有接收参数,那么它将返回 undefined

    已有的先例包括:

    • fp-tsimport { pipe } from 'fp-ts/function';

    Function.pipeAsync

    Function.pipeAsync 静态方法将一个可能异步的回调序列应用于给定的输入值,并返回一个 promise。 该 promise 将解析为最后一个回调的结果。

    Function.pipeAsync(input, ...fns);
    
    const { pipeAsync } = Function;
    
    // Promise.resolve(5).then(f0).then(f1).then(f2).
    pipeAsync(5, f0, f1, f2);
    
    // Promise.resolve(5).
    pipeAsync(5);
    
    // Promise.resolve(undefined).
    pipeAsync();

    输入首先被 await。 然后第一个回调应用于 input,结果再被 await, 接着第二个回调应用于第一个回调的结果,结果再被 await, 依此类推。 换句话说,函数管道从左到右进行。

    每个回调都应是一个一元函数。

    如果任何回调返回的 promise 随后因错误而拒绝, 那么 Function.pipeAsync 返回的 promise 也将以相同的错误拒绝。

    如果 Function.pipeAsync 只接收一个参数, 那么它将默认返回 Promise.resolve(input)
    如果 Function.pipeAsync 没有接收参数, 那么它将返回 Promise.resolve(undefined)

    (LTR) Function.flow

    Function.flow 静态方法通过按从左到右的顺序组合多个回调来创建一个新函数。

    Function.flow(...fns);
    
    const { flow } = Function;
    
    const f = flow(f0, f1, f2);
    f(5, 7); // f2(f1(f0(5, 7))).
    
    const g = flow(g0);
    g(5, 7); // g0(5, 7).
    
    const h = flow();
    h(5, 7); // 5.

    以下真实世界示例原本使用了 lodash.flow

    // From gatsby@3.14.3/packages/gatsby-plugin-sharp/src/plugin-options.js:
    flow(
      mapUserLinkHeaders(pluginData),
      applySecurityHeaders(pluginOptions),
      applyCachingHeaders(pluginData, pluginOptions),
      mapUserLinkAllPageHeaders(pluginData, pluginOptions),
      applyLinkHeaders(pluginData, pluginOptions),
      applyTransfromHeaders(pluginOptions),
      saveHeaders(pluginData)
    )
    
    // From strapi@3.6.8
    // packages/strapi-admin/services/permission/permissions-manager/query-builers.js:
    const transform = flow(flattenDeep, cleanupUnwantedProperties);
    
    // From semantic-ui-react@v2.0.4/docs/static/utils/getInfoForSeeTags.js:
    const getInfoForSeeTags = flow(
      _.get('docblock.tags'),
      _.filter((tag) => tag.title === 'see'),
      _.map((tag) => { /* … */ }),
    )

    任何由 Function.flow 创建的函数 都会将自己的参数应用于最左边的回调。 然后该结果被应用于下一个回调。

    最左边的回调可以具有任意元数, 但后续回调都应是一元函数。

    如果 Function.flow 没有接收参数,那么默认情况下, 它将返回一个一元恒等函数。

    已有的先例包括:

    (LTR) Function.flowAsync

    Function.flowAsync 静态方法通过按从左到右的顺序组合多个可能异步的回调来创建一个新函数; 该函数将始终返回一个 promise。

    Function.flowAsync(...fns);
    
    const { flowAsync } = Function;
    
    // async (...args) => await f2(await f1(await f0(...args))).
    flowAsync(f0, f1, f2);
    
    const f = flowAsync(f0, f1, f2);
    await f(5, 7); // await f2(await f1(await f0(5, 7))).
    
    const g = flowAsync(g0);
    await g(5, 7); // await g0(5, 7).
    
    const h = flowAsync();
    await h(5, 7); // await 5.

    任何由 Function.flowAsync 创建的函数 都会将自己的参数应用于最左边的回调。 然后该结果在应用于下一个回调之前会被 await

    最左边的回调可以具有任意元数, 但后续回调都应是一元函数。

    如果 Function.flowAsync 没有接收参数,那么默认情况下, 它将返回 Promise.resolve

    (RTL) Function.compose

    Function.compose 静态方法通过按从右到左的顺序组合多个回调来创建一个新函数。

    Function.compose(...fns);
    
    const { compose } = Function;
    
    const f = compose(f2, f1, f0);
    f(5, 7); // f2(f1(f0(5, 7))).
    
    const g = compose(g0);
    g(5, 7); // g0(5, 7).
    
    const h = compose();
    h(5, 7); // 5.

    任何由 Function.compose 创建的函数 都会将自己的参数应用于最右边的回调。 然后该结果被应用于下一个回调。

    最右边的回调可以具有任意元数, 但后续回调都应是一元函数。

    如果 Function.compose 没有接收参数,那么默认情况下, 它将返回一个一元恒等函数。

    (RTL) Function.composeAsync

    Function.composeAsync 静态方法通过按从右到左的顺序组合多个可能异步的回调来创建一个新函数; 该函数将始终返回一个 promise。

    Function.composeAsync(...fns);
    
    const { composeAsync } = Function;
    
    // async (...args) => await f2(await f1(await f0(...args))).
    composeAsync(f2, f1, f0);
    
    const f = composeAsync(f2, f1, f0);
    await f(5, 7); // await f2(await f1(await f0(5, 7))).
    
    const g = composeAsync(g0);
    await g(5, 7); // await g0(5, 7).
    
    const h = composeAsync();
    await h(5, 7); // await 5.

    任何由 Function.composeAsync 创建的函数 都会将自己的参数应用于最右边的回调。 然后该结果在应用于下一个回调之前会被 await。 换句话说,异步函数组合从左到右进行。

    最右边的回调可以具有任意元数, 但后续回调都应是一元函数。

    如果 Function.composeAsync 没有接收参数,那么默认情况下, 它将返回 Promise.resolve