跳至主内容

管道运算符 (|>) 提案进展如何?

· 1 分钟阅读
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

在发布 babel@7.0.0-beta52 时,我们为 @babel/plugin-proposal-pipeline-operator 引入了一个新的必需配置标志,这对管道运算符来说是一项破坏性变更。为澄清可能的困惑,让我们深入了解管道提案以及为何需要引入此配置选项。

当前状态

管道运算符最初由 Gilbert Garza 引入,旨在"以可读性强、函数式的方式简化链式函数调用"。该运算符源自 F#、Hack、Elm、Elixir 等多种语言,但在引入 JavaScript 时存在两个主要争议点:

  • 是否引入占位符及如何引入

  • 如何在管道中处理 async/await

占位符问题

首要争议是占位符问题。Kevin Smith此议题中首次提出,建议采用 Hack 风格的管道。在 Hack 中,管道的每个右侧都需要占位符,示例如下:

namespace Hack\UserDocumentation\Operators\Pipe\Examples\MapFilterCountPiped;

function piped_example(array<int> $arr): int {
return $arr
|> \array_map($x ==> $x * $x, $$)
|> \array_filter($$, $x ==> $x % 2 == 0)
|> \count($$);
}

var_dump(piped_example(range(1, 10)));

我们基于此概念进行扩展,因为占位符可轻松用于任意表达式,代表前一步骤的返回值。这为管道步骤提供了额外的灵活性和功能。

缺点在于引入新标记的复杂性。当前选择井号(#)作为占位符,尽管仍有争议空间,但任何标记都可能存在多重含义。井号已被私有字段提案使用,且其他所有选项均有不同形式的用途

异步处理

管道运算符初始提案包含以下 await 语法:

JavaScript
x |> await f

其脱糖(desugar)后等价于:

JavaScript
await f(x)

遗憾的是,用户可能期望另一种脱糖形式:

JavaScript
(await f)(x)

尽管存在反对在管道中处理异步操作的声音,但委员会成员担忧不支持 async/await 的管道运算符。虽然可通过显式语法之外的方式处理返回 Promise 的函数,但这些方法要么过于繁琐,要么需要辅助函数。

提案解决方案

经过讨论,形成了两个主要提案及一个基础最小提案:F# 管道和智能管道。下面解析它们如何解决上述问题。

最小管道提案

该提案涵盖管道运算符的基本功能。最小提案禁止使用 await(完全不涉及异步处理)且不包含占位符。其行为与我们引入配置前的 Babel 插件一致,也是当前管道运算符提案仓库中的规范。它更像是用于对比其他提案优劣的"稻草人"方案,若两个替代方案均无致命缺陷,该提案不太可能被直接采纳。

F# 管道提案

关于占位符问题,F# 管道主张无需引入。在基础提案中,箭头函数可替代占位符的功能,这样既减少新语法量,又基于开发者自 ES2015 以来已熟悉且广泛使用的语法构建。

按照当前规范,箭头函数必须包裹在括号中:

JavaScript
let person = { score: 25 };

let newScore = person.score
|> double
|> (_ => add(7, _))
|> (_ => boundScore(0, 100, _));

社区正在探索是否可能允许箭头函数省略括号,因为括号会带来显著的语法负担。

关于异步处理,F# 管道将 await 视为一元函数:

JavaScript
promise |> await

其等价转换形式为:

JavaScript
await promise

因此可在包含异步操作的函数链中这样使用:

JavaScript
promise
|> await
|> (x => doubleSay(x, ', '))
|> capitalize
|> (x => x + '!')
|> (x => new User.Message(x))
|> (x => stream.write(x))
|> await
|> console.log;

这种对 await 的特殊处理可能也适用于其他一元操作符(如 typeof),但 F# 管道初始版本并不支持。

智能管道(Smart Pipelines)

智能管道将占位符理念推向极致,使其既能管理部分应用也能处理管道中的任意表达式。上文的长链可改写为:

JavaScript
promise
|> await #
|> doubleSay(#, ', ')
|> # || throw new TypeError()
|> capitalize
|> # + '!'
|> new User.Message(#)
|> await stream.write(#)
|> console.log;

智能管道对占位符设有特定规则。若管道步骤中传入的是简单标识符(称为"裸风格"),则无需占位符标记:

JavaScript
x |> a;
x |> f.b;

与 Hack 不同,一元函数在此不需占位符标记。

对于其他表达式(称为"话题风格"),则必须使用占位符(称为"词法话题标记"),否则会抛出早期语法错误:

JavaScript
10 |> # + 1;
promise |> await #;

若表达式包含操作符、括号(含方法调用)、方括号或除标识符与点号外的任何元素,则必须使用话题标记。这避免了未使用话题标记时的隐患和歧义。

因此智能管道以集成化方式解决了异步问题,允许所有表达式(不仅是 await,还包括 typeofyield 等操作符)嵌入管道。

Babel 的介入角色

当三个提案均完善后,我们意识到仅靠讨论难以化解它们之间的根本矛盾。决定通过开发者在实际代码中的使用反馈来做最终选择。鉴于 Babel 在社区中的定位,我们决定在管道操作符插件中同时实现这三个提案。

由于各提案的解析逻辑存在差异,需要在 @babel/parser(原 babylon)中增加支持,并根据目标提案配置解析器。因此管道操作符插件需引入 "proposal" 选项,用于配置解析器及自身转换逻辑。

此项工作面临紧迫时间线:必须在 babel@7 结束 beta 前完成对 babel 核心、@babel/parser 及管道提案插件的破坏性变更。同时希望插件最终能默认采用被 TC39 接纳的提案,使配置选项自然淘汰。

基于这两点约束,我们决定引入这个必需配置项,强制用户明确项目所用提案。待某个提案成为管道操作符的规范实现后,我们将弃用 "proposal" 选项并默认采用该提案,被否决的提案将在后续主版本中移除。

参与贡献

如果你有兴趣参与管道操作符提案,所有讨论都是公开的,你可以在pipeline operator repository中找到相关内容。也可以查看上次TC39会议中的演示文稿。最后,你可以在Twitter上关注James DiGioiaJ. S. ChoiDaniel Ehrenberg

但最重要的是,当相关工作完成后,请在你的项目中亲自试用管道操作符!我们也在为repl添加配置选项,届时你也可以在那里体验代码。我们需要实际使用反馈和真实代码样本来完善这项功能,非常期待听到你的见解。欢迎通过Twitter@babeljs与我们联系。