ITADN
tc39/proposal-pipeline-operator
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

JavaScript 的管道运算符 (|>)

  • 阶段: 2
  • 提案者: J. S. Choi, James DiGioia, Ron Buckton, Tab Atkins-Bittner, [列表不完整]
  • 前提案者: Daniel Ehrenberg
  • [规范][]
  • [贡献指南][]
  • [提案历史][]
  • Babel 插件: 已在 v7.15 中实现。参见 [Babel 文档][]。

(本文档使用 % 作为主题引用的占位符。 这几乎肯定不是最终选择; 详情请参见 关于该标记的讨论。)

为什么需要管道运算符

在 2020 年 State of JS 调查中, “你认为 JavaScript 目前缺少什么?”第四大答案管道运算符。为什么?

当我们在 JavaScript 中对一个执行连续操作(例如函数调用)时, 目前有两种基本风格:

  • 将值作为参数传递给操作 (如果存在多个操作,则嵌套这些操作),
  • 或者将函数作为值上的方法进行调用 (如果存在多个方法,则链式调用更多方法)。

也就是说,three(two(one(value)))value.one().two().three()。 然而,这些风格在可读性、流畅性和适用性上差异很大。

深层嵌套难以阅读

第一种风格,嵌套,通常具有通用性 – 它适用于任何操作序列: 函数调用、算术运算、数组/对象字面量、awaityield 等。

然而,当嵌套变得很深时,它难以阅读: 执行流程是从右到左移动的, 而不是像普通代码那样从左到右阅读。 如果在某些层级有多个参数, 阅读甚至会在来回之间跳跃: 我们的眼睛必须向左跳以找到函数名, 然后必须向右跳以找到额外的参数。 此外,之后编辑代码可能会充满风险: 我们必须在许多嵌套括号中找到正确的插入位置 以添加新参数。

现实世界示例

考虑这段来自 React 的现实世界代码

console.log(
  chalk.dim(
    `$ ${Object.keys(envars)
      .map(envar =>
        `${envar}=${envars[envar]}`)
      .join(' ')
    }`,
    'node',
    args.join(' ')));

这段真实世界的代码由深度嵌套的表达式构成。 为了阅读其数据流,人的眼睛必须首先:

  1. 找到初始数据(最内层的表达式,envars)。

  2. 然后对于每个数据转换,反复地来回内到外扫描, 每一个转换要么是左侧容易忽略的前缀运算符, 要么是右侧的后缀运算符:

    1. Object.keys()(左侧),
    2. .map()(右侧),
    3. .join()(右侧),
    4. 模板字符串(两侧),
    5. chalk.dim()(左侧),然后
    6. console.log()(左侧)。

由于深度嵌套了许多表达式 (其中一些使用前缀运算符, 一些使用后缀运算符, 还有一些使用中缀运算符), 我们必须检查左右两侧 才能找到每个表达式头部

方法链的局限性

第二种风格,方法链在 该值拥有其类所指定的方法函数时才可使用。 这限制了其适用范围。 但它适用时,得益于其后缀结构, 它通常更易于使用,且易读易写。 代码执行流向为从左到右。 深层嵌套的表达式被解开。 函数调用的所有参数都与函数名分组在一起。 并且稍后编辑代码以插入或删除更多方法调用是微不足道的, 因为我们只需将光标放在一个位置, 然后开始输入或删除一段连续的字符。

事实上,方法链的好处如此诱人 以至于一些流行库扭曲了其代码结构 专门为了允许更多的方法链。 最典型的例子是 jQuery,它 仍然是世界上最流行的 JS 库。 jQuery 的核心设计是一个带有数十个方法的单一超级对象, 所有这些方法都返回相同的对象类型,以便我们可以继续链式调用。 这种编程风格甚至有一个名称: fluent interfaces

不幸的是,尽管方法链流畅, 仅靠方法链无法容纳 JavaScript 的其他语法: 函数调用、算术运算、数组/对象字面量、awaityield 等。 因此,方法链在适用性方面仍然有限

管道运算符融合两者

管道运算符试图将方法链便捷与易用性 与表达式嵌套的广泛适用性结合起来。

