{"id":13482008,"url":"https://github.com/tc39/proposal-pipeline-operator","last_synced_at":"2025-04-07T05:08:32.889Z","repository":{"id":38970850,"uuid":"45870793","full_name":"tc39/proposal-pipeline-operator","owner":"tc39","description":"A proposal for adding a useful pipe operator to JavaScript.","archived":false,"fork":false,"pushed_at":"2023-11-30T23:28:19.000Z","size":392,"stargazers_count":7538,"open_issues_count":30,"forks_count":108,"subscribers_count":254,"default_branch":"main","last_synced_at":"2024-10-29T15:10:33.508Z","etag":null,"topics":["javascript","operator","pipeline","proposal","syntax","tc39"],"latest_commit_sha":null,"homepage":"http://tc39.github.io/proposal-pipeline-operator/","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tc39.png","metadata":{"files":{"readme":"README.md","changelog":"HISTORY.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-11-09T22:23:36.000Z","updated_at":"2024-10-29T13:02:53.000Z","dependencies_parsed_at":"2024-06-10T08:02:42.879Z","dependency_job_id":null,"html_url":"https://github.com/tc39/proposal-pipeline-operator","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-pipeline-operator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-pipeline-operator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-pipeline-operator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-pipeline-operator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tc39","download_url":"https://codeload.github.com/tc39/proposal-pipeline-operator/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247595334,"owners_count":20963943,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript","operator","pipeline","proposal","syntax","tc39"],"created_at":"2024-07-31T17:00:58.178Z","updated_at":"2025-04-07T05:08:32.870Z","avatar_url":"https://github.com/tc39.png","language":"HTML","funding_links":[],"categories":["HTML","javascript","📦 Legacy \u0026 Inactive Projects"],"sub_categories":[],"readme":"# Pipe Operator (`|\u003e`) for JavaScript\n\n* **Stage**: 2\n* **Champions**: J. S. Choi, James DiGioia, Ron Buckton, Tab Atkins-Bittner, \\[list incomplete] \u003c!-- Alpha order by first name, plz --\u003e\n* **Former champions**: Daniel Ehrenberg\n* **[Specification][]**\n* **[Contributing guidelines][]**\n* **[Proposal history][]**\n* **Babel plugin**: [Implemented in v7.15][Babel 7.15]. See [Babel documentation][].\n\n(This document uses `%`\nas the placeholder token for the topic reference.\nThis will ***almost certainly not be the final choice***;\nsee [the token bikeshedding discussion][token bikeshedding] for details.)\n\n[specification]: http://tc39.github.io/proposal-pipeline-operator/\n[Babel 7.15]: https://babeljs.io/blog/2021/07/26/7.15.0#hack-style-pipeline-operator-support-13191httpsgithubcombabelbabelpull13191-13416httpsgithubcombabelbabelpull13416\n[Babel documentation]: https://babeljs.io/docs/en/babel-plugin-proposal-pipeline-operator\n[token bikeshedding]: https://github.com/tc39/proposal-pipeline-operator/issues/91\n[contributing guidelines]: https://github.com/tc39/proposal-pipeline-operator/blob/main/CONTRIBUTING.md\n[proposal history]: https://github.com/tc39/proposal-pipeline-operator/blob/main/HISTORY.md\n\n## Why a pipe operator\nIn the State of JS 2020 survey, the **fourth top answer** to\n[“What do you feel is currently missing from\nJavaScript?”](https://2020.stateofjs.com/en-US/opinions/#missing_from_js)\nwas a **pipe operator**. Why?\n\nWhen we perform **consecutive operations** (e.g., function calls)\non a **value** in JavaScript,\nthere are currently two fundamental styles:\n* passing the value as an argument to the operation\n  (**nesting** the operations if there are multiple operations),\n* or calling the function as a method on the value\n  (**chaining** more method calls if there are multiple methods).\n\nThat is, `three(two(one(value)))` versus `value.one().two().three()`.\nHowever, these styles differ much in readability, fluency, and applicability.\n\n### Deep nesting is hard to read\nThe first style, **nesting**, is generally applicable –\nit works for any sequence of operations:\nfunction calls, arithmetic, array/object literals, `await` and `yield`, etc.\n\nHowever, nesting is **difficult to read** when it becomes deep:\nthe flow of execution moves **right to left**,\nrather than the left-to-right reading of normal code.\nIf there are **multiple arguments** at some levels,\nreading even bounces **back and forth**:\nour eyes must **jump left** to find a function name,\nand then they must **jump right** to find additional arguments.\nAdditionally, **editing** the code afterwards can be fraught:\nwe must find the correct **place to insert** new arguments\namong **many nested parentheses**.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eReal-world example\u003c/strong\u003e\u003c/summary\u003e\n\nConsider this [real-world code from React](https://github.com/facebook/react/blob/17.0.2/scripts/jest/jest-cli.js#L295).\n\n```js\nconsole.log(\n  chalk.dim(\n    `$ ${Object.keys(envars)\n      .map(envar =\u003e\n        `${envar}=${envars[envar]}`)\n      .join(' ')\n    }`,\n    'node',\n    args.join(' ')));\n```\n\nThis real-world code is made of **deeply nested expressions**.\nIn order to read its flow of data, a human’s eyes must first:\n\n1. Find the **initial data** (the innermost expression, `envars`).\n2. And then scan **back and forth** repeatedly from **inside out**\n   for each data transformation,\n   each one either an easily missed prefix operator on the left\n   or a suffix operators on the right:\n\n   1. `Object.keys()` (left side),\n   2. `.map()` (right side),\n   3. `.join()` (right side),\n   4. A template literal (both sides),\n   5. `chalk.dim()` (left side), then\n   6. `console.log()` (left side).\n\nAs a result of deeply nesting many expressions\n(some of which use **prefix** operators,\nsome of which use **postfix** operators,\nand some of which use **circumfix** operators),\nwe must check **both left and right sides**\nto find the **head** of **each expression**.\n\n\u003c/details\u003e\n\n### Method chaining is limited\nThe second style, **method chaining**, is **only** usable\nif the value has the functions designated as **methods** for its class.\nThis **limits** its applicability.\nBut **when** it applies, thanks to its postfix structure,\nit is generally more usable and **easier** to read and write.\nCode execution flows **left to right**.\nDeeply nested expressions are **untangled**.\nAll arguments for a function call are **grouped** with the function’s name.\nAnd editing the code later to **insert or delete** more method calls is trivial,\nsince we would just have to put our cursor in one spot,\nthen start typing or deleting one **contiguous** run of characters.\n\nIndeed, the benefits of method chaining are **so attractive**\nthat some **popular libraries contort** their code structure\nspecifically to allow **more method chaining**.\nThe most prominent example is **[jQuery][]**, which\nstill remains the **most popular JS library** in the world.\njQuery’s core design is a single über-object with dozens of methods on it,\nall of which return the same object type so that we can **continue chaining**.\nThere is even a name for this style of programming:\n**[fluent interfaces][]**.\n\n[jQuery]: https://jquery.com/\n[fluent interfaces]: https://en.wikipedia.org/wiki/Fluent_interface\n\nUnfortunately, for all of its fluency,\n**method chaining** alone cannot accommodate JavaScript’s **other syntaxes**:\nfunction calls, arithmetic, array/object literals, `await` and `yield`, etc.\nIn this way, method chaining remains **limited** in its **applicability**.\n\n### Pipe operators combine both worlds\nThe pipe operator attempts to marry the **convenience** and ease of **method chaining**\nwith the wide **applicability** of **expression nesting**.\n\nThe general structure of all the pipe operators is\n`value |\u003e` \u003cvar\u003ee1\u003c/var\u003e `|\u003e` \u003cvar\u003ee2\u003c/var\u003e `|\u003e` \u003cvar\u003ee3\u003c/var\u003e,\nwhere \u003cvar\u003ee1\u003c/var\u003e, \u003cvar\u003ee2\u003c/var\u003e, \u003cvar\u003ee3\u003c/var\u003e\nare all expressions that take consecutive values as their parameters.\nThe `|\u003e` operator then does some degree of magic to “pipe” `value`\nfrom the lefthand side into the righthand side.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eReal-world example\u003c/strong\u003e, continued\u003c/summary\u003e\n\nContinuing this deeply nested [real-world code from React][react/scripts/jest/jest-cli.js]:\n\n```js\nconsole.log(\n  chalk.dim(\n    `$ ${Object.keys(envars)\n      .map(envar =\u003e\n        `${envar}=${envars[envar]}`)\n      .join(' ')\n    }`,\n    'node',\n    args.join(' ')));\n```\n\n…we can **untangle** it as such using a pipe operator\nand a placeholder token (`%`) standing in for the previous operation’s value:\n\n```js\nObject.keys(envars)\n  .map(envar =\u003e `${envar}=${envars[envar]}`)\n  .join(' ')\n  |\u003e `$ ${%}`\n  |\u003e chalk.dim(%, 'node', args.join(' '))\n  |\u003e console.log(%);\n```\n\nNow, the human reader can **rapidly find** the **initial data**\n(what had been the most innermost expression, `envars`),\nthen **linearly** read, from **left to right**,\neach transformation on the data.\n\n\u003c/details\u003e\n\n### Temporary variables are often tedious\nOne could argue that using **temporary variables**\nshould be the only way to untangle deeply nested code.\nExplicitly naming every step’s variable\ncauses something similar to method chaining to happen,\nwith similar benefits to reading and writing code.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eReal-world example\u003c/strong\u003e, continued\u003c/summary\u003e\n\nFor example, using our previous modified\n[real-world example from React][react/scripts/jest/jest-cli.js]:\n\n```js\nObject.keys(envars)\n  .map(envar =\u003e `${envar}=${envars[envar]}`)\n  .join(' ')\n  |\u003e `$ ${%}`\n  |\u003e chalk.dim(%, 'node', args.join(' '))\n  |\u003e console.log(%);\n```\n\n…a version using temporary variables would look like this:\n\n```js\nconst envarString = Object.keys(envars)\n  .map(envar =\u003e `${envar}=${envars[envar]}`)\n  .join(' ');\nconst consoleText = `$ ${envarString}`;\nconst coloredConsoleText = chalk.dim(consoleText, 'node', args.join(' '));\nconsole.log(coloredConsoleText);\n```\n\n\u003c/details\u003e\n\nBut there are reasons why we encounter deeply nested expressions\nin each other’s code **all the time in the real world**,\n**rather than** lines of temporary variables.\nAnd there are reasons why the **method-chain-based [fluent interfaces][]**\nof jQuery, Mocha, and so on are still **popular**.\n\nIt is often simply too **tedious and wordy** to **write**\ncode with a long sequence of temporary, single-use variables.\nIt is arguably even tedious and visually noisy for a human to **read**, too.\n\nIf [**naming** is one of the **most difficult tasks** in programming][naming hard],\nthen programmers will **inevitably avoid naming** variables\nwhen they perceive their benefit to be relatively small.\n\n[naming hard]: https://martinfowler.com/bliki/TwoHardThings.html\n\n### Reusing temporary variables is prone to unexpected mutation\nOne could argue that using a single **mutable variable** with a short name\nwould reduce the wordiness of temporary variables, achieving\nsimilar results as with the pipe operator.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eReal-world example\u003c/strong\u003e, continued\u003c/summary\u003e\n\nFor example, our previous modified\n[real-world example from React][react/scripts/jest/jest-cli.js]\ncould be re-written like this:\n```js\nlet _;\n_ = Object.keys(envars)\n  .map(envar =\u003e `${envar}=${envars[envar]}`)\n  .join(' ');\n_ = `$ ${_}`;\n_ = chalk.dim(_, 'node', args.join(' '));\n_ = console.log(_);\n```\n\n\u003c/details\u003e\n\nBut code like this is **not common** in real-world code.\nOne reason for this is that mutable variables can **change unexpectedly**,\ncausing silent bugs that are hard to find.\nFor example, the variable might be accidentally referenced in a closure.\nOr it might be mistakenly reassigned within an expression.\n\n\u003cdetails\u003e\n\u003csummary\u003eExample code\u003c/summary\u003e\n\n```js\n// setup\nfunction one () { return 1; }\nfunction double (x) { return x * 2; }\n\nlet _;\n_ = one(); // _ is now 1.\n_ = double(_); // _ is now 2.\n_ = Promise.resolve().then(() =\u003e\n  // This does *not* print 2!\n  // It prints 1, because `_` is reassigned downstream.\n  console.log(_));\n\n// _ becomes 1 before the promise callback.\n_ = one(_);\n```\n\nThis issue would not happen with the pipe operator.\nThe topic token cannot be reassigned, and\ncode outside of each step cannot change its binding.\n\n```js\nlet _;\n_ = one()\n  |\u003e double(%)\n  |\u003e Promise.resolve().then(() =\u003e\n    // This prints 2, as intended.\n    console.log(%));\n\n_ = one();\n```\n\n\u003c/details\u003e\n\nFor this reason, code with mutable variables is also harder to read.\nTo determine what the variable represents at any given point,\nyou must to **search the entire preceding scope** for places where it is **reassigned**.\n\nThe topic reference of a pipeline, on the other hand, has a limited lexical scope,\nand its binding is immutable within its scope.\nIt cannot be accidentally reassigned, and it can be safely used in closures.\n\nAlthough the topic value also changes with each pipeline step,\nwe only scan the previous step of the pipeline to make sense of it,\nleading to code that is easier to read.\n\n### Temporary variables must be declared in statements\nAnother benefit of the pipe operator over sequences of assignment statements\n(whether with mutable or with immutable temporary variables)\nis that they are **expressions**.\n\nPipe expressions are expressions that can be directly returned,\nassigned to a variable, or used in contexts such as JSX expressions.\n\nUsing temporary variables, on the other hand, requires sequences of statements.\n\n\u003cdetails\u003e\n\u003csummary\u003eExamples\u003c/summary\u003e\n\n\u003ctable\u003e\n\u003cthead\u003e\n\u003cth\u003ePipelines\u003c/th\u003e\n\u003cth\u003eTemporary Variables\u003c/th\u003e\n\u003c/thead\u003e\n\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\n\n```js\nconst envVarFormat = vars =\u003e\n  Object.keys(vars)\n    .map(var =\u003e `${var}=${vars[var]}`)\n    .join(' ')\n    |\u003e chalk.dim(%, 'node', args.join(' '));\n```\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```js\nconst envVarFormat = (vars) =\u003e {\n  let _ = Object.keys(vars);\n  _ = _.map(var =\u003e `${var}=${vars[var]}`);\n  _ = _.join(' ');\n  return chalk.dim(_, 'node', args.join(' '));\n}\n```\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\n\n```jsx\n// This example uses JSX.\nreturn (\n  \u003cul\u003e\n    {\n      values\n        |\u003e Object.keys(%)\n        |\u003e [...Array.from(new Set(%))]\n        |\u003e %.map(envar =\u003e (\n          \u003cli onClick={\n            () =\u003e doStuff(values)\n          }\u003e{envar}\u003c/li\u003e\n        ))\n    }\n  \u003c/ul\u003e\n);\n```\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```js\n// This example uses JSX.\nlet _ = values;\n_= Object.keys(_);\n_= [...Array.from(new Set(_))];\n_= _.map(envar =\u003e (\n  \u003cli onClick={\n    () =\u003e doStuff(values)\n  }\u003e{envar}\u003c/li\u003e\n));\nreturn (\n  \u003cul\u003e{_}\u003c/ul\u003e\n);\n```\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003c/details\u003e\n\n## Why the Hack pipe operator\nThere were **two competing proposals** for the pipe operator: Hack pipes and F# pipes.\n(Before that, there **was** a [third proposal for a “smart mix” of the first two proposals][smart mix],\nbut it has been withdrawn,\nsince its syntax is strictly a superset of one of the proposals’.)\n\n[smart mix]: https://github.com/js-choi/proposal-smart-pipelines/\n\nThe two pipe proposals just differ **slightly** on what the “magic” is,\nwhen we spell our code when using `|\u003e`.\n\n**Both** proposals **reuse** existing language concepts:\nHack pipes are based on the concept of the **expression**,\nwhile F# pipes are based on the concept of the **unary function**.\n\nPiping **expressions** and piping **unary functions**\ncorrespondingly have **small** and nearly **symmetrical trade-offs**.\n\n### This proposal: Hack pipes\nIn the **Hack language**’s pipe syntax,\nthe righthand side of the pipe is an **expression** containing a special **placeholder**,\nwhich is evaluated with the placeholder bound to the result of evaluating the lefthand side's expression.\nThat is, we write `value |\u003e one(%) |\u003e two(%) |\u003e three(%)`\nto pipe `value` through the three functions.\n\n**Pro:** The righthand side can be **any expression**,\nand the placeholder can go anywhere any normal variable identifier could go,\nso we can pipe to any code we want **without any special rules**:\n\n* `value |\u003e foo(%)` for unary function calls,\n* `value |\u003e foo(1, %)` for n-ary function calls,\n* `value |\u003e %.foo()` for method calls,\n* `value |\u003e % + 1` for arithmetic,\n* `value |\u003e [%, 0]` for array literals,\n* `value |\u003e {foo: %}` for object literals,\n* `` value |\u003e `${%}` `` for template literals,\n* `value |\u003e new Foo(%)` for constructing objects,\n* `value |\u003e await %` for awaiting promises,\n* `value |\u003e (yield %)` for yielding generator values,\n* `value |\u003e import(%)` for calling function-like keywords,\n* etc.\n\n**Con:** Piping through **unary functions**\nis **slightly more verbose** with Hack pipes than with F# pipes.\nThis includes unary functions\nthat were created by **[function-currying][] libraries** like [Ramda][],\nas well as [unary arrow functions\nthat perform **complex destructuring** on their arguments][destruct]:\nHack pipes would be slightly more verbose\nwith an **explicit** function call suffix `(%)`.\n\n(Complex destructuring of the topic value\nwill be easier when [do expressions][] progress,\nas you will then be able to do variable assignment/destructuring\ninside of a pipe body.)\n\n[function-currying]: https://en.wikipedia.org/wiki/Currying\n[Ramda]: https://ramdajs.com/\n[destruct]: https://github.com/js-choi/proposal-hack-pipes/issues/4#issuecomment-817208635\n\n### Alternative proposal: F# pipes\nIn the [**F# language**’s pipe syntax][F# pipes],\nthe righthand side of the pipe is an expression\nthat must **evaluate into a unary function**,\nwhich is then **tacitly called**\nwith the lefthand side’s value as its **sole argument**.\nThat is, we write `value |\u003e one |\u003e two |\u003e three` to pipe `value`\nthrough the three functions.\n`left |\u003e right` becomes `right(left)`.\nThis is called [tacit programming or point-free style][tacit].\n\n[F# pipes]: https://github.com/valtech-nyc/proposal-fsharp-pipelines\n[tacit]: https://en.wikipedia.org/wiki/Tacit_programming\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eReal-world example\u003c/strong\u003e, continued\u003c/summary\u003e\n\nFor example, using our previous modified\n[real-world example from React][react/scripts/jest/jest-cli.js]:\n\n```js\nObject.keys(envars)\n  .map(envar =\u003e `${envar}=${envars[envar]}`)\n  .join(' ')\n  |\u003e `$ ${%}`\n  |\u003e chalk.dim(%, 'node', args.join(' '))\n  |\u003e console.log(%);\n```\n\n…a version using F# pipes instead of Hack pipes would look like this:\n\n```js\nObject.keys(envars)\n  .map(envar =\u003e `${envar}=${envars[envar]}`)\n  .join(' ')\n  |\u003e x=\u003e `$ ${x}`\n  |\u003e x=\u003e chalk.dim(x, 'node', args.join(' '))\n  |\u003e console.log;\n```\n\n\u003c/details\u003e\n\n**Pro:** The restriction that the righthand side\n**must** resolve to a unary function\nlets us write very terse pipes\n**when** the operation we want to perform\nis a **unary function call**:\n\n* `value |\u003e foo` for unary function calls.\n\nThis includes unary functions\nthat were created by **[function-currying][] libraries** like [Ramda][],\nas well as [unary arrow functions\nthat perform **complex destructuring** on their arguments][destruct]:\nF# pipes would be **slightly less verbose**\nwith an **implicit** function call (no `(%)`).\n\n**Con:** The restriction means that **any operations**\nthat are performed by **other syntax**\nmust be made **slightly more verbose** by **wrapping** the operation\nin a unary **arrow function**:\n\n* `value |\u003e x=\u003e x.foo()` for method calls,\n* `value |\u003e x=\u003e x + 1` for arithmetic,\n* `value |\u003e x=\u003e [x, 0]` for array literals,\n* `value |\u003e x=\u003e ({foo: x})` for object literals,\n* `` value |\u003e x=\u003e `${x}` `` for template literals,\n* `value |\u003e x=\u003e new Foo(x)` for constructing objects,\n* `value |\u003e x=\u003e import(x)` for calling function-like keywords,\n* etc.\n\nEven calling **named functions** requires **wrapping**\nwhen we need to pass **more than one argument**:\n\n* `value |\u003e x=\u003e foo(1, x)` for n-ary function calls.\n\n**Con:** The **`await` and `yield`** operations are **scoped**\nto their **containing function**,\nand thus **cannot be handled by unary functions** alone.\nIf we want to integrate them into a pipe expression,\n[`await` and `yield` must be handled as **special syntax cases**][enhanced F# pipes]:\n\n* `value |\u003e await` for awaiting promises, and\n* `value |\u003e yield` for yielding generator values.\n\n[enhanced F# pipes]: https://github.com/valtech-nyc/proposal-fsharp-pipelines/\n\n### Hack pipes favor more common expressions\n**Both** Hack pipes and F# pipes respectively impose\na small **syntax tax** on different expressions:\\\n**Hack pipes** slightly tax only **unary function calls**, and\\\n**F# pipes** slightly tax **all expressions except** unary function calls.\n\nIn **both** proposals, the syntax tax per taxed expression is **small**\n(**both** `(%)` and `x=\u003e` are **only three characters**).\nHowever, the tax is **multiplied** by the **prevalence**\nof its respectively taxed expressions.\nIt therefore might make sense\nto impose a tax on whichever expressions are **less common**\nand to **optimize** in favor of whichever expressions are **more common**.\n\nUnary function calls are in general **less common**\nthan **all** expressions **except** unary functions.\nIn particular, **method** calling and **n-ary function** calling\nwill **always** be **popular**;\nin general frequency,\n**unary** function calling is equal to or exceeded by\nthose two cases **alone** –\nlet alone by other ubiquitous syntaxes\nsuch as **array literals**, **object literals**,\nand **arithmetic operations**.\nThis explainer contains several [real-world examples][]\nof this difference in prevalence.\n\n[real-world examples]: #real-world-examples\n\nFurthermore, several other proposed **new syntaxes**,\nsuch as **[extension calling][]**,\n**[do expressions][]**,\nand **[record/tuple literals][]**,\nwill also likely become **pervasive** in the **future**.\nLikewise, **arithmetic** operations would also become **even more common**\nif TC39 standardizes **[operator overloading][]**.\nUntangling these future syntaxes’ expressions would be more fluent\nwith Hack pipes compared to F# pipes.\n\n[extension calling]: https://github.com/tc39/proposal-extensions/\n[do expressions]: https://github.com/tc39/proposal-do-expressions/\n[record/tuple literals]: https://github.com/tc39/proposal-record-tuple/\n[operator overloading]: https://github.com/tc39/proposal-operator-overloading/\n\n### Hack pipes might be simpler to use\nThe syntax tax of Hack pipes on unary function calls\n(i.e., the `(%)` to invoke the righthand side’s unary function)\nis **not a special case**:\nit simply is **explicitly writing ordinary code**,\nin **the way we normally would** without a pipe.\n\nOn the other hand, **F# pipes require** us to **distinguish**\nbetween “code that resolves to an unary function”\nversus **“any other expression”** –\nand to remember to add the arrow-function wrapper around the latter case.\n\nFor example, with Hack pipes, `value |\u003e someFunction + 1`\nis **invalid syntax** and will **fail early**.\nThere is no need to recognize that `someFunction + 1`\nwill not evaluate into a unary function.\nBut with F# pipes, `value |\u003e someFunction + 1` is **still valid syntax** –\nit’ll just **fail late** at **runtime**,\nbecause `someFunction + 1` isn’t callable.\n\n### TC39 has rejected F# pipes multiple times\nThe pipe champion group has presented F# pipes for Stage 2 to TC39 **twice**.\nIt was **unsuccessful** in advancing to Stage 2 both times.\nBoth F# pipes (and [partial function application (PFA)][PFA syntax])\nhave run into strong pushback from multiple other TC39 representatives\ndue to various concerns. These have included:\n\n* Memory performance concerns (e.g., [especially from browser-engine implementors][V8 pushback]),\n* Syntax concerns about `await`.\n* Concerns about encouraging ecosystem bifurcation/forking, etc.\n\n[V8 pushback]: https://github.com/tc39/proposal-pipeline-operator/blob/main/HISTORY.md#2021-07\n\nThis pushback has occurred from **outside** the pipe champion group.\nSee [HISTORY.md][] for more information.\n\nIt is the pipe champion group’s belief that any pipe operator is better than none,\nin order to [easily linearize deeply nested expressions](#why-a-pipe-operator)\nwithout resorting to named variables.\nMany members of the champion group believe that Hack pipes are slightly better than F# pipes,\nand some members of the champion group believe that F# pipes are slightly better than Hack pipes.\nBut everyone in the champion group agrees that F# pipes have met with far too much resistance\nto be able to pass TC39 in the foreseeable future.\n\nTo emphasize, it is likely that an attempt to switch from Hack pipes back to F# pipes\nwill result in TC39 never agreeing to any pipes at all.\n[PFA syntax][] is similarly facing an uphill battle in TC39 (see [HISTORY.md][]).\nMany members of the pipe champion group think this is unfortunate,\nand they are willing to fight again **later** for an [F#-pipe split mix][split mix] and [PFA syntax][].\nBut there are quite a few representatives (including [browser-engine implementers][V8 pushback])\noutside of the Pipe Champion Group\nwho are generally against encouraging [tacit programming][] (and [PFA syntax][]),\nregardless of Hack pipes.\n\n[HISTORY.md]: https://github.com/tc39/proposal-pipeline-operator/blob/main/HISTORY.md\n[tacit programming]: https://en.wikipedia.org/wiki/Tacit_programming\n[PFA syntax]: https://github.com/tc39/proposal-partial-application\n[split mix]: #tacit-unary-function-application-syntax\n\n## Description\n(A [formal draft specification][specification] is available.)\n\nThe **topic reference** `%` is a **nullary operator**.\nIt acts as a placeholder for a **topic value**,\nand it is **lexically scoped** and **immutable**.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003e%\u003c/code\u003e is not a final choice\u003c/summary\u003e\n\n(The precise [**token** for the topic reference is **not final**][token bikeshedding].\n`%` could instead be `^`, or many other tokens.\nWe plan to [**bikeshed** what actual token to use][token bikeshedding]\nbefore advancing to Stage 3.\nHowever, `%` seems to be the [least syntactically problematic][],\nand it also resembles the placeholders of **[printf format strings][]**\nand [**Clojure**’s `#(%)` **function literals**][Clojure function literals].)\n\n[least syntactically problematic]: https://github.com/js-choi/proposal-hack-pipes/issues/2\n[Clojure function literals]: https://clojure.org/reference/reader#_dispatch\n[printf format strings]: https://en.wikipedia.org/wiki/Printf_format_string\n\n\u003c/details\u003e\n\nThe **pipe operator** `|\u003e` is an **infix operator**\nthat forms a **pipe expression** (also called a **pipeline**).\nIt evaluates its lefthand side (the **pipe head** or **pipe input**),\nimmutably **binds** the resulting value (the **topic value**) to the **topic reference**,\nthen evaluates its righthand side (the **pipe body**) with that binding.\nThe resulting value of the righthand side\nbecomes the whole pipe expression’s final value (the **pipe output**).\n\nThe pipe operator’s precedence is the **same** as:\n* the function arrow `=\u003e`;\n* the assignment operators `=`, `+=`, etc.;\n* the generator operators `yield` and `yield *`;\n\nIt is **tighter** than only the comma operator `,`.\\\nIt is **looser** than **all other** operators.\n\nFor example, `v =\u003e v |\u003e % == null |\u003e foo(%, 0)`\\\nwould group into `v =\u003e (v |\u003e (% == null) |\u003e foo(%, 0))`,\\\nwhich in turn is equivalent to `v =\u003e foo(v == null, 0)`.\n\nA pipe body **must** use its topic value **at least once**.\nFor example, `value |\u003e foo + 1` is **invalid syntax**,\nbecause its body does not contain a topic reference.\nThis design is because **omission** of the topic reference\nfrom a pipe expression’s body\nis almost certainly an **accidental** programmer error.\n\nLikewise, a topic reference **must** be contained in a pipe body.\nUsing a topic reference outside of a pipe body\nis also **invalid syntax**.\n\nTo prevent confusing grouping,\nit is **invalid** syntax to use **other** operators that have **similar precedence**\n(i.e., the arrow `=\u003e`, the ternary conditional operator `?` `:`,\nthe assignment operators, and the `yield` operator)\nas a **pipe head or body**.\nWhen using `|\u003e` with these operators, we must use **parentheses**\nto explicitly indicate what grouping is correct.\nFor example, `a |\u003e b ? % : c |\u003e %.d` is invalid syntax;\nit should be corrected to either `a |\u003e (b ? % : c) |\u003e %.d`\nor `a |\u003e (b ? % : c |\u003e %.d)`.\n\nLastly, topic bindings **inside dynamically compiled** code\n(e.g., with `eval` or `new Function`)\n**cannot** be used **outside** of that code.\nFor example, `v |\u003e eval('% + 1')` will throw a syntax error\nwhen the `eval` expression is evaluated at runtime.\n\nThere are **no other special rules**.\n\nA natural result of these rules is that,\nif we need to interpose a **side effect**\nin the middle of a chain of pipe expressions,\nwithout modifying the data being piped through,\nthen we could use a **comma expression**,\nsuch as with `value |\u003e (sideEffect(), %)`.\nAs usual, the comma expression will evaluate to its righthand side `%`,\nessentially passing through the topic value without modifying it.\nThis is especially useful for quick debugging: `value |\u003e (console.log(%), %)`.\n\n## Real-world examples\nThe only changes to the original examples were dedentation and removal of comments.\n\nFrom [jquery/build/tasks/sourceMap.js][]:\n```js\n// Status quo\nvar minLoc = Object.keys( grunt.config( \"uglify.all.files\" ) )[ 0 ];\n\n// With pipes\nvar minLoc = grunt.config('uglify.all.files') |\u003e Object.keys(%)[0];\n```\n\nFrom [node/deps/npm/lib/unpublish.js][]:\n```js\n// Status quo\nconst json = await npmFetch.json(npa(pkgs[0]).escapedName, opts);\n\n// With pipes\nconst json = pkgs[0] |\u003e npa(%).escapedName |\u003e await npmFetch.json(%, opts);\n```\n\nFrom [underscore.js][]:\n```js\n// Status quo\nreturn filter(obj, negate(cb(predicate)), context);\n\n// With pipes\nreturn cb(predicate) |\u003e _.negate(%) |\u003e _.filter(obj, %, context);\n```\n\nFrom [ramda.js][].\n```js\n// Status quo\nreturn xf['@@transducer/result'](obj[methodName](bind(xf['@@transducer/step'], xf), acc));\n\n// With pipes\nreturn xf\n  |\u003e bind(%['@@transducer/step'], %)\n  |\u003e obj[methodName](%, acc)\n  |\u003e xf['@@transducer/result'](%);\n```\n\nFrom [ramda.js][].\n```js\n// Status quo\ntry {\n  return tryer.apply(this, arguments);\n} catch (e) {\n  return catcher.apply(this, _concat([e], arguments));\n}\n\n// With pipes: Note the visual parallelism between the two clauses.\ntry {\n  return arguments\n    |\u003e tryer.apply(this, %);\n} catch (e) {\n  return arguments\n    |\u003e _concat([e], %)\n    |\u003e catcher.apply(this, %);\n}\n```\n\nFrom [express/lib/response.js][].\n```js\n// Status quo\nreturn this.set('Link', link + Object.keys(links).map(function(rel){\n  return '\u003c' + links[rel] + '\u003e; rel=\"' + rel + '\"';\n}).join(', '));\n\n// With pipes\nreturn links\n  |\u003e Object.keys(%).map(function (rel) {\n    return '\u003c' + links[rel] + '\u003e; rel=\"' + rel + '\"';\n  })\n  |\u003e link + %.join(', ')\n  |\u003e this.set('Link', %);\n```\n\nFrom [react/scripts/jest/jest-cli.js][].\n```js\n// Status quo\nconsole.log(\n  chalk.dim(\n    `$ ${Object.keys(envars)\n      .map(envar =\u003e `${envar}=${envars[envar]}`)\n      .join(' ')}`,\n    'node',\n    args.join(' ')\n  )\n);\n\n// With pipes\nObject.keys(envars)\n  .map(envar =\u003e `${envar}=${envars[envar]}`)\n  .join(' ')\n  |\u003e `$ ${%}`\n  |\u003e chalk.dim(%, 'node', args.join(' '))\n  |\u003e console.log(%);\n```\n\nFrom [ramda.js][].\n```js\n// Status quo\nreturn _reduce(xf(typeof fn === 'function' ? _xwrap(fn) : fn), acc, list);\n\n// With pipes\nreturn fn\n  |\u003e (typeof % === 'function' ? _xwrap(%) : %)\n  |\u003e xf(%)\n  |\u003e _reduce(%, acc, list);\n```\n\nFrom [jquery/src/core/init.js][].\n```js\n// Status quo\njQuery.merge( this, jQuery.parseHTML(\n  match[ 1 ],\n  context \u0026\u0026 context.nodeType ? context.ownerDocument || context : document,\n  true\n) );\n\n// With pipes\ncontext\n  |\u003e (% \u0026\u0026 %.nodeType ? %.ownerDocument || % : document)\n  |\u003e jQuery.parseHTML(match[1], %, true)\n  |\u003e jQuery.merge(%);\n```\n\n[ramda.js]: https://github.com/ramda/ramda/blob/v0.27.1/dist/ramda.js\n[node/deps/npm/lib/unpublish.js]: https://github.com/nodejs/node/blob/v16.x/deps/npm/lib/unpublish.js\n[node/deps/v8/test/mjsunit/regress/regress-crbug-158185.js]: https://github.com/nodejs/node/blob/v16.x/deps/v8/test/mjsunit/regress/regress-crbug-158185.js\n[express/lib/response.js]: https://github.com/expressjs/express/blob/5.0/lib/response.js\n[react/scripts/jest/jest-cli.js]: https://github.com/facebook/react/blob/17.0.2/scripts/jest/jest-cli.js\n[jquery/build/tasks/sourceMap.js]: https://github.com/jquery/jquery/blob/2.2-stable/build/tasks/sourcemap.js\n[jquery/src/core/init.js]: https://github.com/jquery/jquery/blob/2.2-stable/src/core/init.js\n[underscore.js]: https://underscorejs.org/docs/underscore-esm.html\n\n## Relationships with other proposals\n\n### `Function` helpers\nHack pipes can and would coexist with the [`Function` helpers proposal][helpers],\nincluding its `pipe` and `flow` functions.\nThese simple (and commonly downloaded) convenience functions\nmanipulate unary functions without extra syntax.\n\n[helpers]: https://github.com/js-choi/proposal-function-helpers\n\n[TC39 has rejected the F# pipe operator twice][rejected].\nGiven this reality, TC39 is considerably more likely to pass\n`pipe` and `flow` helper functions than a similar syntactic operator.\n\n[rejected]: #tc39-has-rejected-f-pipes-multiple-times\n\nStandardized `pipe` and `flow` convenience functions\nmay also obviate some of the need for a F#-pipe infix operator.\n(They would not preclude standardizing an equivalent operator later.\nFor example, TC39 standardized binary `**` even when `Math.pow` existed.)\n\n### Partial-function-application syntax\nHack pipes can coexist with a syntax for **partial function application** (PFA).\nThere are two approaches with which they may coexist.\n\nThe **first approach** is with an **eagerly** evaluated PFA syntax,\nwhich has [already been proposed in proposal-partial-application][PFA syntax].\nThis eager PFA syntax would add an `…~(…)` operator.\nThe operator’s right-hand side would be a list of arguments,\neach of which is an ordinary expression or a `?` placeholder.\nEach consecutive `?` placeholder would represent another parameter.\n\nOrdinary expressions would be evaluated **before** the function is created.\nFor example, `f~(g(), ?, h(), ?)` would evaluate `f`, then `g()`, then `h()`,\nand *then* it would create a partially applied version of `f` with two arguments.\n\nAn optional number after `?` placeholder\nwould override the parameter’s position.\nFor example, `f~(?1, ?0)` would have two parameters but would switch them when calling `f`.\n\nThe **second approach** is with a **lazily** evaluated syntax.\nThis could be handled with an **extension to Hack pipes**,\nwith a syntax further inspired by\n[Clojure’s `#(%1 %2)` function literals][Clojure function literals].\nIt would do so by **combining** the Hack pipe `|\u003e`\nwith the **arrow function** `=\u003e`\ninto a **pipe-function** operator `+\u003e`,\nwhich would use the same general rules as `|\u003e`.\n\n`+\u003e` would be a **prefix operator** that **creates a new function**,\nwhich in turn **binds its argument(s)** to topic references.\n**Non-unary functions** would be created\nby including topic references with **numbers** (`%0`, `%1`, `%2`, etc.) or `...`.\n`%0` (equivalent to plain `%`) would be bound to the **zeroth argument**,\n`%1` would be bound to the next argument, and so on.\n`%...` would be bound to an array of **rest arguments**.\nAnd just as with `|\u003e`, `+\u003e` would require its body\nto contain at least one topic reference\nin order to be syntactically valid.\n\n| Eager PFA                  | Pipe functions             |\n| ---------------------------| -------------------------- |\n|`a.map(f~(?, 0))`           |`a.map(+\u003e f(%, 0))`         |\n|`a.map(f~(?, ?, 0))`        |`a.map(+\u003e f(%0, %1, 0))`    |\n|`a.map(x=\u003e x + 1)`          |`a.map(+\u003e % + 1)`           |\n|`a.map(x=\u003e x + x)`          |`a.map(+\u003e % + %)`           |\n|`a.map(x=\u003e f(x, x))`        |`a.map(+\u003e f(%, %))`         |\n\nIn contrast to the [eagerly evaluated PFA syntax][PFA syntax],\ntopic functions would **lazily** evaluate its arguments,\njust like how an arrow function would.\n\nFor example, `+\u003e f(g(), %0, h(), %1)` would evaluate `f`,\nand then it would create an arrow function that closes over `g` and `h`.\nThe created function would **not** evaluate `g()` or `h()`\nuntil the every time the created function is called.\n\nNo matter the approach taken, Hack pipes could coexist with PFA.\n\n### Eventual sending / pipelining\nDespite sharing the word “pipe” in their name,\nthe pipe operator and the [eventual-send proposal][]’s remote-object pipelines\nare orthogonal and independent.\nThey can coexist and even work together.\n\n```js\nconst fileP = E(\n  E(target).openDirectory(dirName)\n).openFile(fileName);\n\nconst fileP = target\n|\u003e E(%).openDirectory(dirName)\n|\u003e E(%).openFile(fileName);\n```\n\n[eventual-send proposal]: https://github.com/tc39/proposal-eventual-send/\n\n## Possible future extensions\n\n### Hack-pipe syntax for `if`, `catch`, and `for`–`of`\nMany **`if`, `catch`, and `for` statements** could become pithier\nif they gained **“pipe syntax”** that bound the topic reference.\n\n`if () |\u003e` would bind its condition value to `%`,\\\n`catch |\u003e` would bind its caught error to `%`,\\\nand `for (of) |\u003e` would consecutively bind each of its iterator’s values to `%`.\n\n| Status quo                  | Hack-pipe statement syntax |\n| --------------------------- | -------------------------- |\n|`const c = f(); if (c) g(c);`|`if (f()) \\|\u003e g(%);`        |\n|`catch (e) f(e);`            |`catch \\|\u003e f(%);`           |\n|`for (const v of f()) g(v);` |`for (f()) \\|\u003e g(%);`       |\n\n### Optional Hack pipes\nA **short-circuiting** optional-pipe operator `|?\u003e` could also be useful,\nmuch in the way `?.` is useful for optional method calls.\n\nFor example, `value |\u003e (% == null ? % : await foo(%) |\u003e (% == null ? % : % + 1))`\\\nwould be equivalent to `value |?\u003e await foo(%) |?\u003e % + 1`.\n\n### Tacit unary function application syntax\n**Syntax** for **tacit unary function application** – that is, the F# pipe operator –\nhas been [rejected twice by TC39][rejected].\nHowever, they could still eventually be added to the language in two ways.\n\nFirst, it can be added as a convenience function `Function.pipe`.\nThis is what the [function-helpers proposal][helpers] proposes.\n`Function.pipe` may obviate much of the need for an F#-pipe operator,\nwhile still not closing off the possibility of an F#-pipe operator.\n\nSecondly, it can be added as **another pipe operator** `|\u003e\u003e` –\nsimilarly to how [Clojure has multiple pipe macros][Clojure pipes]\n`-\u003e`, `-\u003e\u003e`, and `as-\u003e`.\\\nFor example, `value |\u003e % + 1 |\u003e\u003e f |\u003e g(%, 0)`\\\nwould mean `value |\u003e % + 1 |\u003e f(%) |\u003e g(%, 0)`.\n\n[Clojure pipes]: https://clojure.org/guides/threading_macros\n\nThere was an [informal proposal for such a **split mix** of two pipe operators][split mix],\nwhich was set aside in favor of single-operator proposals.\nThis split mix might return as a proposal after Hack pipes.\n\n[split mix]: https://github.com/tc39/proposal-pipeline-operator/wiki#proposal-3-split-mix\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftc39%2Fproposal-pipeline-operator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftc39%2Fproposal-pipeline-operator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftc39%2Fproposal-pipeline-operator/lists"}