tc39 / proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
http://tc39.github.io/proposal-pipeline-operator/
BSD 3-Clause "New" or "Revised" License
7.55k stars 108 forks source link

Allow pointfree pipelines #96

Closed masaeedu closed 6 years ago

masaeedu commented 6 years ago

I have the following definition:

const capitalizeKeys = o =>
  o |> pairs |> map(([k, v]) => [toUpper(k), v]) |> fromPairs;

Which is basically equivalent to o => fromPairs(map(...)(pairs(o))). The o => o |> ... abstraction seems kind of unnecessary, since it is in a position where it can basically be omitted.


With something like the pipe primitive from an FP library I can do:

const capitalizeKeys = pipe([pairs, map(...), fromPairs])

or with a hypothetical . composition operator I would be able to do:

const capitalizeKeys = fromPairs . map(...) . pairs

Neither of these require me to explicitly introduce a new function with an o parameter.


It would be nice if the |> syntax allowed me to do something like:

const capitalizeKeys = |> pairs |> map(...) |> fromPairs

// or maybe some kind of interaction with https://github.com/tc39/proposal-partial-application
const capitalizeKeys = ? |> pairs |> map(...) |> fromPairs

I'm not hung up on the syntax, I just want to be able to compose sequences of functions without having to introduce wrapping functions where not necessary.

ljharb commented 6 years ago

Starting a pipeline with |> seems relatively elegant, but I’d be concerned about grammar conflicts.

js-choi commented 6 years ago

Background: Upon a request from this proposal’s champion, @mAAdhaTTah and I have been drafting two formal specifications of Proposal 1: F-sharp Style Only and Proposal 4: Smart Mix respectively.

For what it’s worth, my smart-pipe draft includes an appendix of ideas for extending its pipeline placeholder into a general “topic reference” concept (like the topic variables of other languages). This could enable new tacit versions of many syntaxes, which would tacitly use those topic references.

For instance, if there was a arrow function -> that could be headless and that would bind its first argument to the topic reference, then your example would be const capitalizeKeys = -> pairs |> map(…) |> fromPairs.

But all this is very preliminary. I’m currently focusing on formally specifying the smart pipe operator first while making sure it would be forward compatible with such future tacit topicalized syntaxes. It’s very unfinished, though; I hope to be done with the first draft by the end of this week.

mAAdhaTTah commented 6 years ago

I kind of like this, I'm pretty sure this works with both proposals, and it would eliminate the need for a separate composition operator.

js-choi commented 6 years ago

@mAAdhaTTah: I like the idea a lot too. It could explain not only composition but also (for Proposal 4: Smart Mix) partial application to unary functions. I had opened #97 to inquire whether including such functionality in the new proposal (at least for Proposal 4) would be a good idea, given that TC39 has blocked the previous pipe proposal because it did not address partial application.

In fact, for Proposal 4, a unary headless-pipe-function operator would solve not only functional composition but also method extraction and partial application (at least into unary functions). Proposal 1 would also benefit, though just for composition and extraction and not for partial application.

I might have made a mistake in framing the question #97 as an alternative to using => arrow functions; I used the symbol -> for the unary headless pipe function. The response from @ljharb and @littledan was concern over confusion with =>, just like in the past when arrow functions were being created. So for now I’m leaving out unary headless pipe functions and focusing on just the pipe operator, to see how far we get with just pipes and arrow functions.

I like -> for the unary headless pipe function operator, but the particular symbol could be something else, like =|>: it’s just bikeshedding to me. But what I would not want is an overloading of |>’s arity. We are creating a function of a pipeline here, not immediately evaluating a pipeline. I don’t want to confuse this crucial semantic difference.

mAAdhaTTah commented 6 years ago

We are creating a function of a pipeline here, not immediately evaluating a pipeline. I don’t want to confuse this crucial semantic difference.

This difference might make OP's suggestion untenable.

js-choi commented 6 years ago

@mAAdhaTTah I actually think it’s tenable…if another operator is added, rather than overloading |> by arity. Another operator (like -> or =|> or whatever) for headless/tacit pipe functions could well be worth it, because with |> it could handle both terse composition and terse method extraction (for Proposals 1,3,4), as well as terse partial application (for Proposals 3,4), unified with the same single concept. Basically, with one additional terse pipe-function operator, you wouldn’t have to add any of the operators from the other proposals! (Except maybe binary object::function for Function.prototype.bind and .call.)