所有管道运算符的一般结构为 value |> e1 |> e2 |> e3, 其中 e1e2e3 都是将连续的值作为参数的表达式。 然后 |> 运算符进行某种程度的“魔法”操作,将 value 从左侧“管道”传输到右侧。

实际示例,续

继续这段深度嵌套的 来自 React 的实际代码

console.log(
  chalk.dim(
    `$ ${Object.keys(envars)
      .map(envar =>
        `${envar}=${envars[envar]}`)
      .join(' ')
    }`,
    'node',
    args.join(' ')));

…我们可以使用管道运算符和一个占位符标记(%)来解开它,该标记代表前一个操作的值:

Object.keys(envars)
  .map(envar => `${envar}=${envars[envar]}`)
  .join(' ')
  |> `$ ${%}`
  |> chalk.dim(%, 'node', args.join(' '))
  |> console.log(%);

现在,人类读者可以快速找到 初始数据 (即最内层的表达式,envars), 然后线性地左到右阅读 对数据的每一次变换。

临时变量往往令人厌烦

有人可能会认为,使用临时变量 应该是解开深度嵌套代码的唯一方式。 为每一步的变量显式命名 会导致类似方法链式调用的情况发生, 在代码的阅读和编写方面具有类似的好处。

现实世界示例,续

例如,使用我们之前修改过的 来自 React 的现实世界示例

Object.keys(envars)
  .map(envar => `${envar}=${envars[envar]}`)
  .join(' ')
  |> `$ ${%}`
  |> chalk.dim(%, 'node', args.join(' '))
  |> console.log(%);

…使用临时变量的版本如下所示:

const envarString = Object.keys(envars)
  .map(envar => `${envar}=${envars[envar]}`)
  .join(' ');
const consoleText = `$ ${envarString}`;
const coloredConsoleText = chalk.dim(consoleText, 'node', args.join(' '));
console.log(coloredConsoleText);

但我们在彼此的代码中在现实世界中总是遇到深度嵌套的表达式, 而不是临时变量行,是有原因的。 而 jQuery、Mocha 等基于方法链的 fluent interfaces 仍然流行,也是有原因的。

使用长串的临时、一次性变量来编写 代码往往过于繁琐和啰嗦。 对于人类来说,阅读这样的代码可能同样繁琐且视觉上杂乱。

如果命名是编程中最困难的任务之一, 那么当程序员感知到其收益相对较小时, 就不可避免地会避免命名变量。

重用临时变量容易引发意外的变异

有人可能会争辩说,使用一个短名的可变变量 可以减少临时变量的啰嗦程度,达到 与管道运算符类似的效果。

现实示例,续

例如,我们之前修改过的 来自 React 的现实示例 可以重写为如下形式:

let _;
_ = Object.keys(envars)
  .map(envar => `${envar}=${envars[envar]}`)
  .join(' ');
_ = `$ ${_}`;
_ = chalk.dim(_, 'node', args.join(' '));
_ = console.log(_);

但这样的代码在真实世界的代码中并不常见。 其中一个原因是可变变量可能会意外改变, 从而导致难以发现的静默错误。 例如,该变量可能在闭包中被意外引用。 或者它可能在表达式中被错误地重新赋值。

示例代码
// setup
function one () { return 1; }
function double (x) { return x * 2; }

let _;
_ = one(); // _ is now 1.
_ = double(_); // _ is now 2.
_ = Promise.resolve().then(() =>
  // This does *not* print 2!
  // It prints 1, because `_` is reassigned downstream.
  console.log(_));

// _ becomes 1 before the promise callback.
_ = one(_);

此问题在使用管道操作符时不会发生。 主题令牌无法被重新赋值,并且 每个步骤之外的代码无法更改其绑定。

let _;
_ = one()
  |> double(%)
  |> Promise.resolve().then(() =>
    // This prints 2, as intended.
    console.log(%));

_ = one();

因此,包含可变变量的代码也更难阅读。 要确定变量在任意给定时刻代表什么, 你必须搜索整个前置作用域中对其进行重新赋值的位置。

