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.52k stars 108 forks source link

Named placeholders #203

Open shuckster opened 3 years ago

shuckster commented 3 years ago

Just wondering if it wouldn't guide our intuition a little better if we could gives names to placeholders in the same manner as arrow-function arguments?

envars
     x |> Object.keys(x)
  keys |> keys.map(x => `${x}=${envars[x]}`)
   arr |> arr.join(' ')
   str |> `$ ${str}`
  line |> chalk.dim(line, 'node', args.join(' '))
   out |> console.log(out);

No need to choose between ^ or %, no conflicts with mod/pow, and has a better chance of producing self-documenting code.

What are the drawbacks to this approach?

samhh commented 3 years ago

It now looks quite similar to the F# proposal except it disallows point-free in exchange for dropping an arrow:

envars
  |> x => Object.keys(x)
  |> keys => keys.map(x => `${x}=${envars[x]}`)
  |> arr => arr.join(' ')
  |> str => `$ ${str}`
  |> line => chalk.dim(line, 'node', args.join(' '))
  |> out => console.log(out)

Where for clarity point-free means something more like this, which would've been deemed more idiomatic with that proposal:

envars
  |> keys
  |> map(x => `${x}=${envars[x]}`)
  |> join(' ')
  |> prefix('$ ')
  |> chalk.dim('node')(args.join(' '))
  |> console.log

If there is actually any chance of an F#-style operator following the blessed Hack one then I think it covers this use case without introducing more special syntax.

Avaq commented 3 years ago

Related comments:

The named placeholder idea was also proposed here: https://github.com/tc39/proposal-pipeline-operator/issues/91#issuecomment-363092439 With a similar response here: https://github.com/tc39/proposal-pipeline-operator/issues/91#issuecomment-917466063

kiprasmel commented 3 years ago

@Avaq

The named placeholder idea was also proposed here: https://github.com/tc39/proposal-pipeline-operator/issues/91#issuecomment-363092439 With a similar response here: https://github.com/tc39/proposal-pipeline-operator/issues/91#issuecomment-917466063

I wouldn't say my response is similar per se. Let me just clarify: the first link advocates for named placeholders with Hack, and mine advocates F# over Hack. edit: correction: The first link also considers F# over Hack in the second part, which is identical to mine.

Named placeholders, as @samhh mentions here in the comment above:

https://github.com/tc39/proposal-pipeline-operator/issues/203#issuecomment-917481832

It [named placeholder] now looks quite similar to the F# proposal except it disallows point-free in exchange for dropping an arrow

(note the point-free mention on yet another point for why F# is better than Hack, even with named placeholders!), and

If there is actually any chance of an F#-style operator following the blessed Hack one then I think it covers this use case without introducing more special syntax.

which is exactly what I'm advocating for in my response - F# over Hack:

https://github.com/tc39/proposal-pipeline-operator/issues/91#issuecomment-917466063


In the meantime, I'm trying to write a longer peace about pipeline operators, how the F# way is already available via [].map and Promise.then (monads & stuff), relations with functional programming and why we shouldn't rush this, but it will take a while for me. The point is - the Hack proposal choice feels rushed, we need more experienced people in the discussion (esp. from the FP space since they've had experience solving the same problem we're trying to solve) and I am very afraid of us making a big mistake.

runarberg commented 3 years ago

Advocates of functional programming (FP) have very much been part of the discussion post and prior to the decision to go with hack. (See e.g. here). However I’m not aware of any of them being a member of the language committee that ultimately made the decision to go with hack pipelines.

I guess we are all waiting to see the meeting notes and see which justification for why the concerns raised by FP folks were not important enough to warrant further consideration.

kiprasmel commented 3 years ago

Good points @runarberg.

As per https://github.com/ReactiveX/rxjs/issues/6582#issuecomment-909556856,

It's at the discretion of members of the TC39 and has little to do with what non-members want or even popular common usage. It's just what a paid member is willing to push forward, unfortunately, and the other proposal didn't have a champion from a member company.

which seems complete bonkers. How it is even possible to choose one of the competing proposals if nobody is defending the other one?

mAAdhaTTah commented 3 years ago

How it is even possible to choose one of the competing proposals if nobody is defending the other one?

That's not what happened. There were several members of the champion group who advocated for F# but with the committee starting to achieve consensus, believe Hack is better than no pipeline and won't be blocking the proposal.

shuckster commented 3 years ago

Thank you for the replies so far all. I must say, after reading through them and the linked content I'm convinced that the F# style fits JavaScript better than the Hack proposal.

I was rather alarmed by the slightly conspiratorial wording that @kiprasmel quoted. It added to a nagging sensation that came to me as I looked at the README: pipe() examples are conspicuous by their absence:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x)