But recent TC39-member feedback suggests so far that, despite the results of the September 2017 meeting (pipeline proposal blocked to coordinate further with other proposals), it would be best for us to hold off on adding another operator to our proposals, for now. Let’s focus on |> itself and take care of function creation later. As long as there’s some plan of a coordinated approach to the different proposals, TC39’s concerns from September might be mollified, when our proposals are presented at the next meeting.

mAAdhaTTah commented 6 years ago

Agreed, and the current syntax:

const myFun = x => x |> someFunc |> someOtherFunc

is reasonable as-is for any of the current proposals / mixes.

Adding another operator should probably be a separate proposal.

ivan7237d commented 6 years ago

A really great thing about headless |> would be that it would make it very easy and intuitive to refactor say this

const y = x |> a |> b |> c |> d

to this:

const e = |> c |> d
const y = x |> a |> b |> e
masaeedu commented 6 years ago

@ivan7237d The annoying thing about the pipe operator as specified is that it breaks equational reasoning. Given const y = x |> a |> b |> c |> d, I can't just cut and paste the b |> c |> d expression to obtain const e = b |> c |> d; const y = x |> a |> e.

Because |> means different things when embedded in different expressions (namely, expressions containing itself), it's not simple to transparently rearrange pure expressions that involve it. When a |> expression occurs in the RHS of another |> expression, x |> y expands (conceptually) to compose(y, x) or a => y(x(a)). When in a leading position, x |> y instead expands to y(x).

The proposal above might give you a few more options for refactorability, but it doesn't really solve the core problem.

js-choi commented 6 years ago

I’m hoping that |> a might eventually mean something different, if Proposal 4 is selected: not a function creation, but an immediate evaluation of # |> a, as long as # is defined in the scope. Which I hope eventually will be more than pipelines, but also things like block parameters’ first arguments, for-of loops’ items, catch clauses’ errors, match clauses’ match results. But that doesn’t matter right now.

What matters right now is, whatever the case, having |> suddenly return a function instead of immediately evaluating just because of omission of the head strikes me as inconsistent – one result of which was just given by @masaeedu. In general, polymorphism by arity like this is a footgun.

More to the point, how is this any different from #97, which is the same thing, except with another operator?

masaeedu commented 6 years ago

@js-choi I don't know what the difference is, because I can't understand what #97 is proposing (I'm not very involved with the proposal process and don't know about smart/mixed/etc. pipes).

Regarding "one result of which was given by ...", the problem I'm referring to is a result of the pipe operator as-specified, i.e. as it is currently implemented in the Babel transform, without any further changes. |> already returns a function instead of a function application when chained with itself.

js-choi commented 6 years ago

@ljharb: I’m sorry – I’m confused about the semantics of (x => x) |> a here. I’m assuming the |> is from Proposals 1,4, and that a is a unary function. Why would we wish to pass an identity function to a?

ljharb commented 6 years ago

@js-choi ah, maybe i misunderstood. I'll step back until I've reread :-)

js-choi commented 6 years ago

@masaeedu: If you’re confused about all the different proposals, then check out the wiki. @gilbert wrote a pretty good summary of the four competing proposals. @mAAdhaTTah is writing a formal draft and Babel plugin of Proposal 1. I am writing a formal draft and Babel plugin of Proposal 4. I do wish the issues here were more organized by Proposal. Maybe the readme should be updated…

As I understand it, in your original post, you proposed a unary operator – let’s call it 🖤 for now, to avoid bikeshedding – that interprets its operand as if it were the RHS body of a |> operation, then creates a unary function – and this unary function would pass its sole parameter into its body as if its body were the RHS of a pipeline. That is, 🖤 expression would be equivalent to x => x |> expression. This “tacit pipe function” operator would work on all four Proposals 1,2,3,4.

Anyways, 🖤 here is exactly the same thing as an idea I’ve been developing in my draft for Proposal 4. 🖤 is also the same idea as in #97, except #97 gets into more details on the ramifications of 🖤 for solving function composition (assuming Proposals 1,3,4), method extraction (for Proposals 1,3,4), and partial application into a unary function (Proposals 2,3,4).

97 focused on Proposal 4, in that I pointed out that TC39 last September wanted more coherence between pipelines, function composition, method binding, and partial application – and 🖤 applied to Proposal 4 might address nearly all their concerns. I was asking (poorly, I know) whether I (and @mAAdhaTTah too) should integrate 🖤 into our respective proposals, before their first presentation to TC39 at the next meeting. The answer appears to be no for now, but I’m hoping eventually we will be able to revisit this in a follow-up proposal.