另一方面,管道的主题引用具有有限的词法作用域, 且其绑定在其作用域内是不可变的。 它不会被意外重新赋值,并且可以安全地用于闭包中。

尽管主题值也会随着每个管道步骤而变化, 我们只需扫描管道的前一个步骤来理解它, 从而生成更易读的代码。

临时变量必须在语句中声明

管道运算符相较于赋值语句序列的另一个好处 (无论是使用可变还是不可变临时变量) 在于它们是表达式

管道表达式是可以直接返回的表达式, 可以赋值给变量,或用于 JSX 表达式等上下文中。

另一方面,使用临时变量需要语句序列。

示例
管道临时变量
const envVarFormat = vars =>
  Object.keys(vars)
    .map(var => `${var}=${vars[var]}`)
    .join(' ')
    |> chalk.dim(%, 'node', args.join(' '));
const envVarFormat = (vars) => {
  let _ = Object.keys(vars);
  _ = _.map(var => `${var}=${vars[var]}`);
  _ = _.join(' ');
  return chalk.dim(_, 'node', args.join(' '));
}
// This example uses JSX.
return (
  <ul>
    {
      values
        |> Object.keys(%)
        |> [...Array.from(new Set(%))]
        |> %.map(envar => (
          <li onClick={
            () => doStuff(values)
          }>{envar}</li>
        ))
    }
  </ul>
);
// This example uses JSX.
let _ = values;
_= Object.keys(_);
_= [...Array.from(new Set(_))];
_= _.map(envar => (
  <li onClick={
    () => doStuff(values)
  }>{envar}</li>
));
return (
  <ul>{_}</ul>
);

为什么选择 Hack 管道运算符

关于管道运算符,曾有两个相互竞争的提案:Hack 管道和 F# 管道。 (在此之前,曾一个针对前两个提案的“智能混合”的第三提案, 但它已被撤回, 因为其语法严格是其中一个提案的超集。)

这两个管道提案仅在当我们使用 |> 编写代码时, 关于“魔法”所在之处存在细微差异。

两者复用了现有的语言概念: Hack 管道基于表达式的概念, 而 F# 管道基于一元函数的概念。

管道传递表达式和管道传递一元函数 相应地具有较小且近乎对称的权衡。

本提案:Hack 管道

Hack 语言的管道语法中, 管道右侧是一个包含特殊占位符表达式, 该占位符被绑定到左侧表达式求值的结果后进行求值。 也就是说,我们编写 value |> one(%) |> two(%) |> three(%) 以将 value 通过三个函数进行管道传递。

优点: 右侧可以是任意表达式, 且占位符可以出现在任何普通变量标识符可以出现的位置, 因此我们可以将数据管道传递到任何我们想要的代码中,而无需任何特殊规则

  • value |> foo(%) 用于一元函数调用,
  • value |> foo(1, %) 用于 n 元函数调用,
  • value |> %.foo() 用于方法调用,
  • value |> % + 1 用于算术运算,
  • value |> [%, 0] 用于数组字面量,
  • value |> {foo: %} 用于对象字面量,
  • value |> `${%}` 用于模板字面量,
  • value |> new Foo(%) 用于构造对象,
  • value |> await % 用于等待 promise,
  • value |> (yield %) 用于生成器值,
  • value |> import(%) 用于调用类函数关键字,
  • 等等。

缺点: 通过 一元函数 进行管道传输 使用 Hack pipes 比使用 F# pipes 略显冗长。 这包括由 function-currying(如 Ramda)创建的一元函数, 以及 对参数执行 复杂解构 的一元箭头函数: 使用 Hack pipes 时,需要加上 显式 的函数调用后缀 (%),会略显冗长。

(当 do expressions 取得进展后, 对主题值进行复杂解构将变得更加容易, 届时你将能够在管道主体内进行变量赋值/解构。)

替代方案:F# 管道

F# 语言的管道语法 中, 管道右侧是一个表达式, 它必须求值为一个一元函数, 然后该函数会被隐式调用, 以左侧的值作为其唯一参数。 也就是说,我们编写 value |> one |> two |> threevalue 通过这三个函数进行管道处理。 left |> right 变为 right(left)。 这被称为 无点编程或无点风格

