|>) for JavaScript(This document uses %
as the placeholder token for the topic reference.
This will almost certainly not be the final choice;
see the token bikeshedding discussion for details.)
In the State of JS 2020 survey, the fourth top answer to “What do you feel is currently missing from JavaScript?” was a pipe operator. Why?
When we perform consecutive operations (e.g., function calls) on a value in JavaScript, there are currently two fundamental styles:
That is, three(two(one(value))) versus value.one().two().three().
However, these styles differ much in readability, fluency, and applicability.
The first style, nesting, is generally applicable –
it works for any sequence of operations:
function calls, arithmetic, array/object literals, await and yield, etc.
However, nesting is difficult to read when it becomes deep: the flow of execution moves right to left, rather than the left-to-right reading of normal code. If there are multiple arguments at some levels, reading even bounces back and forth: our eyes must jump left to find a function name, and then they must jump right to find additional arguments. Additionally, editing the code afterwards can be fraught: we must find the correct place to insert new arguments among many nested parentheses.
<details> <summary><strong>Real-world example</strong></summary>Consider this real-world code from React.
console.log( chalk.dim( `$ ${Object.keys(envars) .map(envar => `${envar}=${envars[envar]}`) .join(' ') }`, 'node', args.join(' ')));
This real-world code is made of deeply nested expressions. In order to read its flow of data, a human’s eyes must first:
Find the initial data (the innermost expression, envars).
And then scan back and forth repeatedly from inside out for each data transformation, each one either an easily missed prefix operator on the left or a suffix operators on the right:
Object.keys() (left side),.map() (right side),.join() (right side),chalk.dim() (left side), thenconsole.log() (left side).As a result of deeply nesting many expressions (some of which use prefix operators, some of which use postfix operators, and some of which use circumfix operators), we must check both left and right sides to find the head of each expression.
</details>The second style, method chaining, is only usable if the value has the functions designated as methods for its class. This limits its applicability. But when it applies, thanks to its postfix structure, it is generally more usable and easier to read and write. Code execution flows left to right. Deeply nested expressions are untangled. All arguments for a function call are grouped with the function’s name. And editing the code later to insert or delete more method calls is trivial, since we would just have to put our cursor in one spot, then start typing or deleting one contiguous run of characters.
Indeed, the benefits of method chaining are so attractive that some popular libraries contort their code structure specifically to allow more method chaining. The most prominent example is jQuery, which still remains the most popular JS library in the world. jQuery’s core design is a single über-object with dozens of methods on it, all of which return the same object type so that we can continue chaining. There is even a name for this style of programming: fluent interfaces.
Unfortunately, for all of its fluency,
method chaining alone cannot accommodate JavaScript’s other syntaxes:
function calls, arithmetic, array/object literals, await and yield, etc.
In this way, method chaining remains limited in its applicability.
The pipe operator attempts to marry the convenience and ease of method chaining with the wide applicability of expression nesting.
The general structure of all the pipe operators is
value |> <var>e1</var> |> <var>e2</var> |> <var>e3</var>,
where <var>e1</var>, <var>e2</var>, <var>e3</var>
are all expressions that take consecutive values as their parameters.
The |> operator then does some degree of magic to “pipe” value
from the lefthand side into the righthand side.
Continuing this deeply nested [real-world code from React][react/scripts/jest/jest-cli.js]:
console.log( chalk.dim( `$ ${Object.keys(envars) .map(envar => `${envar}=${envars[envar]}`) .join(' ') }`, 'node', args.join(' ')));
…we can untangle it as such using a pipe operator
and a placeholder token (%) standing in for the previous operation’s value:
Object.keys(envars) .map(envar => `${envar}=${envars[envar]}`) .join(' ') |> `$ ${%}` |> chalk.dim(%, 'node', args.join(' ')) |> console.log(%);
Now, the human reader can rapidly find the initial data
(what had been the most innermost expression, envars),
then linearly read, from left to right,
each transformation on the data.
One could argue that using temporary variables should be the only way to untangle deeply nested code. Explicitly naming every step’s variable causes something similar to method chaining to happen, with similar benefits to reading and writing code.
<details> <summary><strong>Real-world example</strong>, continued</summary>For example, using our previous modified [real-world example from React][react/scripts/jest/jest-cli.js]:
Object.keys(envars) .map(envar => `${envar}=${envars[envar]}`) .join(' ') |> `$ ${%}` |> chalk.dim(%, 'node', args.join(' ')) |> console.log(%);
…a version using temporary variables would look like this:
</details>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);
But there are reasons why we encounter deeply nested expressions in each other’s code all the time in the real world, rather than lines of temporary variables. And there are reasons why the method-chain-based fluent interfaces of jQuery, Mocha, and so on are still popular.
It is often simply too tedious and wordy to write code with a long sequence of temporary, single-use variables. It is arguably even tedious and visually noisy for a human to read, too.
If naming is one of the most difficult tasks in programming, then programmers will inevitably avoid naming variables when they perceive their benefit to be relatively small.
One could argue that using a single mutable variable with a short name would reduce the wordiness of temporary variables, achieving similar results as with the pipe operator.
<details> <summary><strong>Real-world example</strong>, continued</summary>For example, our previous modified [real-world example from React][react/scripts/jest/jest-cli.js] could be re-written like this:
</details>let _; _ = Object.keys(envars) .map(envar => `${envar}=${envars[envar]}`) .join(' '); _ = `$ ${_}`; _ = chalk.dim(_, 'node', args.join(' ')); _ = console.log(_);
But code like this is not common in real-world code. One reason for this is that mutable variables can change unexpectedly, causing silent bugs that are hard to find. For example, the variable might be accidentally referenced in a closure. Or it might be mistakenly reassigned within an expression.
<details> <summary>Example code</summary>// 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(_);
This issue would not happen with the pipe operator. The topic token cannot be reassigned, and code outside of each step cannot change its binding.
</details>let _; _ = one() |> double(%) |> Promise.resolve().then(() => // This prints 2, as intended. console.log(%)); _ = one();
For this reason, code with mutable variables is also harder to read. To determine what the variable represents at any given point, you must to search the entire preceding scope for places where it is reassigned.
The topic reference of a pipeline, on the other hand, has a limited lexical scope, and its binding is immutable within its scope. It cannot be accidentally reassigned, and it can be safely used in closures.
Although the topic value also changes with each pipeline step, we only scan the previous step of the pipeline to make sense of it, leading to code that is easier to read.
Another benefit of the pipe operator over sequences of assignment statements (whether with mutable or with immutable temporary variables) is that they are expressions.
Pipe expressions are expressions that can be directly returned, assigned to a variable, or used in contexts such as JSX expressions.
Using temporary variables, on the other hand, requires sequences of statements.
<details> <summary>Examples</summary> <table> <thead> <th>Pipelines</th> <th>Temporary Variables</th> </thead> <tbody> <tr> <td></td> <td>const envVarFormat = vars => Object.keys(vars) .map(var => `${var}=${vars[var]}`) .join(' ') |> chalk.dim(%, 'node', args.join(' '));
</td> </tr> <tr> <td>const envVarFormat = (vars) => { let _ = Object.keys(vars); _ = _.map(var => `${var}=${vars[var]}`); _ = _.join(' '); return chalk.dim(_, 'node', args.join(' ')); }
</td> <td>// This example uses JSX. return ( <ul> { values |> Object.keys(%) |> [...Array.from(new Set(%))] |> %.map(envar => ( <li onClick={ () => doStuff(values) }>{envar}</li> )) } </ul> );
</td> </tr> </tbody> </table> </details>// This example uses JSX. let _ = values; _= Object.keys(_); _= [...Array.from(new Set(_))]; _= _.map(envar => ( <li onClick={ () => doStuff(values) }>{envar}</li> )); return ( <ul>{_}</ul> );
There were two competing proposals for the pipe operator: Hack pipes and F# pipes. (Before that, there was a third proposal for a “smart mix” of the first two proposals, but it has been withdrawn, since its syntax is strictly a superset of one of the proposals’.)
The two pipe proposals just differ slightly on what the “magic” is,
when we spell our code when using |>.
Both proposals reuse existing language concepts: Hack pipes are based on the concept of the expression, while F# pipes are based on the concept of the unary function.
Piping expressions and piping unary functions correspondingly have small and nearly symmetrical trade-offs.
In the Hack language’s pipe syntax,
the righthand side of the pipe is an expression containing a special placeholder,
which is evaluated with the placeholder bound to the result of evaluating the lefthand side's expression.
That is, we write value |> one(%) |> two(%) |> three(%)
to pipe value through the three functions.
Pro: The righthand side can be any expression, and the placeholder can go anywhere any normal variable identifier could go, so we can pipe to any code we want without any special rules:
value |> foo(%) for unary function calls,value |> foo(1, %) for n-ary function calls,value |> %.foo() for method calls,value |> % + 1 for arithmetic,value |> [%, 0] for array literals,value |> {foo: %} for object literals,value |> `${%}` for template literals,value |> new Foo(%) for constructing objects,value |> await % for awaiting promises,value |> (yield %) for yielding generator values,value |> import(%) for calling function-like keywords,Con: Piping through unary functions
is slightly more verbose with Hack pipes than with F# pipes.
This includes unary functions
that were created by function-currying libraries like Ramda,
as well as unary arrow functions
that perform complex destructuring on their arguments:
Hack pipes would be slightly more verbose
with an explicit function call suffix (%).
(Complex destructuring of the topic value will be easier when [do expressions][] progress, as you will then be able to do variable assignment/destructuring inside of a pipe body.)
In the F# language’s pipe syntax,
the righthand side of the pipe is an expression
that must evaluate into a unary function,
which is then tacitly called
with the lefthand side’s value as its sole argument.
That is, we write value |> one |> two |> three to pipe value
through the three functions.
left |> right becomes right(left).
This is called tacit programming or point-free style.
For example, using our previous modified [real-world example from React][react/scripts/jest/jest-cli.js]:
Object.keys(envars) .map(envar => `${envar}=${envars[envar]}`) .join(' ') |> `$ ${%}` |> chalk.dim(%, 'node',


职场AI,就用扣子
AI办公助手,复杂任务高效处理。办公效率低?扣子空间AI助手支持播客生成、PPT制作、网页开发及报告写作,覆盖科研、商业、舆情等领域的专家Agent 7x24小时响应,生活工作无缝切换,提升50%效率!


多风格AI绘画神器
堆友平台由阿里巴巴设计团队创建,作为一款AI驱动的设计工具,专为设计师提供一站式增长服务。功能覆盖海量3D素材、AI绘画、实时渲染以及专业抠图,显著提升设计品质和效率。平台不仅提供工具,还是一个促进创意交流和个人发展的空间,界面友好,适合所有级别的设计师和创意工作者。


零代码AI应用开发平台
零代码AI应用开发平台,用户只需一句话简单描述需求,AI能自动生成小程序、APP或H5网页应用,无需编写代码。


免费创建高清无水印Sora视频
Vora是一个免费创建高清无水印Sora视频的AI工具


最适合小白的AI自动化工作流平台
无需编码,轻松生成可复用、可变现的AI自动化工作流

大模型驱动的Excel数据处理工具
基于大模型交互的表格处理系统,允许用户通过对话方式完成数据整理和可视化分析。系统采用机器学习算法解析用户指令,自动执行排序、公式计算和数据透视等操作,支持多种文件格式导入导出。数据处理响应速度保持在0.8秒以内,支持超过100万行数据的即时分析。


AI辅助编程,代码自动修复
Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。