ivan7237d commented 6 years ago

@masaeedu Yes, I'm just looking at it from the perspective of my TypeScript codebase where in maybe two thirds of the cases I use a utility function const applyPipe = (x, a, b) = b(a(x)) and in the other third I use another utility function const pipe = (a, b) => x => b(a(x)). If the pipe operator gets introduced in just the basic form, that would be applyPipe, and if I want to switch over to it completely, I'd have to first replace pipe calls with arrow functions, which will make the code more verbose and will give a minor performance hit (I would do it anyway). And it definitely won't be a good idea to only replace applyPipe with the operator and keep using pipe, because that would mean that some of my operator chains will be separated by commas, and some by |>.

kurtmilam commented 6 years ago

@js-choi

In general, polymorphism by arity like this is a footgun.

I don't follow how having a headless pipeline operator return a function qualifies as polymorphism by arity.

If it does qualify, don't all function declarations qualify, as well?

For instance:

x => x
// vs
(x => x)(1)
// or
function (x) { return x }
// vs
(function (x) { return x })(1)
// or
Ramda.pipe(Ramda.identity)
// vs
Ramda.pipe(Ramda.identity)(1)

I don't see any polymorphism by arity or footguns there.

masaeedu commented 6 years ago

@js-choi Thanks for the info, I'll do some reading.

The gist of what I'm saying is that |> is already two different operators. One is reverse function application, i.e. x |> y => y(x). The other is reverse function composition, i.e. x |> y |> z => x |> compose(z, y). When it occurs in the RHS of a |> expression, |> behaves as composition, and otherwise as application.

This is fine from a pragmatic standpoint, but when someone's just trying to compose a bunch of functions, they need to dance around the fact that the first of several |> is special and will behave differently. The standard workaround is to prepend a x => x |> wherever you're trying to compose several functions, so you can get that first |> "out of the way". My proposal above was to sweeten this workaround a little bit.

A better solution IMO is to just have two unambiguous operators. A reverse function composition operator (reverse of . from Haskell), and a reverse function application operator (reverse of $ from Haskell). IIUC, the latter is the "F# style only" |> mentioned in the wiki.

Based on what you're saying, it seems you're focusing on making reverse function application more ergonomic. This is great, and probably the ❤️ operator from your proposal is a good way of doing that. What I want, however, is a consistent, language level function composition operator (for various reasons, not least of which is the ability of runtimes to recognize and inline composition).

kurtmilam commented 6 years ago

@masaeedu the are several links to userland composition operator proposals in the pipeline operator readme. For the record, of the various pipeline operator proposals, I prefer the F# style, and would be pleased if a headless operator returned a function.

Otherwise, I'm hoping it won't take too long to see a composition operator added to the language, and hope it won't be compromised by a desire to turn it into a jack of all trades, master of none.

js-choi commented 6 years ago

@kurtmilam: Sorry – I don’t yet understand how your examples are parallel to the idea of “a :: operation evaluates into a function vs. a function call, depending on whether it is being used as a unary or binary operator”. In your examples, (x => x), function (x) { return x }, and Ramda.pipe(Ramda.identity) always evaluate to functions. Then they are called, which always results in whatever their function bodies evaluate into. I’m not sure how this is similar a single operator performing two different operations depending on whether it’s binary or unary.

A more illustrative example I can think of would be the APL and R programming languages, which integrate polymorphic operators (by unary vs. binary use) at their cores, and which also benefit from a very uniform concatenative syntax. JavaScript does not have many operations, if any at all, that do dispatch on arity similar to APL, R, and its ilk; and JavaScript has a much more variable expression syntax, so it is important make it not easy to have composed things suddenly become something unexpected. Now, I could be wrong about the “not many operations in JavaScript that are polymorphic on arity” part – but even if that were wrong, I think it would be undesirable in JavaScript. I still see mode errors when pipelines unexpectedly result in functions rather than other values. I would prefer a separate operator for headless pipe functions. But this is premature bikeshedding, I would think.

In any case, I am quite generally enthusiastic about tacit programming myself, and I admire your work in applying it to JavaScript. Perhaps if you could elaborate your examples on how they are applicable to “:: evaluates into two different sorts of things, depending on whether it’s being used as a unary vs. binary operator”, then I would better understand your concerns.

