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()。
然而,这些风格在可读性、流畅性和适用性上差异很大。
深层嵌套难以阅读
第一种风格,嵌套,通常具有通用性 –
它适用于任何操作序列:
函数调用、算术运算、数组/对象字面量、await 和 yield 等。
然而,当嵌套变得很深时,它难以阅读: 执行流程是从右到左移动的, 而不是像普通代码那样从左到右阅读。 如果在某些层级有多个参数, 阅读甚至会在来回之间跳跃: 我们的眼睛必须向左跳以找到函数名, 然后必须向右跳以找到额外的参数。 此外,之后编辑代码可能会充满风险: 我们必须在许多嵌套括号中找到正确的插入位置 以添加新参数。
现实世界示例
考虑这段来自 React 的现实世界代码。
console.log(
chalk.dim(
`$ ${Object.keys(envars)
.map(envar =>
`${envar}=${envars[envar]}`)
.join(' ')
}`,
'node',
args.join(' ')));
这段真实世界的代码由深度嵌套的表达式构成。 为了阅读其数据流,人的眼睛必须首先:
-
找到初始数据(最内层的表达式,
envars)。 -
然后对于每个数据转换,反复地来回从内到外扫描, 每一个转换要么是左侧容易忽略的前缀运算符, 要么是右侧的后缀运算符:
Object.keys()(左侧),.map()(右侧),.join()(右侧),- 模板字符串(两侧),
chalk.dim()(左侧),然后console.log()(左侧)。
由于深度嵌套了许多表达式 (其中一些使用前缀运算符, 一些使用后缀运算符, 还有一些使用中缀运算符), 我们必须检查左右两侧 才能找到每个表达式的头部。
方法链的局限性
第二种风格,方法链,仅在 该值拥有其类所指定的方法函数时才可使用。 这限制了其适用范围。 但当它适用时,得益于其后缀结构, 它通常更易于使用,且更易读易写。 代码执行流向为从左到右。 深层嵌套的表达式被解开。 函数调用的所有参数都与函数名分组在一起。 并且稍后编辑代码以插入或删除更多方法调用是微不足道的, 因为我们只需将光标放在一个位置, 然后开始输入或删除一段连续的字符。
事实上,方法链的好处如此诱人 以至于一些流行库扭曲了其代码结构 专门为了允许更多的方法链。 最典型的例子是 jQuery,它 仍然是世界上最流行的 JS 库。 jQuery 的核心设计是一个带有数十个方法的单一超级对象, 所有这些方法都返回相同的对象类型,以便我们可以继续链式调用。 这种编程风格甚至有一个名称: fluent interfaces。
不幸的是,尽管方法链流畅,
仅靠方法链无法容纳 JavaScript 的其他语法:
函数调用、算术运算、数组/对象字面量、await 和 yield 等。
因此,方法链在适用性方面仍然有限。
管道运算符融合两者
管道运算符试图将方法链的便捷与易用性 与表达式嵌套的广泛适用性结合起来。
所有管道运算符的一般结构为
value |> e1 |> e2 |> e3,
其中 e1、e2、e3
都是将连续的值作为参数的表达式。
然后 |> 运算符进行某种程度的“魔法”操作,将 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 表达式等上下文中。
另一方面,使用临时变量需要语句序列。
示例
| 管道 | 临时变量 |
|---|---|
|
|
|
|
为什么选择 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 |> three 将 value
通过这三个函数进行管道处理。
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 元函数调用。
缺点: await 和 yield 操作是作用域
限于其包含函数的,
因此无法仅由一元函数处理。
如果我们想将它们集成到管道表达式中,
[await 和 yield 必须作为特殊语法情况处理][enhanced F# pipes]:
value |> await用于等待 Promise,以及value |> yield用于生成器值。
Hack 管道更倾向于更常见的表达式
两者 Hack 管道和 F# 管道分别对不同的表达式
施加了少量的语法税:
Hack 管道仅对一元函数调用略微征税,而
F# 管道则对除一元函数调用外的所有表达式略微征税。
在两者提案中,每个被征税表达式的语法税都是小的
(两者 (%) 和 x=> 都只有三个字符)。
然而,该税是乘以其相应被征税表达式的普遍性的。
因此,对较少见的表达式征税
并优化以偏向更常见的表达式可能更有意义。
一元函数调用通常较少见 所有表达式除一元函数外。 特别是,方法调用和n 元函数调用 将始终是流行的; 在总体频率上, 一元函数调用等于或被 仅这两种情况所超过 – 更不用说其他无处不在的语法 如数组字面量、对象字面量 和算术运算。 本说明包含几个真实世界示例 关于这种普遍性差异。
此外,其他几种提议的新语法, 例如**extension calling、 do 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 其他多位代表的强烈反对。这些顾虑包括:
- 内存性能方面的顾虑(例如,特别是来自浏览器引擎实现者的反对),
- 关于
await的语法顾虑。 - 关于鼓励生态系统分裂/分叉等问题的顾虑。
这种反对意见来自管道提案主导小组外部。 更多信息请参阅 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 的 #(%) 函数字面量。)
管道运算符 |> 是一个中缀运算符,
用于构成管道表达式(也称为流水线)。
它先求值其左侧(管道头部或管道输入),
以不可变方式将所得值(主题值)绑定到主题引用,
然后在该绑定的情况下求值其右侧(管道主体)。
右侧的求值结果
成为整个管道表达式的最终值(管道输出)。
管道运算符的优先级与以下运算符相同:
- 函数箭头
=>; - 赋值运算符
=、+=等; - 生成器运算符
yield和yield *;
它仅比逗号运算符 , 更紧。
它比所有其他运算符更松。
例如,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) |> %.d
或 a |> (b ? % : c |> %.d)。
最后,在动态编译的代码内部的主题绑定
(例如,使用 eval 或 new 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, %);
}
// 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);
// 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 辅助函数提案 共存,
包括其 pipe 和 flow 函数。
这些简单(且下载量普遍较高)的便捷函数
在没有额外语法的情况下操作一元函数。
TC39 已两次否决 F# 管道运算符。
鉴于这一现实,TC39 通过
pipe 和 flow 辅助函数的可能性,远高于通过类似的语法运算符。
标准化的 pipe 和 flow 便捷函数
也可能消除对 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,
然后它会创建一个闭包,捕获 g 和 h。
所创建的函数不会求值 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);
可能的未来扩展
针对 if、catch 和 for–of 的 Hack-pipe 语法
许多 if、catch 和 for 语句 如果获得了绑定主题引用的 “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 管道之后作为提案重新出现。