|>) 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音乐社区,致力让每个人都能用音乐表达自我。音述AI提供零门槛AI创作工具,独创GETI法则帮助用户精准定义音乐风格,AI润色功能支持自动优化作品质感。音述AI支持交流讨论、二次创作与价值变现。针对中文用户的语言习惯与文化背景进行专门优化,支持国风融合、C-pop等本土音乐标签,让技术更好地承载人文表达。


一站式搞定所有学习需求
不再被海量信息淹没,开始真正理解知识。Lynote 可摘要 YouTube 视频、PDF、文章等内容。即时创建笔记,检测 AI 内容并下载资料,将您的学习效率提升 10 倍。


为AI短剧协作而生
专为AI短剧协作而生的AniShort正式发布,深度重构AI短剧全流程生产模式,整合创意策划、制作执行、实时协作、在线审片、资产复用等全链路功能,独创无限画布、双轨并行工业化工作流与Ani智能体助手,集成多款主流AI大模型,破解素材零散、版本混乱、沟通低效等行业痛点,助力3人团队效率提升800%,打造标准化、可追溯的AI短剧量产体系,是AI短剧团队协同创作、提升制作效率的核心工具。


能听懂你表达的视频模型
Seedance two是基于seedance2.0的中国大模型,支持图像、视频、音频、文本四种模态输入,表达方式更丰富,生成也更可控。


国内直接访问,限时3折
输入简单文字,生成想要的图片,纳米香蕉中文站基于 Google 模型的 AI 图片生成网站,支持文字生图、图生图。官网价格限时3折活动


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


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


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


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


最适合小白的AI自动化工作流平台
无需编码,轻松生成可复用、可变现的AI自动化工作流
最新AI工具、AI资讯
独家AI资源、AI项目落地

微信扫一扫关注公众号