Remember that a headless function operator’s particular spelling is just bikeshedding. Whether it’s unary :: or -> or something else doesn’t change the fact that many of us agree that it’s a useful semantic, though we will have to do our best to craft a good approach and convince TC39 of the same. In time, though.

kurtmilam commented 6 years ago

@js-choi

I must be overlooking something, because I don't see anyone suggesting that there should be any differences in behavior based on arity. I see at least a couple of participants suggesting that they'd like to see a headless pipe be equivalent to a function declaration, but that's it.

masaeedu commented 6 years ago

Upon further consideration, I withdraw my proposal. If you look at the |> operator as right associative, as I was doing, the behavior seems inconsistent:

a |> b |> c |> d
=>
a |> (b |> (c |> d))
=>
a |> (b |> compose(d, c))
=>
a |> compose(compose(d, c), b)
=>
(compose(compose(d, c), b))(a) // inconsistency here

If you look at it as left associative however, it is consistent:

a |> b |> c |> d
=>
((a |> b) |> c) |> d
=>
(b(a) |> c) |> d
=>
c(b(a)) |> d
=>
d(c(b(a)))

So the problem is that I'm misunderstanding the fixity and trying to abuse a function application operator as a function composition operator. As @kurtmilam has pointed out, there's other repos tracking proposals for function composition operators, which I'll go look at.

I'll leave this issue open since it seems there's some people in favor of it, although I personally don't think it's necessary anymore.

js-choi commented 6 years ago

@masaeedu:

The gist of what I'm saying is that |> is already two different operators. One is reverse function application, i.e. x |> y => y(x). The other is reverse function composition, i.e. x |> y |> z => x |> compose(z, y). When it occurs in the RHS of a |> expression, |> behaves as composition, and otherwise as application.

This analysis is understandable, except that I don’t think the mapping is from JavaScript |> to both Haskell $ (fn application) and . (fn composition). JavaScript |> alone technically does not deal with function composition.

a |> b is not function composition. $=>$ |> a |> y or 🖤 a |> b would be function composition.

JavaScript |> is much more like Haskell $ alone. x |> a vs. a $ x. Note how both have asymmetric data types (JavaScript: value |> function; Haskell: function $ value).\ To get composition in JS, you need to prepend a $=>$ or 🖤 head to the |> chain.\ To get application in Haskell, you need to prepend a … $ tail to the . chain.

You need two operations in JavaScript to get all expected semantics, just like how you would need two operators in Haskell. It’s just flipped in a different way: in JavaScript, composition has looser precedence than application. In Haskell, composition has tighter precedence than application. That’s the true difference.

Otherwise, it’s quite similar. In Haskell, . “acts like both composition and application” if you group it in a certain way on its LHS, just like how |> “acts like both composition and application” if you group it in a certain way on its RHS.

Imagine that, in Haskell, c . b. a $ x. This is the same in fact as c $ b $ (a $ x)), right? This precedence has advantages and disadvantages. The advantage is that you can move around c, b, and a without adding parentheses. The disadvantage is that you are dependent on creating many small functions, rather than non-function expressions. This is quite fine in Haskell, where all expressions are quite uniformly composable. JavaScript, however, has numerous types of non-function expressions; in particular, there are several ones that are function-scoped. And other practical reasons, as you point out.

I will point out, however, that Proposal 4: Smart Mix’s |> is bidirectionally associative. It has the associative property. (x |> a) |> b is the same as x |> (a |> b), for reasons that my draft explainer will get into once it’s finished. This is a pretty darn good property for refactorability, I think, but we will see.

The annoying thing about the pipe operator as specified is that it breaks equational reasoning. Given const y = x |> a |> b |> c |> d, I can't just cut and paste the b |> c |> d expression to obtain const e = b |> c |> d; const y = x |> a |> e.

With this JavaScript example, yes, you would have to cap b |> c |> d with a 🖤: 🖤 b |> c |> d to perform composition. This is akin to having to cap the Haskell b . c . d with $ in Haskell whenever you want to perform application. It’s pretty parallel. The result, as explained above, is the differing relative operator precedences.

The core problem is actually a core tradeoff. It emphasizes application over composition. But both are still “solved”. You just have to cap your expression chain with the opposite kind of operator sometimes, and leave your expression chain bare in other times. A tradeoff between two kinds of situations.