pipe(
  x => one(x),
  x => two(x),
  x => three(x),
)(value)

Why are there no example like this? It's a common and unremarkable pattern in FP-JS circles. Even without the exciting point-free potential, surely F# would be synchronous with this? Is there a use-case that Hack solves better that justifies its idiomatic deviation?

kiprasmel commented 3 years ago

Your observation is correct @shuckster.

@benlesh has discussed this in the RxJS repo (basically, Hack sucks):

Currently our "pipeable operators" rely on higher-order functions. Using these with the Hack Pipeline will be cumbersome, ugly and hard to read: <...>

and as he mentions in the @tabatkins discussion:

To get what we want with Hack Pipeline without hamstringing the entire JavaScript functional programming community, the ideal solution (that would please everyone) is to land the F# pipeline, and then focus on the partial application proposal.

aadamsx commented 3 years ago

That's not what happened. There were several members of the champion group who advocated for F# but with the committee starting to achieve consensus, believe Hack is better than no pipeline and won't be blocking the proposal.

I don't belileve you or I or anyone outside the committee knows exactly how things have gone down @mAAdhaTTah -- I think there are dissenting views on the committee that should not be overlooked: https://github.com/tc39/proposal-pipeline-operator/issues/91#issuecomment-917645179. Also, I think it would be better to have no pipe rather than a half-baked hack-pipe.

If you have to type placeholders -- you're doing it wrong! 🐙

mAAdhaTTah commented 3 years ago

@aadamsx That dissenting view has not been overlooked; he advocated for F# but the committee's momentum is heading towards Hack, and he has made (imo) a reasonable decision that he will not prevent that momentum from allowing the Hack pipe to advance into the language. You're welcome to disagree with that decision, but he has been a part of this process since the beginning and is hardly being overlooked.

kiprasmel commented 3 years ago

the committee's momentum

is a joke. I completely agree with @aadamsx:

it would be better to have no pipe rather than a half-baked hack-pipe.

and I will post more on this soon.

mAAdhaTTah commented 3 years ago

I don't belileve you or I or anyone outside the committee knows exactly how things have gone down

If you don't know how it went down, then why are you so sure the answer isn't "most of the committee was convinced of Hack's advantageousness, and the ones who preferred F# saw the writing on the wall & preferred Hack to advance than nothing at all"?

aadamsx commented 3 years ago

why are you so sure the answer isn't "most of the committee was convinced of Hack's advantageousness, and the ones who preferred F# saw the writing on the wall & preferred Hack to advance than nothing at all

I'm not saying one way or the other, just that you/I/we don't know for sure, but only what other committee members let out.

What I take from Ron B's comments, all where on board to go to Stage 2 based on what was presented by Tab, but NOT ALL agree as of yet that Hack > F# -- therefore no consensus.

mAAdhaTTah commented 3 years ago

I'm not saying one way or the other, just that you/I/we don't know for sure, but only what other committee members let out.

Given Ron's comment and the conversations I've had with Tab & other champions, given that I myself moved from F# -> Hack preference, and given that the committee doesn't advance things without consensus, I have far more evidence this is what happened than a coup by a small number of committee members. The committee had to be in consensus to advance, because if a single member objected, it would not, could not, advance.

runarberg commented 3 years ago

From where I stand—as a layperson in language design and a mere user of the JavaScript language—there are two possibilities here. Either the TC39 process was hacked in order to advance one proposal over the other or the TC39 process is flawed.