实际示例,续

例如,使用我们之前修改过的 来自 React 的实际示例

Object.keys(envars)
  .map(envar => `${envar}=${envars[envar]}`)
  .join(' ')
  |> `$ ${%}`
  |> chalk.dim(%, 'node', args.join(' '))
  |> console.log(%);

…一个使用 F# 管道而非 Hack 管道的版本将如下所示:

Object.keys(envars)
  .map(envar => `${envar}=${envars[envar]}`)
  .join(' ')
  |> x=> `$ ${x}`
  |> x=> chalk.dim(x, 'node', args.join(' '))
  |> console.log;

优点: 右侧 必须解析为一元函数 的限制让我们能够编写非常简洁的管道 我们要执行的操作 是一个一元函数调用时:

  • value |> foo 用于一元函数调用。

这包括由 function-currying(如 Ramda)创建的 一元函数,以及对参数执行复杂解构的 一元箭头函数: F# 管道在使用隐式函数调用(无 (%))时 会稍微不那么冗长

缺点: 该限制意味着由其他语法 执行的任何操作 都必须通过包装该操作 为一元箭头函数而变得稍微冗长

  • value |> x=> x.foo() 用于方法调用,
  • value |> x=> x + 1 用于算术运算,
  • value |> x=> [x, 0] 用于数组字面量,
  • value |> x=> ({foo: x}) 用于对象字面量,
  • value |> x=> `${x}` 用于模板字符串,
  • value |> x=> new Foo(x) 用于构造对象,
  • value |> x=> import(x) 用于调用类函数关键字,
  • 等等。

即使调用命名函数,当我们需要传递多个参数时 也需要包装

  • value |> x=> foo(1, x) 用于 n 元函数调用。