Based on what you're saying, it seems you're focusing on making reverse function application more ergonomic. This is great, and probably the 🖤 operator from your proposal is a good way of doing that. What I want, however, is a consistent, language level function composition operator (for various reasons, not least of which is the ability of runtimes to recognize and inline composition).

I wouldn’t characterize 🖤 as application. I would characterize 🖤 as adding composition to application, just as $ adds application to composition.

This was a very fun exercise, and I thank you for the food for thought. Hopefully concerns and benefits will become more definite once @mAAdhaTTah and I finish the specs/plugins for Proposals 1 and 4, after which both you and TC39 will be able to try them out hands on.

js-choi commented 6 years ago

@kurtmilam: Thanks for your patience. Let me explain my understanding of the original proposal, which I had thought was similar to an idea that I have been having.

My understanding is that a headless pipe operator is tantamount to a unary pipe operator. |> expression would be a unary operation on expression that prevents the immediate evaluation of expression, turning it into a unary function. It would interpret expression as if it were the RHS body of a pipeline. In other words, |> expression would be the same as $ => $ |> expression.

|> f is the same as $ => $ |> f, which is the same as f.\ |> console.log is the same as $ => $ |> console.log, which is the same as $ => console.log($) (which incidentally is the same as console.log.bind(console).)\ |> f |> g is the same as $ => $ |> f |> g, which is the same as $ => g(f($)).

This |> unary operation would also need to have looser precedence than the binary |> operator in order to wrap |> a |> b |> c correctly as $ => ($ |> a |> b |> c). Otherwise, it would be ((|> a) |> b) |> c, which would be a |> b |> c, which would immediately evaluate into c(b(a)), which is a value, not a composed function.

I like this loose, function-returning unary operator, except I am not a fan of how it has the same name as the tighter, value-returning binary |> operator.

But perhaps I have been severely misunderstanding the original post. I had thought @mAAdhaTTah has had a similar understanding as me, though.

masaeedu commented 6 years ago

The core problem is actually a core tradeoff. It emphasizes application over composition. But both are still “solved”. You just have to cap your expression chain with the opposite kind of operator sometimes, and leave your expression chain bare in other times. A tradeoff between two kinds of situations.

You can avoid a tradeoff if both reverse composition and reverse application are available as distinct operators. When you want to perform composition, use the composition operators. When you want to perform application, use the application operators. Then you won't have to cap an extracted sub-expression with anything while refactoring.

For this reason I think a "capping operator" like I proposed is probably unnecessary if both the current pipeline operator and something like https://github.com/isiahmeadows/function-composition-proposal are available in the language.

js-choi commented 6 years ago

@masaeedu: Thanks for the reply.

The tradeoff cannot be avoided, I think. It is still present even with reverse composition and reverse application. With the Haskell style, you still have to use an operator (application) to cap many expressions (to convert them from composition). Other times (composition alone), you do not. It makes some refactoring harder and some easier. It’s the opposite tradeoff to JavaScript (use composition to convert from application; bare application). But it’s good food for thought as I write this draft.

It’s not their RTL dataflow that matters so much as it’s their relative precedence. Even if JavaScript introduced RTL composition and RTL application, it would still need to wrangle with this reversal of operator precedence. We are currently going with “application tighter–composition looser”, in the same manner that arrow functions are very syntactically loose (at the same level as assignment operations). JavaScript is a language of expressions, not just functions, but functions can still be composed with a single additional prefix operator. This is more ergonomic in most JavaScript than “composition tighter–application looser”. I in particular want to avoid allocating new function contexts where possible, especially given function-scoped operations like await and yield, which are much more ergonomic with expressions alone.

The tradeoff isn’t very big anyway, either way, I think: the pros and cons are small on either side. I had forgotten that reversed precedence was a thing though. I’ll have to re-review @isiahmeadows’ repository too to double check which precedence levels he chose…

masaeedu commented 6 years ago

@js-choi Hmm. I'm not quite following how the refactoring tradeoff persists if I have both operators available. Could you perhaps give me an example?

E.g. if I use the composition operator proposal and change the thing in the OP to const capitalizeKeys = pairs :> map(...) :> fromPairs, I can exploit the associativity of composition to factor out map(...) :> fromPairs or pairs :> map(...) as I please, without paying any toll/tradeoff.