I was under the impression that users of functional libraries were going to get a language construct which would ease development using them. A few years ago this construct was supposed to be the bind operator (::) but that was abandoned in favor or the pipe operator (|>). I’m not sure this is how things were, but this is how I perceived it. The pipe operator laid in limbo for a couple of years, some discussion popped up occasionally comparing alternatives, but no conclusion was reached. The pipeline there seemed to be a deadlock in choosing either the pipeline operator that I thought we were promised or an alternative which would make it useless for us. Several proposals were voiced that would break the deadlock (including minimal proposals (#167), the lowest common denominator (#192), usability studies (https://github.com/tc39/proposal-pipeline-operator/issues/167#issuecomment-751322272)). Then after no progress, all of a sudden a decision was made and the benefits that the pipe operator provides to the functional community has been striped away. The decision was made based on unconvincing evidence and a sizable portion of the community are now seemingly either disappointed or angry.

To my eyes something has failed. Either the wrong decision was made or at the very least, only minimal effort has been made in convincing us otherwise. The arguments I am perceiving rely heavily on appeal to authority which is not a good look on the TC39 process.

tabatkins commented 3 years ago

I'm disappointed that I've had to once again mark a large chunk of an issue thread as off-topic. The OP and the first few comments presented a reasonable issue and discussion; this is not a place to diverge into yet another discussion of F#-vs-Hack or baseless speculation on committee proceedings. There are dedicated threads for that.

shuckster commented 3 years ago

@tabatkins - Apologies for my part in that. I've since caught up in much of the discussion and regret getting caught-up in the politics of it.

tabatkins commented 3 years ago

I'm not certain the syntax suggested in the OP is actually viable; there might be parsing ambiguities with the preceding line.

Putting that to the side, tho, this runs into some of the arguments against temp variables from the README. Definitely not all, since the bindings are still scoped to a single pipe step, which avoids several issues, but the "naming is hard" part still applies.

Most of the time, you don't need to give a name to the topic; it's clear from context, and the topic value doesn't represent a semantically significant unit in your project. (See the README again, where several of the names are just restatements of the operation happening on the line, rather than actually meaningful names.) So I expect most of these lines would, in practice, just use the name x or something, whatever the user typically uses for temp var names. I'd prefer we just use a chosen standard name in those cases, to communicate the semantic that the value is just flowing from the previous line and isn't significant on its own.

That said, I'm not opposed to naming the placeholder sometimes, when it helps. It would just need a good syntax that doesn't cause issues.

kiprasmel commented 3 years ago

I'm disappointed that I've had to once again mark a large chunk of an issue thread as off-topic.

@tabatkins, with all due respect, could you just not?

Nobody complained about the comments being off-topic.

If you had marked my comment https://github.com/tc39/proposal-pipeline-operator/issues/91#issuecomment-917755642 to move the discussion from #91 to #205 earlier than you did, you would've denied a wider discussion with community members that's happening right now in #205, AND made it worse for the #91 thread itself, because you'd get actually off-topic comments there, instead of them being on-topic in #205.

Also, how is https://github.com/tc39/proposal-pipeline-operator/issues/203#issuecomment-917677239 off-topic? It highlights an important concern, and you allow yourself to hide it from others' eyes just because it had something non-so-related to say too?

Do you realize the negative impact that your moderating has?

shuckster commented 3 years ago

@kiprasmel - Judging from his last reply, I don't actually think @tabatkins is afraid of hearing criticism, even if it's delivered emotionally, so long as we keep it in separate threads. I still genuinely wonder about pipe() examples, but I realise I didn't do myself any favours by embroiling myself in conspiracy, especially as I had merely read the README at that point.

Anyway, I did have an idea for placeholder-naming that follows-on from the OP, but I'll reply separately as this post is also clearly off-topic at this point.

shuckster commented 3 years ago

Thank you for the reply @tabatkins . My initial crack at placeholder naming is as follows:

envars
  |> Object.keys(^)
  |> ^keys.map(x => `${x}=${envars[x]}`)
  |> ^arr.join(' ')
  |> `$ ${^}`
  |> chalk.dim(^line, 'node', args.join(' '))
  |> console.log(^chalked);

Essentially, everything following the token is working like an inline comment up until the next consumable character.

Janky, but that's all I got at the moment. I do appreciate the arguments against temp-variables, but I'd like to think the Hack token is just about comparable to the i in the uncountable for-loops of times gone by. But still today we have a chance of renaming them!

lightmare commented 3 years ago

It took me a while to realize that I suggested the exact same thing in another topic a few days after @shuckster, apologies for not giving proper credit, I must've tricked myself into thinking "iOriginal".

Although I like the OP syntax the most (out of all the named-placeholder options I've seen), I share @tabatkins concern regarding grammar. I'm not sure how exactly it'd play out, at first I thought it's fairly simple: Expr Identifier |> Expr. But it seems like it would require more lookahead for any expression followed by a newline and an identifier. Currently, when the parser reads Expr \n Identifier, it inserts a semicolon at the newline. But if that could be the left-hand-side of a pipeline, it would need to check the next token as well.

So it might be safer, though not as pretty, to place the identifier after the operator: Expr |> Identifier Expr.

Exploring further, here's something crazy: In this variant you could replace the `Identifier` with `(BindingIdentifier | BindingPattern)` — since the context is clear from the preceding `|>` you can have destructuring right there. And then, if we disallow newline between the binding and the pipe body, we could allow a pipe *without* that binding to behave differently... ```js // Expr |> BindingIdentifier [noLineBreak] Expr // single-binding Hack behaviour obj |> _ Object.keys(_) |> _ log(_.join(", ")); // Expr |> BindingPattern [noLineBreak] Expr // destructuring Hack behaviour /(?\w+)=(?.*)/.exec(input) |> { groups: { k, v }} (options[k] = v); // Expr |> Expr // F# behaviour arg |> foo |> (x => bar(x, 2)); // yes I would absolutely require parentheses around arrow functions // to avoid all kinds of ambiguities ```
lightmare commented 3 years ago

@js-choi I just noticed you added the follow-on-proposal label here. May I suggest considering named placeholder an alternative baseline instead?

If we started with grammar such as: Expr |> BindingIdentifier [noLineBreak] Expr The topic would be user-defined identifier, so you could introduce the pipe operator without a new topic token.

Expr is a placeholder for whatever the precedence would dictate. I would give the pipe the highest precedence among binary operators — putting a writer-tax on binary expressions and yield in pipeline. But that's a separate discussion.

Then if need arises you could allow destructuring the topic in place in a follow-up: Expr |> BindingPattern [noLineBreak] Expr

And when bikeshedding the topic token finally concludes, you could enable: Expr |> Expr without explicit topic binding. Or if implicit topic becomes unnecessary, reserve that for F# style. (edit: this has serious potential for human-reader confusion, so probably not viable; but Expr |> PFA might be viable)

aikeru commented 3 years ago

I did a search of this repository for the word "with" and didn't see this idea, which I take as an imperfect hint that this hasn't been suggested in the repo. Just thought, since with use is discouraged, maybe something like this could work...

with name |> expr(name)

such as...

// A single name
const shoutedName = with userName |> capitalize(userName) |> amplifyVolume(userName)

// multiple names??
const shoutedName = with userName |> capitalize(userName)
  with capitalName |> amplifyVolume(capitalName)

// or perhaps it only names the next one?
const shoutedName = with userName |> capitalize(userName) |> amplifyVolume(^)

This would seem to be unambiguous to the untrained eye, since old-style with requires parens with(expr). It also seems coincidentally teachable since "with x" and "const x" are similar.

ljharb commented 3 years ago

@aikeru none of those pipelines seem to have an initial value.

aikeru commented 3 years ago

@aikeru none of those pipelines seem to have an initial value.

Ah, ha! Perhaps that foils this idea, then, but maybe something like this?

const result = unNamed with named |> make(named)

...or not. Thanks anyway!

js-choi commented 3 years ago

I just noticed you added the follow-on-proposal label here. May I suggest considering named placeholder an alternative baseline instead?

Whether to make declaring an identifier at each pipe step required in the initial proposal…

It’s not only up to me, of course.

But I’m personally lukewarm towards this. It seems pretty verbose, since it’d be required in each step.

By the time we’re naming variables, we might as well be just using variable assignment, right? A lot of the purpose of this proposal is to Not Name the Thing When You Don’t Want to.

Anyways, sorry if that response is disappointing. Hopefully it’s understandable. Also, it’s not only up to me—there are other champions—but I also suspect that it would be unpopular with the greater Committee…

lightmare commented 3 years ago

By the time we’re naming variables, we might as well be just using variable assignment, right?

No. Because apples-to-apples equivalent would use const which you can't re-assign. Also variables don't go out of scope afterwards.

The difference is who gets to name the variable: the committee, or the user.

tabatkins commented 3 years ago

Yeah, baking this in as a requirement for each pipeline step is a no-go. The existing piping syntaxes in JS (method chaining, userland pipe()) don't name the topic variable being passed between each stage, and that still makes for perfectly reasonable and readable code. We don't need to require additional verbosity at each stage for this piping syntax.

lightmare commented 3 years ago

The existing piping syntaxes in JS (method chaining, userland pipe()) don't name the topic variable being passed between each stage, and that still makes for perfectly reasonable and readable code.

Not sure how that's relevant, as this piping syntax does name the topic.

We don't need to require additional verbosity at each stage for this piping syntax.

That's assuming a single-character topic token. Otherwise there's no additional verbosity required.

js-choi commented 3 years ago

No. Because apples-to-apples equivalent would use const which you can't re-assign.

Yeah, sorry, by “variable assignment” I meant “constant assignment”. By the time we’re naming constants, we might as well be just using constant assignment, right?

Also variables don't go out of scope afterwards.

Well, right now Hack pipes are associative. (a |> f(^)) |> g(^) and a |> (f(^) |> g(^)) are equivalent.

Having f() |> a g(a) |> b h(a, b) throw a ReferenceError would impose left associativity on |> and get rid of its right associativity. I personally would expect f() |> a g(a) |> b h(a, b) to work.

In fact, my own mental model of |> is right associative, which matches that of nested monadic-bind callbacks: how (a => (b => h(a, b))(g(a)))(f()) works. In fact, I had brainstormed a whole big draft proposal for named topics with monadic modifiers based on that intuition, and that proposal would have imposed right associativity. (I scrapped it in favor of exploring F#-style computation expressions and/or algebraic effects.)

In any case, I also suspect that it would be unpopular with the greater Committee, but yeah.

lightmare commented 3 years ago

Having f() |> a g(a) |> c h(a, b) throw a ReferenceError would impose left associativity on |> and get rid of its right associativity. I personally would expect f() |> a g(a) |> b h(a, b) to work.

I didn't mean to imply ReferenceError in that case.

Whether f() |> a g(a) |> b h(a, b) would work as you expect, or would throw ReferenceError, could be another discussion. I tend to agree with your expectation — if someone used different topic names, they probably wanted to use them together at some point, so it makes more sense to impose right-associativity.

I meant that if you used const a = ..., b = ...; those are in scope till the end of block, meaning not only that you cannot re-purpose those names later in the block (which is one annoying difference from topic binding), but also that you can use those values arbitrarily far away (another annoying difference).

runarberg commented 3 years ago

This is going off topic but could you explain this a little:

Well, right now Hack pipes are associative. (a |> f(^)) |> g(^) and a |> (f(^) |> g(^)) are equivalent.

This looks a bit weird in my mental model. I actually wouldn’t expect F# to be associative this way:

(a |> f) |> g  // Same as g(f(a))
a |> (f |> g)  // Same as g(f)(a)

I’m actually having a hard time figuring out what a |> (f(^) |> g(^)) should do. Does the outer RHS (f(^) |> g(^)) actually return an expression g(f(^))? ~If so, why shouldn’t it return a different expression g(f)(^)?~

EDIT: I see now how it returns g(f(^)) I simply take the LHS expression and replace the ^ on the RHS with it which gives g(f(^)) so it is indeed associative. Sorry for the confusion.

jithujoshyjy commented 2 years ago

I would like to propose a syntax for the named placeholder idea;

const superhero = [] as powers
    |> grantPower("nightvision", powers)
    |> make("invincible", powers)
    |> powers.map(x => x.toUpperCase())

We've been discussing about it at TC39 Discourse Group More details mentioned there☝

ljharb commented 2 years ago

I’m unclear on the argument for naming the token that doesn’t allow you to name it in each step.

ghost commented 2 years ago

Not sure if this has been said before but 2 things about this proposal:

Which is neat.

Anyways, just my 2 cents.

shuckster commented 2 years ago

@edeboursetty I'm not sure the current proposal permits a |> without a balancing ^, so your first example would be a syntax error. I'm not sure I fully understand the second example - there don't appear to be any placeholder ^ tokens?

theScottyJam commented 2 years ago

One option would be to allow us to provide a name after the pipeline token. For example, say we go with %% as the hack-pipe token. Then, if you want to provide a name, just stick it right after the token, like this %%user.

As an example:

id
  |> getUserById(%%)
  |> getGroupFromUser(%%user)
  |> doOperation(%%group) + anotherOperation(%%group)

// And this would be a syntax error
// because you can't use different names within the same pipeline step
data |> doOperation(%%xyz) + anotherOperation(%%abc)

This would require us picking a hack-pipe token that can actually be used in this way (i.e. using a binary operator, such as ^ wouldn't work, because x ^y would cause ambiguity)

This solution also doesn't fix the nested pipeline issue, but what I care about more is the ability to name the token (to help self-document it), then the ability to nest pipelines in a more readable way.