- Notifications
You must be signed in to change notification settings - Fork109
A proposal for adding a useful pipe operator to JavaScript.
License
tc39/proposal-pipeline-operator
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
- Stage: 2
- Champions: J. S. Choi, James DiGioia, Ron Buckton, Tab Atkins-Bittner, [list incomplete]
- Former champions: Daniel Ehrenberg
- Specification
- Contributing guidelines
- Proposal history
- Babel plugin:Implemented in v7.15. SeeBabel documentation.
(This document uses%
as the placeholder token for the topic reference.This willalmost certainly not be the final choice;seethe token bikeshedding discussion for details.)
In the State of JS 2020 survey, thefourth top answer to“What do you feel is currently missing fromJavaScript?”was apipe operator. Why?
When we performconsecutive operations (e.g., function calls)on avalue in JavaScript,there are currently two fundamental styles:
- passing the value as an argument to the operation(nesting the operations if there are multiple operations),
- or calling the function as a method on the value(chaining more method calls if there are multiple methods).
That is,three(two(one(value)))
versusvalue.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
andyield
, etc.
However, nesting isdifficult to read when it becomes deep:the flow of execution movesright to left,rather than the left-to-right reading of normal code.If there aremultiple arguments at some levels,reading even bouncesback and forth:our eyes mustjump left to find a function name,and then they mustjump right to find additional arguments.Additionally,editing the code afterwards can be fraught:we must find the correctplace to insert new argumentsamongmany nested parentheses.
Real-world example
Consider thisreal-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 ofdeeply nested expressions.In order to read its flow of data, a human’s eyes must first:
Find theinitial data (the innermost expression,
envars
).And then scanback and forth repeatedly frominside outfor each data transformation,each one either an easily missed prefix operator on the leftor a suffix operators on the right:
Object.keys()
(left side),.map()
(right side),.join()
(right side),- A template literal (both sides),
chalk.dim()
(left side), thenconsole.log()
(left side).
As a result of deeply nesting many expressions(some of which useprefix operators,some of which usepostfix operators,and some of which usecircumfix operators),we must checkboth left and right sidesto find thehead ofeach expression.
The second style,method chaining, isonly usableif the value has the functions designated asmethods for its class.Thislimits its applicability.Butwhen it applies, thanks to its postfix structure,it is generally more usable andeasier to read and write.Code execution flowsleft to right.Deeply nested expressions areuntangled.All arguments for a function call aregrouped with the function’s name.And editing the code later toinsert or delete more method calls is trivial,since we would just have to put our cursor in one spot,then start typing or deleting onecontiguous run of characters.
Indeed, the benefits of method chaining areso attractivethat somepopular libraries contort their code structurespecifically to allowmore method chaining.The most prominent example isjQuery, whichstill remains themost 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 cancontinue 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’sother syntaxes:function calls, arithmetic, array/object literals,await
andyield
, etc.In this way, method chaining remainslimited in itsapplicability.
The pipe operator attempts to marry theconvenience and ease ofmethod chainingwith the wideapplicability ofexpression nesting.
The general structure of all the pipe operators isvalue |>
e1|>
e2|>
e3,wheree1,e2,e3are 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.
Real-world example, continued
Continuing this deeply nestedreal-world code from React:
console.log(chalk.dim(`$${Object.keys(envars).map(envar=>`${envar}=${envars[envar]}`).join(' ')}`,'node',args.join(' ')));
…we canuntangle it as such using a pipe operatorand 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 canrapidly find theinitial data(what had been the most innermost expression,envars
),thenlinearly read, fromleft to right,each transformation on the data.
One could argue that usingtemporary variablesshould be the only way to untangle deeply nested code.Explicitly naming every step’s variablecauses something similar to method chaining to happen,with similar benefits to reading and writing code.
Real-world example, continued
For example, using our previous modifiedreal-world example from React:
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:
constenvarString=Object.keys(envars).map(envar=>`${envar}=${envars[envar]}`).join(' ');constconsoleText=`$${envarString}`;constcoloredConsoleText=chalk.dim(consoleText,'node',args.join(' '));console.log(coloredConsoleText);
But there are reasons why we encounter deeply nested expressionsin each other’s codeall the time in the real world,rather than lines of temporary variables.And there are reasons why themethod-chain-basedfluent interfacesof jQuery, Mocha, and so on are stillpopular.
It is often simply tootedious and wordy towritecode with a long sequence of temporary, single-use variables.It is arguably even tedious and visually noisy for a human toread, too.
Ifnaming is one of themost difficult tasks in programming,then programmers willinevitably avoid naming variableswhen they perceive their benefit to be relatively small.
One could argue that using a singlemutable variable with a short namewould reduce the wordiness of temporary variables, achievingsimilar results as with the pipe operator.
Real-world example, continued
For example, our previous modifiedreal-world example from Reactcould be re-written like this:
let_;_=Object.keys(envars).map(envar=>`${envar}=${envars[envar]}`).join(' ');_=`$${_}`;_=chalk.dim(_,'node',args.join(' '));_=console.log(_);
But code like this isnot common in real-world code.One reason for this is that mutable variables canchange 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.
Example code
// setupfunctionone(){return1;}functiondouble(x){returnx*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, andcode outside of each step cannot change its binding.
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 tosearch the entire preceding scope for places where it isreassigned.
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 areexpressions.
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.
Examples
Pipelines | Temporary Variables |
---|---|
constenvVarFormat=vars=>Object.keys(vars).map(var=>`${var}=${vars[var]}`).join(' ')|>chalk.dim(%,'node',args.join(' ')); | constenvVarFormat=(vars)=>{let_=Object.keys(vars);_=_.map(var=>`${var}=${vars[var]}`);_=_.join(' ');returnchalk.dim(_,'node',args.join(' '));} |
// This example uses JSX.return(<ul>{values|>Object.keys(%)|>[...Array.from(newSet(%))]|>%.map(envar=>(<lionClick={()=>doStuff(values)}>{envar}</li>))}</ul>); | // This example uses JSX.let_=values;_=Object.keys(_);_=[...Array.from(newSet(_))];_=_.map(envar=>(<lionClick={()=>doStuff(values)}>{envar}</li>));return(<ul>{_}</ul>); |
There weretwo competing proposals for the pipe operator: Hack pipes and F# pipes.(Before that, therewas athird 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 differslightly on what the “magic” is,when we spell our code when using|>
.
Both proposalsreuse existing language concepts:Hack pipes are based on the concept of theexpression,while F# pipes are based on the concept of theunary function.
Pipingexpressions and pipingunary functionscorrespondingly havesmall and nearlysymmetrical trade-offs.
In theHack language’s pipe syntax,the righthand side of the pipe is anexpression containing a specialplaceholder,which is evaluated with the placeholder bound to the result of evaluating the lefthand side's expression.That is, we writevalue |> one(%) |> two(%) |> three(%)
to pipevalue
through the three functions.
Pro: The righthand side can beany expression,and the placeholder can go anywhere any normal variable identifier could go,so we can pipe to any code we wantwithout 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,- etc.
Con: Piping throughunary functionsisslightly more verbose with Hack pipes than with F# pipes.This includes unary functionsthat were created byfunction-currying libraries likeRamda,as well asunary arrow functionsthat performcomplex destructuring on their arguments:Hack pipes would be slightly more verbosewith anexplicit function call suffix(%)
.
(Complex destructuring of the topic valuewill be easier whendo expressions progress,as you will then be able to do variable assignment/destructuringinside of a pipe body.)
In theF# language’s pipe syntax,the righthand side of the pipe is an expressionthat mustevaluate into a unary function,which is thentacitly calledwith the lefthand side’s value as itssole argument.That is, we writevalue |> one |> two |> three
to pipevalue
through the three functions.left |> right
becomesright(left)
.This is calledtacit programming or point-free style.
Real-world example, continued
For example, using our previous modifiedreal-world example from React:
Object.keys(envars).map(envar=>`${envar}=${envars[envar]}`).join(' ')|> `$ ${%}`|>chalk.dim(%,'node',args.join(' '))|>console.log(%);
…a version using F# pipes instead of Hack pipes would look like this:
Object.keys(envars).map(envar=>`${envar}=${envars[envar]}`).join(' ')|>x=>`$${x}`|>x=>chalk.dim(x,'node',args.join(' '))|>console.log;
Pro: The restriction that the righthand sidemust resolve to a unary functionlets us write very terse pipeswhen the operation we want to performis aunary function call:
value |> foo
for unary function calls.
This includes unary functionsthat were created byfunction-currying libraries likeRamda,as well asunary arrow functionsthat performcomplex destructuring on their arguments:F# pipes would beslightly less verbosewith animplicit function call (no(%)
).
Con: The restriction means thatany operationsthat are performed byother syntaxmust be madeslightly more verbose bywrapping the operationin a unaryarrow function:
value |> x=> x.foo()
for method calls,value |> x=> x + 1
for arithmetic,value |> x=> [x, 0]
for array literals,value |> x=> ({foo: x})
for object literals,value |> x=> `${x}`
for template literals,value |> x=> new Foo(x)
for constructing objects,value |> x=> import(x)
for calling function-like keywords,- etc.
Even callingnamed functions requireswrappingwhen we need to passmore than one argument:
value |> x=> foo(1, x)
for n-ary function calls.
Con: Theawait
andyield
operations arescopedto theircontaining function,and thuscannot be handled by unary functions alone.If we want to integrate them into a pipe expression,await
andyield
must be handled asspecial syntax cases:
value |> await
for awaiting promises, andvalue |> yield
for yielding generator values.
Both Hack pipes and F# pipes respectively imposea smallsyntax tax on different expressions:
Hack pipes slightly tax onlyunary function calls, and
F# pipes slightly taxall expressions except unary function calls.
Inboth proposals, the syntax tax per taxed expression issmall(both(%)
andx=>
areonly three characters).However, the tax ismultiplied by theprevalenceof its respectively taxed expressions.It therefore might make senseto impose a tax on whichever expressions areless commonand tooptimize in favor of whichever expressions aremore common.
Unary function calls are in generalless commonthanall expressionsexcept unary functions.In particular,method calling andn-ary function callingwillalways bepopular;in general frequency,unary function calling is equal to or exceeded bythose two casesalone –let alone by other ubiquitous syntaxessuch asarray literals,object literals,andarithmetic operations.This explainer contains severalreal-world examplesof this difference in prevalence.
Furthermore, several other proposednew syntaxes,such asextension calling,do expressions,andrecord/tuple literals,will also likely becomepervasive in thefuture.Likewise,arithmetic operations would also becomeeven more commonif TC39 standardizesoperator overloading.Untangling these future syntaxes’ expressions would be more fluentwith Hack pipes compared to F# pipes.
The syntax tax of Hack pipes on unary function calls(i.e., the(%)
to invoke the righthand side’s unary function)isnot a special case:it simply isexplicitly writing ordinary code,inthe way we normally would without a pipe.
On the other hand,F# pipes require us todistinguishbetween “code that resolves to an unary function”versus“any other expression” –and to remember to add the arrow-function wrapper around the latter case.
For example, with Hack pipes,value |> someFunction + 1
isinvalid syntax and willfail early.There is no need to recognize thatsomeFunction + 1
will not evaluate into a unary function.But with F# pipes,value |> someFunction + 1
isstill valid syntax –it’ll justfail late atruntime,becausesomeFunction + 1
isn’t callable.
The pipe champion group has presented F# pipes for Stage 2 to TC39twice.It wasunsuccessful in advancing to Stage 2 both times.Both F# pipes (andpartial function application (PFA))have run into strong pushback from multiple other TC39 representativesdue to various concerns. These have included:
- Memory performance concerns (e.g.,especially from browser-engine implementors),
- Syntax concerns about
await
. - Concerns about encouraging ecosystem bifurcation/forking, etc.
This pushback has occurred fromoutside the pipe champion group.SeeHISTORY.md for more information.
It is the pipe champion group’s belief that any pipe operator is better than none,in order toeasily linearize deeply nested expressionswithout resorting to named variables.Many members of the champion group believe that Hack pipes are slightly better than F# pipes,and some members of the champion group believe that F# pipes are slightly better than Hack pipes.But everyone in the champion group agrees that F# pipes have met with far too much resistanceto be able to pass TC39 in the foreseeable future.
To emphasize, it is likely that an attempt to switch from Hack pipes back to F# pipeswill result in TC39 never agreeing to any pipes at all.PFA syntax is similarly facing an uphill battle in TC39 (seeHISTORY.md).Many members of the pipe champion group think this is unfortunate,and they are willing to fight againlater for anF#-pipe split mix andPFA syntax.But there are quite a few representatives (includingbrowser-engine implementers)outside of the Pipe Champion Groupwho are generally against encouragingtacit programming (andPFA syntax),regardless of Hack pipes.
(Aformal draft specification is available.)
Thetopic reference%
is anullary operator.It acts as a placeholder for atopic value,and it islexically scoped andimmutable.
%
is not a final choice
(The precisetoken for the topic reference isnot final.%
could instead be^
, or many other tokens.We plan tobikeshed what actual token to usebefore advancing to Stage 3.However,%
seems to be theleast syntactically problematic,and it also resembles the placeholders ofprintf format stringsandClojure’s#(%)
function literals.)
Thepipe operator|>
is aninfix operatorthat forms apipe expression (also called apipeline).It evaluates its lefthand side (thepipe head orpipe input),immutablybinds the resulting value (thetopic value) to thetopic reference,then evaluates its righthand side (thepipe body) with that binding.The resulting value of the righthand sidebecomes the whole pipe expression’s final value (thepipe output).
The pipe operator’s precedence is thesame as:
- the function arrow
=>
; - the assignment operators
=
,+=
, etc.; - the generator operators
yield
andyield *
;
It istighter than only the comma operator,
.
It islooser thanall other operators.
For example,v => v |> % == null |> foo(%, 0)
would group intov => (v |> (% == null) |> foo(%, 0))
,
which in turn is equivalent tov => foo(v == null, 0)
.
A pipe bodymust use its topic valueat least once.For example,value |> foo + 1
isinvalid syntax,because its body does not contain a topic reference.This design is becauseomission of the topic referencefrom a pipe expression’s bodyis almost certainly anaccidental programmer error.
Likewise, a topic referencemust be contained in a pipe body.Using a topic reference outside of a pipe bodyis alsoinvalid syntax.
To prevent confusing grouping,it isinvalid syntax to useother operators that havesimilar precedence(i.e., the arrow=>
, the ternary conditional operator?
:
,the assignment operators, and theyield
operator)as apipe head or body.When using|>
with these operators, we must useparenthesesto explicitly indicate what grouping is correct.For example,a |> b ? % : c |> %.d
is invalid syntax;it should be corrected to eithera |> (b ? % : c) |> %.d
ora |> (b ? % : c |> %.d)
.
Lastly, topic bindingsinside dynamically compiled code(e.g., witheval
ornew Function
)cannot be usedoutside of that code.For example,v |> eval('% + 1')
will throw a syntax errorwhen theeval
expression is evaluated at runtime.
There areno other special rules.
A natural result of these rules is that,if we need to interpose aside effectin the middle of a chain of pipe expressions,without modifying the data being piped through,then we could use acomma expression,such as withvalue |> (sideEffect(), %)
.As usual, the comma expression will evaluate to its righthand side%
,essentially passing through the topic value without modifying it.This is especially useful for quick debugging:value |> (console.log(%), %)
.
The only changes to the original examples were dedentation and removal of comments.
Fromjquery/build/tasks/sourceMap.js:
// Status quovarminLoc=Object.keys(grunt.config("uglify.all.files"))[0];// With pipesvarminLoc=grunt.config('uglify.all.files')|>Object.keys(%)[0];
Fromnode/deps/npm/lib/unpublish.js:
// Status quoconstjson=awaitnpmFetch.json(npa(pkgs[0]).escapedName,opts);// With pipesconstjson=pkgs[0]|>npa(%).escapedName|>awaitnpmFetch.json(%,opts);
Fromunderscore.js:
// Status quoreturnfilter(obj,negate(cb(predicate)),context);// With pipesreturncb(predicate)|>_.negate(%)|>_.filter(obj,%,context);
Fromramda.js.
// Status quoreturnxf['@@transducer/result'](obj[methodName](bind(xf['@@transducer/step'],xf),acc));// With pipesreturnxf|>bind(%['@@transducer/step'],%)|>obj[methodName](%,acc)|>xf['@@transducer/result'](%);
Fromramda.js.
// Status quotry{returntryer.apply(this,arguments);}catch(e){returncatcher.apply(this,_concat([e],arguments));}// With pipes: Note the visual parallelism between the two clauses.try{returnarguments|>tryer.apply(this,%);}catch(e){returnarguments|>_concat([e],%)|>catcher.apply(this,%);}
// Status quoreturnthis.set('Link',link+Object.keys(links).map(function(rel){return'<'+links[rel]+'>; rel="'+rel+'"';}).join(', '));// With pipesreturnlinks|>Object.keys(%).map(function(rel){return'<'+links[rel]+'>; rel="'+rel+'"';})|>link+%.join(', ')|>this.set('Link',%);
Fromreact/scripts/jest/jest-cli.js.
// Status quoconsole.log(chalk.dim(`$${Object.keys(envars).map(envar=>`${envar}=${envars[envar]}`).join(' ')}`,'node',args.join(' ')));// With pipesObject.keys(envars).map(envar=>`${envar}=${envars[envar]}`).join(' ')|> `$ ${%}`|>chalk.dim(%,'node',args.join(' '))|>console.log(%);
Fromramda.js.
// Status quoreturn_reduce(xf(typeoffn==='function' ?_xwrap(fn) :fn),acc,list);// With pipesreturnfn|>(typeof%==='function' ?_xwrap(%) :%)|>xf(%)|>_reduce(%,acc,list);
// Status quojQuery.merge(this,jQuery.parseHTML(match[1],context&&context.nodeType ?context.ownerDocument||context :document,true));// With pipescontext|>(%&&%.nodeType ?%.ownerDocument||% :document)|>jQuery.parseHTML(match[1],%,true)|>jQuery.merge(%);
Hack pipes can and would coexist with theFunction
helpers proposal,including itspipe
andflow
functions.These simple (and commonly downloaded) convenience functionsmanipulate unary functions without extra syntax.
TC39 has rejected the F# pipe operator twice.Given this reality, TC39 is considerably more likely to passpipe
andflow
helper functions than a similar syntactic operator.
Standardizedpipe
andflow
convenience functionsmay also obviate some of the need for a F#-pipe infix operator.(They would not preclude standardizing an equivalent operator later.For example, TC39 standardized binary**
even whenMath.pow
existed.)
Hack pipes can coexist with a syntax forpartial function application (PFA).There are two approaches with which they may coexist.
Thefirst approach is with aneagerly evaluated PFA syntax,which hasalready been proposed in proposal-partial-application.This eager PFA syntax would add an…~(…)
operator.The operator’s right-hand side would be a list of arguments,each of which is an ordinary expression or a?
placeholder.Each consecutive?
placeholder would represent another parameter.
Ordinary expressions would be evaluatedbefore the function is created.For example,f~(g(), ?, h(), ?)
would evaluatef
, theng()
, thenh()
,andthen it would create a partially applied version off
with two arguments.
An optional number after?
placeholderwould override the parameter’s position.For example,f~(?1, ?0)
would have two parameters but would switch them when callingf
.
Thesecond approach is with alazily evaluated syntax.This could be handled with anextension to Hack pipes,with a syntax further inspired byClojure’s#(%1 %2)
function literals.It would do so bycombining the Hack pipe|>
with thearrow function=>
into apipe-function operator+>
,which would use the same general rules as|>
.
+>
would be aprefix operator thatcreates a new function,which in turnbinds its argument(s) to topic references.Non-unary functions would be createdby including topic references withnumbers (%0
,%1
,%2
, etc.) or...
.%0
(equivalent to plain%
) would be bound to thezeroth argument,%1
would be bound to the next argument, and so on.%...
would be bound to an array ofrest arguments.And just as with|>
,+>
would require its bodyto contain at least one topic referencein order to be syntactically valid.
Eager PFA | Pipe functions |
---|---|
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(%, %)) |
In contrast to theeagerly evaluated PFA syntax,topic functions wouldlazily evaluate its arguments,just like how an arrow function would.
For example,+> f(g(), %0, h(), %1)
would evaluatef
,and then it would create an arrow function that closes overg
andh
.The created function wouldnot evaluateg()
orh()
until the every time the created function is called.
No matter the approach taken, Hack pipes could coexist with PFA.
Despite sharing the word “pipe” in their name,the pipe operator and theeventual-send proposal’s remote-object pipelinesare orthogonal and independent.They can coexist and even work together.
constfileP=E(E(target).openDirectory(dirName)).openFile(fileName);constfileP=target|>E(%).openDirectory(dirName)|>E(%).openFile(fileName);
Manyif
,catch
, andfor
statements could become pithierif they gained“pipe syntax” that bound the topic reference.
if () |>
would bind its condition value to%
,catch |>
would bind its caught error to%
,
andfor (of) |>
would consecutively bind each of its iterator’s values to%
.
Status quo | Hack-pipe statement syntax |
---|---|
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(%); |
Ashort-circuiting optional-pipe operator|?>
could also be useful,much in the way?.
is useful for optional method calls.
For example,value |> (% == null ? % : await foo(%) |> (% == null ? % : % + 1))
would be equivalent tovalue |?> await foo(%) |?> % + 1
.
Syntax fortacit unary function application – that is, the F# pipe operator –has beenrejected twice by TC39.However, they could still eventually be added to the language in two ways.
First, it can be added as a convenience functionFunction.pipe
.This is what thefunction-helpers proposal proposes.Function.pipe
may obviate much of the need for an F#-pipe operator,while still not closing off the possibility of an F#-pipe operator.
Secondly, it can be added asanother pipe operator|>>
–similarly to howClojure has multiple pipe macros->
,->>
, andas->
.
For example,value |> % + 1 |>> f |> g(%, 0)
would meanvalue |> % + 1 |> f(%) |> g(%, 0)
.
There was aninformal proposal for such asplit mix of two pipe operators,which was set aside in favor of single-operator proposals.This split mix might return as a proposal after Hack pipes.
About
A proposal for adding a useful pipe operator to JavaScript.
Topics
Resources
License
Code of conduct
Contributing
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.