If I did indeed mean to use reverse function application, which is left-associative, I can left-associatively factor out a |> b |> c => const x = a |> b; x |> c. Conversely, you can right associatively factor out c(b(a)) => const x = b(a); c(x) in forward function application.

Perhaps there should be a a <| b => a(b) operator that is right-associatively factorable (a <| b <| c => const x = b <| c; a <| x). This would give you parity with the composition operator proposal's <: and :>, but it's more OCD on my part than anything else.

ivan7237d commented 6 years ago

@masaeedu Let me make sure I get this right — please correct me if I'm wrong. In my code I sometimes need application (const y = x |> a |> b |>c |> d), and sometimes I need composition (const e = a :> b :>c :> d). If I want to be able to easily refactor between the two types of expression chains, and to be able to easily factor out parts of any of the chains, what I should really do is always chain expressions with :> rather than |>, meaning that instead of const y = x |> a |> b |>c |> d, I'd write const y = x |> a :> b :>c :> d.

What caused the confusion for me is that the operator is called the pipeline operator, so I assumed that that's what you'd use to chain expressions together. But while the reverse application operator is a necessary ingredient to creating pipes in JavaScript (without it you'd have to write const y = (a :> b :>c :> d)(x)), it seems that the composition operator is an equally important ingredient, and adding that composition operator is much better than trying to make a composition operator out of an application operator.

dead-claudia commented 6 years ago

@js-choi

I’ll have to re-review @isiahmeadows’ repository too to double check which precedence levels he chose…

It's low-precedence like this, but I didn't state whether it's higher/equal/lower than this. (I'd prefer higher, as it would enable things like @ivan7237d's idea, and it'd be easier to reason with.) That was something I left more open for interpretation and discussion in the future.

js-choi commented 6 years ago

@masaeedu @ivan7237d I understand better now, after writing out the table below. I had thought that application |> plus a loose prefix pipe-function operator ($=>$|>, or 🖤 for illustration) would be equal, on average, in syntax length, to application plus a tight binary function-composition operator . I was incorrect; there is a small asymmetry of length: still small but present. I had been thinking last night that in Haskell style you would oftentimes still have to cap any composition chains you want with an application…but you still have to cap application chains with another application chain in JavaScript with a 🖤 anyway.

This problem—having to add one prefix operator when extracting functions—is not very large, but it is indeed a disadvantage in terseness. However, a loose prefix pipe-function operator would not only accommodate composition, it would also address method extraction and (for Proposal 4) partial application. This versatility is something that a tight binary composition operator would not bring. That still is a tradeoff. Nevertheless, the two are not mutually exclusive, except insofar that TC39 is generally conservative about adding new operators to JavaScript.

In any case, thanks @masaeedu for your patience as I figured out my own little mistake from last night.

α β β′ α′
LTR flow
L-assoc. infix ops.
LTR flow
L-assoc. infix ops.
RTL flow
R-assoc. infix ops.
RTL flow
R-assoc. infix ops.
Apply is tighter Comp. is tighter Comp. is tighter Apply is tighter
Prefix pipe function Infix composition Infix composition Prefix pipe function
Same function context possible Same function context impossible Same function context impossible Same function context possible
x \|> A \|> B \|> C x \|> A ⋅ B ⋅ C C ⋅ B ⋅ A <\| x C <\| B <\| A <\| x
🖤 A \|> B \|> C A ⋅ B ⋅ C C ⋅ B ⋅ A 🖤 C <\| B <\| A
Δ = 🖤 A \|> B
x \|> Δ \|> C
Δ = A ⋅ B
x \|> Δ ⋅ C
Δ = B ⋅ A
C . Δ <\| x
Δ = 🖤 B <\| A
C <\| Δ <\| x
D = 🖤 B \|> C
x \|> A \|> D
D = A ⋅ B
x \|> A ⋅ D
D = C ⋅ B
D · A <\| x
D = 🖤 C <\| B
D <\| A <\| x

@isiahmeadows: Thanks. Yes, the precedence of a specialized function-composition operator would probably have to be tighter than |> for the composition operator to be of much worth. Otherwise you might as well use arrow functions, I figure.

masaeedu commented 6 years ago

@ivan7237d I think you and I are in agreement.

@js-choi That table is a good illustration, thanks. Regarding:

Yes, the precedence of a specialized function-composition operator would probably have to be tighter than |> for the composition operator to be of much worth. Otherwise you might as well use arrow functions, I figure.