缺点: awaityield 操作是作用域 限于其包含函数的, 因此无法仅由一元函数处理。 如果我们想将它们集成到管道表达式中, [awaityield 必须作为特殊语法情况处理][enhanced F# pipes]:

  • value |> await 用于等待 Promise,以及
  • value |> yield 用于生成器值。

Hack 管道更倾向于更常见的表达式

两者 Hack 管道和 F# 管道分别对不同的表达式 施加了少量的语法税
Hack 管道仅对一元函数调用略微征税,而
F# 管道则对除一元函数调用外的所有表达式略微征税。

两者提案中,每个被征税表达式的语法税都是的 (两者 (%)x=>只有三个字符)。 然而,该税是乘以其相应被征税表达式的普遍性的。 因此,对较少见的表达式征税 并优化以偏向更常见的表达式可能更有意义。

一元函数调用通常较少见 所有表达式一元函数外。 特别是,方法调用和n 元函数调用 将始终流行的; 在总体频率上, 一元函数调用等于或被 仅这两种情况所超过 – 更不用说其他无处不在的语法 如数组字面量对象字面量算术运算。 本说明包含几个真实世界示例 关于这种普遍性差异。

此外,其他几种提议的新语法, 例如**extension callingdo expressions 以及record/tuple literals, 在未来也很可能会变得无处不在**。 同样,如果 TC39 标准化了**operator overloading算术运算也会变得更加常见**。 与 F# 管道相比,使用 Hack 管道来解构这些未来语法的表达式会更加流畅。

Hack 管道可能更易于使用

Hack 管道在单目函数调用上的语法开销 (即调用右侧单目函数所需的 (%)并非特殊情况: 它只是显式地编写普通代码以我们通常在没有管道时那样的方式。

另一方面,F# 管道要求我们区分 “解析为单目函数的代码” 与**“任何其他表达式”**—— 并且要记得在后一种情况下添加箭头函数包装器。

例如,使用 Hack 管道时,value |> someFunction + 1无效语法,并且会尽早失败。 无需识别出 someFunction + 1 不会求值为一个一元函数。 但使用 F# 管道时,value |> someFunction + 1 仍然是有效语法 – 它只是会在运行时 延迟失败, 因为 someFunction + 1 不可调用。

TC39 已多次拒绝 F# 管道

管道提案主导小组曾两次向 TC39 提交 F# 管道以进入 Stage 2。 两次都未能成功推进至 Stage 2。 F# 管道(以及部分函数应用 (PFA)) 均因各种顾虑而遭到 TC39 其他多位代表的强烈反对。这些顾虑包括:

这种反对意见来自管道提案主导小组外部。 更多信息请参阅 HISTORY.md

管道冠军小组认为,任何管道运算符都比没有要好, 以便轻松地将深度嵌套的表达式线性化 而无需诉诸命名变量。 冠军小组的许多成员认为 Hack 管道略优于 F# 管道, 而冠军小组的一些成员则认为 F# 管道略优于 Hack 管道。 但冠军小组的每个人都同意,F# 管道遭遇了过多的阻力, 以至于在可预见的未来无法通过 TC39。

需要强调的是,试图从 Hack 管道切换回 F# 管道 很可能导致 TC39 永远不同意任何管道。 [PFA 语法][] 在 TC39 中同样面临艰难的战斗(参见 HISTORY.md)。 管道冠军小组的许多成员认为这很遗憾, 他们愿意稍后再次为 F#-pipe split mix 和 [PFA 语法][] 而战。 但在管道冠军小组之外,有相当多的代表(包括 浏览器引擎实现者) 总体上反对鼓励 [无显式编程][](以及 [PFA 语法][]), 无论是否涉及 Hack 管道。

描述

正式草案规范 已可用。)

主题引用 % 是一个零元运算符。 它作为主题值的占位符, 并且是词法作用域的且不可变的。

% 并非最终选择

主题引用的精确 token 尚未最终确定% 也可以是 ^,或者许多其他 token。 我们计划在推进到 Stage 3 之前 bikeshed 实际使用哪个 token。 然而,% 似乎是[语法上问题最少的][], 并且它也类似于 [printf 格式字符串][] 的占位符 以及 Clojure#(%) 函数字面量。)

管道运算符 |> 是一个中缀运算符, 用于构成管道表达式(也称为流水线)。 它先求值其左侧(管道头部管道输入), 以不可变方式将所得值(主题值绑定主题引用, 然后在该绑定的情况下求值其右侧(管道主体)。 右侧的求值结果 成为整个管道表达式的最终值(管道输出)。

管道运算符的优先级与以下运算符相同

  • 函数箭头 =>
  • 赋值运算符 =+= 等;
  • 生成器运算符 yieldyield *

它仅比逗号运算符 , 更紧
它比所有其他运算符更松

例如,v => v |> % == null |> foo(%, 0)
会分组为 v => (v |> (% == null) |> foo(%, 0))
而后者又等价于 v => foo(v == null, 0)

管道主体必须至少使用一次其主题值。 例如,value |> foo + 1无效语法, 因为其主体中不包含主题引用。 这样设计是因为从管道表达式的主体中 省略主题引用 几乎肯定是一个意外的程序员错误。

同样,主题引用必须包含在管道主体中。 在管道主体之外使用主题引用 也是无效语法

为了防止产生令人困惑的分组, 使用具有相似优先级其他运算符 (即箭头 =>、三元条件运算符 ? :、 赋值运算符以及 yield 运算符) 作为管道头部或主体无效的语法。 当使用 |> 与这些运算符时,我们必须使用括号 来明确指示正确的分组方式。 例如,a |> b ? % : c |> %.d 是无效语法; 应将其修正为 a |> (b ? % : c) |> %.da |> (b ? % : c |> %.d)

最后,在动态编译的代码内部的主题绑定 (例如,使用 evalnew Function不能该代码外部使用。 例如,当 eval 表达式在运行时被求值时, v |> eval('% + 1') 将抛出语法错误。

没有其他特殊规则

这些规则的一个自然结果是, 如果我们需要在管道表达式链的中间插入一个副作用, 而不修改正在通过管道传输的数据, 那么我们可以使用逗号表达式, 例如使用 value |> (sideEffect(), %)。 通常,逗号表达式将求值为其右侧 %, 基本上在不修改主题值的情况下将其传递过去。 这对于快速调试特别有用:value |> (console.log(%), %)

实际示例

对原始示例的唯一更改是取消缩进和删除注释。

来自 jquery/build/tasks/sourceMap.js

// Status quo
var minLoc = Object.keys( grunt.config( "uglify.all.files" ) )[ 0 ];

// With pipes
var minLoc = grunt.config('uglify.all.files') |> Object.keys(%)[0];

来自 node/deps/npm/lib/unpublish.js

// Status quo
const json = await npmFetch.json(npa(pkgs[0]).escapedName, opts);

// With pipes
const json = pkgs[0] |> npa(%).escapedName |> await npmFetch.json(%, opts);

来自 underscore.js

// Status quo
return filter(obj, negate(cb(predicate)), context);

// With pipes
return cb(predicate) |> _.negate(%) |> _.filter(obj, %, context);

来自 ramda.js

// Status quo
return xf['@@transducer/result'](obj[methodName](bind(xf['@@transducer/step'], xf), acc));

// With pipes
return xf
  |> bind(%['@@transducer/step'], %)
  |> obj[methodName](%, acc)
  |> xf['@@transducer/result'](%);

来自 ramda.js

// Status quo
try {
  return tryer.apply(this, arguments);
} catch (e) {
  return catcher.apply(this, _concat([e], arguments));
}

// With pipes: Note the visual parallelism between the two clauses.
try {
  return arguments
    |> tryer.apply(this, %);
} catch (e) {
  return arguments
    |> _concat([e], %)
    |> catcher.apply(this, %);
}

来自 express/lib/response.js

// Status quo
return this.set('Link', link + Object.keys(links).map(function(rel){
  return '<' + links[rel] + '>; rel="' + rel + '"';
}).join(', '));

// With pipes
return links
  |> Object.keys(%).map(function (rel) {
    return '<' + links[rel] + '>; rel="' + rel + '"';
  })
  |> link + %.join(', ')
  |> this.set('Link', %);

来自 react/scripts/jest/jest-cli.js

// Status quo
console.log(
  chalk.dim(
    `$ ${Object.keys(envars)
      .map(envar => `${envar}=${envars[envar]}`)
      .join(' ')}`,
    'node',
    args.join(' ')
  )
);

// With pipes
Object.keys(envars)
  .map(envar => `${envar}=${envars[envar]}`)
  .join(' ')
  |> `$ ${%}`
  |> chalk.dim(%, 'node', args.join(' '))
  |> console.log(%);

来自 ramda.js

// Status quo
return _reduce(xf(typeof fn === 'function' ? _xwrap(fn) : fn), acc, list);

// With pipes
return fn
  |> (typeof % === 'function' ? _xwrap(%) : %)
  |> xf(%)
  |> _reduce(%, acc, list);

来自 jquery/src/core/init.js

// Status quo
jQuery.merge( this, jQuery.parseHTML(
  match[ 1 ],
  context && context.nodeType ? context.ownerDocument || context : document,
  true
) );

// With pipes
context
  |> (% && %.nodeType ? %.ownerDocument || % : document)
  |> jQuery.parseHTML(match[1], %, true)
  |> jQuery.merge(%);

与其他提案的关系

Function 辅助函数

Hack pipes 可以并且将会与 Function 辅助函数提案 共存, 包括其 pipeflow 函数。 这些简单(且下载量普遍较高)的便捷函数 在没有额外语法的情况下操作一元函数。

TC39 已两次否决 F# 管道运算符。 鉴于这一现实,TC39 通过 pipeflow 辅助函数的可能性,远高于通过类似的语法运算符。

标准化的 pipeflow 便捷函数 也可能消除对 F#-pipe 中缀运算符的部分需求。 (这并不妨碍日后标准化一个等效的运算符。 例如,即使存在 Math.pow,TC39 也标准化了二元 **。)

偏函数应用语法

Hack 管道可以与偏函数应用(PFA)语法共存。 有两种方式可以实现它们的共存。

第一种方式是采用急切求值的 PFA 语法, 该语法已在 proposal-partial-application 中提出。 这种急切 PFA 语法将添加一个 …~(…) 运算符。 该运算符的右侧是一个参数列表, 其中每个参数都是一个普通表达式或一个 ? 占位符。 每个连续的 ? 占位符代表另一个参数。

普通表达式将在函数创建之前求值。 例如,f~(g(), ?, h(), ?) 会先求值 f,然后求值 g(),再求值 h()然后 它会创建一个带有两个参数的 f 的偏应用版本。

? 占位符后的可选数字 将覆盖参数的位置。 例如,f~(?1, ?0) 将有两个参数,但在调用 f 时会交换它们。

第二种方法采用惰性求值语法。 这可以通过扩展 Hack 管道来实现, 其语法进一步受到 Clojure 的 #(%1 %2) 函数字面量 的启发。 它通过组合 Hack 管道 |>箭头函数 => 形成一个管道函数运算符 +>, 该运算符将遵循与 |> 相同的一般规则。

+> 是一个前缀运算符,用于创建新函数, 该函数进而将其参数绑定到主题引用。 非一元函数通过包含带有数字的主题引用(%0%1%2 等)或 ... 来创建。 %0(等同于普通的 %)将绑定到第零个参数%1 将绑定到下一个参数,依此类推。 %... 将绑定到剩余参数数组。 并且正如 |> 一样,+> 要求其主体 至少包含一个主题引用 才能语法有效。

急切 PFA管道函数
a.map(f~(?, 0))a.map(+> f(%, 0))
a.map(f~(?, ?, 0))a.map(+> f(%0, %1, 0))
a.map(x=> x + 1)a.map(+> % + 1)
a.map(x=> x + x)a.map(+> % + %)
a.map(x=> f(x, x))a.map(+> f(%, %))

急切求值的 PFA 语法 不同, 主题函数将惰性求值其参数, 就像箭头函数那样。

例如,+> f(g(), %0, h(), %1) 会求值 f, 然后它会创建一个闭包,捕获 gh。 所创建的函数不会求值 g()h(), 直到每次调用所创建的函数时。

无论采取哪种方法,Hack pipes 都可以与 PFA 共存。

最终发送 / 管道化

尽管名称中都包含“pipe”一词, 但管道操作符和 eventual-send proposal 的远程对象管道 是正交且独立的。 它们可以共存,甚至可以协同工作。

const fileP = E(
  E(target).openDirectory(dirName)
).openFile(fileName);

const fileP = target
|> E(%).openDirectory(dirName)
|> E(%).openFile(fileName);

可能的未来扩展

针对 ifcatchforof 的 Hack-pipe 语法

许多 ifcatchfor 语句 如果获得了绑定主题引用的 “pipe 语法”, 可能会变得更加简洁。

if () |> 会将其条件值绑定到 %
catch |> 会将其捕获的错误绑定到 %
for (of) |> 会依次将其迭代器的每个值绑定到 %

现状Hack-pipe 语句语法
const c = f(); if (c) g(c);if (f()) |> g(%);
catch (e) f(e);catch |> f(%);
for (const v of f()) g(v);for (f()) |> g(%);

可选 Hack pipe

一个短路求值的可选 pipe 运算符 |?> 也可能很有用, 就像 ?. 对可选方法调用很有用一样。

例如,value |> (% == null ? % : await foo(%) |> (% == null ? % : % + 1))
将等价于 value |?> await foo(%) |?> % + 1

隐式一元函数应用语法

隐式一元函数应用语法 – 即 F# pipe 运算符 – 已被 TC39 两次否决。 然而,它们最终仍可能通过两种方式添加到语言中。

首先,它可以作为一个便捷函数 Function.pipe 添加。 这正是 function-helpers 提案 所提议的。 Function.pipe 可能会消除对 F#-pipe 运算符的大部分需求, 同时仍然不关闭添加 F#-pipe 运算符的可能性。

其次,它可以作为另一个管道运算符添加 |>> – 类似于 Clojure 拥有多个管道宏 ->->>as->
例如,value |> % + 1 |>> f |> g(%, 0)
将表示 value |> % + 1 |> f(%) |> g(%, 0)

曾有一个关于这种两个管道运算符的拆分混合非正式提案, 该提案被搁置,以支持单运算符提案。 这种拆分混合可能会在 Hack 管道之后作为提案重新出现。