I don't think this is the case. If |> has a higher fixity than :>, you can still do x |> (a :> b :> c :> d), or const f = a :> b :> c :> d; x |> f. This doesn't save you anything in character count over the 🖤 operator you're discussing, but it still spares user the mental overhead of being unable to refactor using equational reasoning.

Additionally, if you have a corresponding right associative <| operator (equivalent to $ or regular function application), you can set it's fixity to be equivalent to $ and do a :> b :> c :> d <| a.

masaeedu commented 6 years ago

As a slightly off topic comment, all of this discussion really highlights the need for a first class infix operators proposal for JS. If user-defined infix operators are in the language (or at least in transpilers), people can much more easily play around with proposals for the semantics of various operators. As semantics and patterns mature, common operators can be standardized and optimized for in runtimes. This would be in line with how the "what's a good way to do Promises" debate played out in JS.

I understand that with great power comes great responsibility, and undisciplined use of arbitrary infix operators can result in APL programs embedded in your language. Nevertheless, I think the benefit of being able to have these kinds of debates at the library level rather than the language level outweighs the costs.

mAAdhaTTah commented 6 years ago

@masaeedu This proposal may interest you.

js-choi commented 6 years ago

@masaeedu There’s a long path to go before extensible operators, although it would allow more “paving the cow-paths” à la the Extensible Web Manifesto.

In addition to the proposal for HOF operators that @mAAdhaTTah just linked, you may also be interested in prior TC39 discussions about extensible operators and operator overloading, which became much more concrete after progress on the BigInt and Decimal types.

The decorator proposals are also relevant insofar they provide a specialized AST API, which user-defined operators may also need.


As for equational reasoning, I see your point about the relative operator precedence being not as important for that case. In general, however, I would point out that equational reasoning is a continuum, not a binary. A syntax may facilitate ER more or less. For instance, even with a binary function-composition operator, it is still necessary to add something like const f =, which itself is a “cap”. A syntax could make this much harder (like having to instead add the longer cap const f = $=>$|>) or only slightly harder (like having to add const f = ->). Either way, it’s a 1:1 correspondence, and you don’t have to change the extracted phrase itself, but you still have to add boilerplate to it. It’s only a matter of how much more boilerplate.

Having said all that, a prefix pipe-function operator and a binary composition operator are not mutually exclusive anyway. So we’ll see how it goes in the future. 👍

mAAdhaTTah commented 6 years ago

Yeah, I'd probably prefer to see composition & application as separate operators, rather than overloading the pipeline with two different behaviors depending upon its head.

littledan commented 6 years ago

In some other threads, we've talked about considering the composition operator a separate, follow-on proposal. I'm happy with this effort to think through prefix |> and other ways we could meet this feature request, but do you think this should form part of the initial pipeline operator proposal?

js-choi commented 6 years ago

@littledan:

…do you think this [prefix pipe-function operator] should form part of the initial pipeline operator proposal?

Although #97 framed it in terms of a new operator such as ->, #97 asked this precise question about the prefix pipe-function operator in general. To quote a summary from https://github.com/tc39/proposal-pipeline-operator/issues/96#issuecomment-366829256:

[Suppose there is] a unary operator – let’s call it 🖤 for now, to avoid bikeshedding – that interprets its operand as if it were the RHS body of a |> operation, then creates a unary function – and this unary function would pass its sole parameter into its body as if its body were the RHS of a pipeline. That is, 🖤 expression would be equivalent to x => x |> expression. [In other words, 🖤 is a shortcut for x=>x|>, or $=>$|>, etc.] This “tacit pipe function” operator would work on all four Proposals 1,2,3,4.

[The 🖤 operator proposed in #96] is exactly the same thing as an idea I’ve been developing in my draft for Proposal 4. 🖤 is also the same idea as in #97, except #97 gets into more details on the ramifications of 🖤 for solving function composition (assuming Proposals 1,3,4), method extraction (for Proposals 1,3,4), and partial application into a unary function (Proposals 2,3,4).

97 focused on Proposal 4, in that I pointed out that TC39 last September wanted more coherence between pipelines, function composition, method binding, and partial application – and 🖤 applied to Proposal 4 might address nearly all their concerns.

I was asking (poorly, I know) whether I (and @mAAdhaTTah too) should integrate 🖤 into our respective proposals, before their first presentation to TC39 at the next meeting. The answer appears to be no for now, but I’m hoping eventually we will be able to revisit this in a follow-up proposal.

You and @ljharb expressed concerns about ->’s resemblance to =>, which is fine; 🖤 the prefix pipe-function operator need not be ->. It could be =|> or |>> or ||> or =|> or whatever, just as long as it’s equivalent to $=>$|> and as long as it’s not prefix |>.

Like @mAAdhaTTah, I really would not like it to be prefix |>, however (https://github.com/tc39/proposal-pipeline-operator/issues/96#issuecomment-366839372). Both because this is a quite different semantic from infix |>, and because I’m hoping that unary |> could be used for a certain other related semantic in the future.

But what the prefix pipe-function operator 🖤 precisely would look like…is just bikeshedding anyway. The important question in #97 was whether any such prefix pipe-function operator 🖤 should be included in the initial pipeline proposal. After all, TC39 consensus at September was to block pipeline operators until coherency with other semantics (such as partial application) was investigated, and the prefix pipe operator could solve many of them.

My impression had been that the question had been “no, wait until an add-on proposal for 🖤”, but now I realize that I might have given the wrong impression to you and @ljharb about 🖤. It’s not “another arrow function” so much as it’s just a shortcut for the current arrow function + pipeline ($=>$|>). I don’t want it to be prefix |>, but I would like it to be considered at all – if not prefix ->, then as prefix =|> or |>> or something – as part of a future unified approach to pipelines, partial application, function composition, and method extraction.

I am actually nearing the end of my own initial drafts for the explainer and specification of Pipeline Proposal 4. The explainer already has a brief informative appendix discussing the prefix pipeline operator 🖤 (it uses prefix -> right now but this could be changed to something else, like prefix =|> or prefix |>>). It wouldn’t take much more to formally specify its behavior in the specification itself. I don’t think it would be difficult to implement in Babel either.

littledan commented 6 years ago

I'm excited to read your draft, and glad you'll be including a section about this possibility.

Even if this is just another use of the pipeline operator, it's easy to specify and implement, we still might be able to decide to split off this other use into a follow-on proposal (to give us more time to consider it, and collect real-world feedback from the initial pipeline operator).

masaeedu commented 6 years ago

Closing since it seems the consensus is that this is unnecessary.

sberney commented 6 years ago

There's a lot of technical discussion going on here, and I might not be up to speed on every goal in the pipeline proposal, but I think point free form makes a lot of sense.

Disclaimer, I write tons of pipeline style code using promises already:

return await Promise.resolve().
then(() => operation()).
then(compose(a, b)).
then(c => f('foo', c));

and created some tools like (new ExtendedPromise(middlewareFactory)).resolve() where middleware take all the handlers supplied to then callbacks and wrap them, to get arbitrary chaining semantics, including a mapping back to function composition, such as f(g(h(x))). Additionally, with fancy middleware, this lets me automatically wrap values in the chain, make decisions based on the containers, and then call a function to spit out an unwrapped value at the end if desired -- which is inspired by haskell monads.

I haven't published any of these tools (I would have to rewrite them, as I don't have rights to the source), but to me pipelined code is elegant and intuitive; but! it's so easy to do already:

const fn = [
  f,
  g,
  h
].
reduce((g, f) => x => f(g(x)))

const y = fn(x)

or after cleanup

const fn = rcompose([
  f,
  g,
  h,
]);
const y = fn(x);

... There's just no automatic application to a literal, so an extra step is required. But this is actually quite intuitive. I just know if there's a |> operator, I'll have to see it everywhere.

Is there any value added over just creating right-compose and left-compose operators, and handling a few special cases around the compose operators, such as new and await, so that f . new Foo and f . await g resolve correctly?

Or can we get a flow-right and flow-left operator pair, that just give me my point-free style back out?

I know this isn't a proposal for function composition, but the entire time I was reading it, I have wondered why composition isn't preferred, since we aren't doing any monad bind or other value semantic manipulations here. New value semantics are great, but this just seems like function composition (with an ugly F# defacto representation!), but backwards.


Actually, so there's two things that pipelining seems to be doing here:

  1. Applying a composed function to a bottom value (I can think of other solutions)
  2. Acting as a bind >>= operator for async (... a small improvement)

Could there just be an abstraction like this?

let (~>) = new Operator~>(middlewareFactory);
let it = await $ a ~> b ~> c ~> d;

// example
const middlewareFactory = (g, f) => Promise.resolve(g(x)).then(f);