Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative implementation idea using comma operator. #308

Closed
haydenlauer opened this issue Nov 27, 2024 · 27 comments
Closed

Alternative implementation idea using comma operator. #308

haydenlauer opened this issue Nov 27, 2024 · 27 comments

Comments

@haydenlauer
Copy link

Instead of making a pipeline operator we make an expression-scoped variable assignment operator, and then use the comma operator to achieve the pipeline functionality. I think this may give us the best of both worlds.

value |>x, foo(32,x)

This lets you name variables like the F# style but also lets you use arbitrary expressions like the hack style. As an added benefit, you can access any previously defined variable like so:

max-min |> range,
pos-min|>pos1,
console.log(pos1,range),
pos1/range*100 |> percent,
percent>50

Also because it uses the existing comma operator, any performance issues or conflicts with other proposals are pre-existing. The only performance hit would be from expression-scoped variables which I would think should be equivalent to this:

(()=>{
let range=max-min;
let pos1=pos-min;
console.log(pos1,range);
let percent=pos1/range*100;
return percent>50})();

Side note: it would be nice if the |> could be used without commas, but that might be harder to implement:

(pos2/(max-min|>range)-pos1/range)
@snatvb
Copy link

snatvb commented Nov 29, 2024

I have no idea reason to have it and we have already , as seporator
for exmaple:

function foo() {
  return (console.log('hi'), 1) 
}

const result = foo() // output console log: 'hi'; result = 1

If you want Do notation, that should be done as another proposal. And we can learn from other functional languages instead of inventing our own

@haydenlauer
Copy link
Author

I have no idea reason to have it and we have already , as seporator for exmaple:

I listed a few benefits in the OP, but I would be happy to answer any specific questions that you have.

Also I am aware of the comma operator already, as I used it several times in the OP as well as explicitly referencing "the existing comma operator."

@Jopie64
Copy link

Jopie64 commented Nov 30, 2024

I'm not sure about the benefits of this proposal, but it got me thinking though... What if... let within an expression with parenthesis was allowed?
Maybe that would be enough 🤔

actionUri
  |> %.tolower()
  |> baseUri + %
  |> await fetch(%)
  |> JSON.parse(%)

Would be similar to

(let $= actionUri
  ,$= $.tolower()
  ,$= baseUri + $
  ,$= await fetch($)
  ,$= JSON.parse($)
)

Not that much more noise if you ask me... Also you can use your own topic token as long as it can be an identifier.

@gustavopch
Copy link

@Jopie64 I think it wouldn't work well with TypeScript as $ would only be able to have a single type, not a different type per step.

@haydenlauer
Copy link
Author

@Jopie64 That is what I'm proposing. I'm just using the |> operator to make the syntax closer to F# and reduce boilerplate.

@VitorLuizC
Copy link

What exactly you mean by "achieving the pipeline functionality"?

I'm asking because value |>x, foo(32,x) doesn't "pipe" value in a sequence of expressions. You're just assigning value to a variable then using it in a expression. It doesn't seem to address current proposal goals.

  • It doesn't solve deep nesting (e.g. a(b(c(d(1)), 3))).

  • It doesn't reduce necessity of creating and naming temporary variables.

@haydenlauer
Copy link
Author

  • It doesn't solve deep nesting (e.g. a(b(c(d(1)), 3))).

This would become d(1) |> c1, c(c1) |> b1 , b(b1,3) |> a1, a(a1).

  • It doesn't reduce necessity of creating and naming temporary variables.

Technically it does not. But, because the variables are scoped to the expression, you could just name them all r1, r2, ...rN. It also allows you to write a(c(7), b(12)) as c(7) |> a1, b(12) |> a2, a(a1, a2) which I would imagine is more readable than the hack or F# equivalent.

@HansBrende
Copy link

Why not just use the comma operator as the pipe operator? It already has nearly the same semantics as the pipe, except for the ability to "refer to the previous value".

Then the only tweak to ECMAScript would be allowing % in comma expressions to refer to the previous "thrown-away" value.

I.e.:
const result = a(b(c(d(1)), 3))

would become:

const result = (d(1), c(%), b(%, 3), a(%))

Seems a lot more palatable to me to build off of the existing syntax like this than introducing two new operators.

@ljharb
Copy link
Member

ljharb commented Jan 3, 2025

I don't think it's palatable at all to reuse the verboten comma operator, personally.

@haydenlauer
Copy link
Author

Why not just use the comma operator as the pipe operator?

Yes that is what I am suggesting. There is only one new operator, the |>, which is just an assignment operator but the identifier goes on the right. The comma is what is doing the actual pipelining.

@haydenlauer
Copy link
Author

I don't think it's palatable at all to reuse the verboten comma operator, personally.

That's the beauty of this idea. The comma operator is verboten because it can only be used to do questionable things. This feature would allow it to be useful for something.

@HansBrende
Copy link

@ljharb I somewhat agree, but I do think it would be more palatable than introducing two new operators when one of them (halfway) already exists. 50% of the reason comma is verboten is that its existing use-cases are not compelling enough to merit usage. Perhaps that would change if its semantics became slightly more powerful.

@haydenlauer I see, however I think it would be pretty verbose without that extra % operator... might as well just do standard assignments like the following at that point:

const c1 = d(1), b1 = c(c1), a1 = b(b1, 3);
const result = a(a1)

(which doesn't actually use the comma operator at all, since we are just doing multiple assignments on one line).

What I'm saying is that if the comma operator were augmented with the additional power of %, then you'd have a pipe operator that already exists in javascript like so (without having to assign anything at all):

const result = (d(1), c(%), b(%, 3), a(%))

@ljharb
Copy link
Member

ljharb commented Jan 3, 2025

I don’t think it’s better to take something with a harmful meaning and give it an additional beneficial one.

@HansBrende
Copy link

@ljharb but the only harm in its current meaning is its lack of being compellingly beneficial. If you consider the existing semantics of the comma operator harmful, then by extension, the semantics of any pipe operator would be at least as harmful since it would include all the semantics of the comma operator, plus additional semantics.

Or do you mean harmful syntax? I could possibly get behind an argument for harmful syntax, but then again, that's what linters are for.

@gustavopch
Copy link

My two cents: I like the fact that this idea, const result = (d(1), c(%), b(%, 3), a(%)), would require only minimal changes to the language—and consequently to syntax highlighters, Prettier, ESLint, etc. It's probably the simplest way to achieve a pipe operator and the simplicity could perhaps make it more likely that it wouldn't take many years for browsers to implement.

@VitorLuizC
Copy link

It also seems to work vertically.

getPersistedJWT()
  , Maybe.fromNullish(%)
  , Maybe.chain(%, decodeJWT)
  , TaskEither.fromMaybe(%, new AuthenticationError())
  , TaskEither.chain(%, fetchUser)
  , TaskEither.chain(%, persistUser)
  , TaskEither.mapLeft(%, resolveToToastMessage)
  , TaskEither.do(%, showToast, navigateToProfile)

@HansBrende
Copy link

HansBrende commented Jan 3, 2025

Another bonus of comma: it would leave the |> operator available for some future F#-style proposal, should the community decide they want more pipes!

Comma on the other hand seems very naturally suited to the hack-style, since that's what it already does.

If that eventuality ever came to pass, we could have the "smart mix" without any of the syntactic ambiguity concerns inherent in having one operator do both things.

I'd imagine this would get the anti-hacktivists who moved into the "do nothing" camp back on board, since now we are talking about an extremely tiny tweak to the language for the hack style--not even really a "new" language feature in my book, just a tiny enhancement to a previously worthless feature to provide insane value!

Heck, this comma idea has even got someone like myself who originally hated both proposals (no offense) very intrigued. There is nothing more satisfying than repurposing trash (i.e., the comma operator) as art :) It's like when ASCII eventually grew wings and metamorphosed into the breathtaking UTF-8, as if that had been its plan all along. Pure elegance ❤️

@Avaq
Copy link

Avaq commented Jan 3, 2025

You could possibly even generalize this idea, and say that the chosen "topic token" simply returns the value of the last executed expression in the same scope, in similar fashion to how _ does that in many REPLs. That way, it can be used as a topic in comma chains as proposed, but serve other purposes as well, and make the REPL thing a standard.

EDIT: hm I guess that'd complicate something like foo , bar(f(), %) though 😅

@tabatkins
Copy link
Collaborator

I'm going to go ahead and close this. We're not interested in pursuing a comma operator for this functionality. Jordan has given some of the reasoning already (the comma operator is already generally a bad part of the language), and the awkward interaction of it with arglists, array literals, object literals, etc. is another.

@HansBrende
Copy link

@tabatkins I did consider that there would be some syntactic ambiguities to the idea (albeit no less than what already exists), but figured that all such ambiguities could easily be resolved in the worst-offending cases via explicit parentheses.

Personally not a huge fan of adding 2 new operators to the language for one convenience feature when 1 out of those 2 already exists with virtually identical semantics (only real difference being operator precedence 1 level offset from initially proposed, so yes more parentheses)... but, oh well 😕

@ljharb
Copy link
Member

ljharb commented Jan 4, 2025

The math here isn't as simple as "count the operators". 2 operators isn't inherently worse than 1, if it ends up with a better DX outcome.

@gustavopch
Copy link

It would be helpful to elaborate on exactly how the DX could be worse. We have some well-described pros in this issue, but the cons are not clear, at least to me.

@HansBrende
Copy link

HansBrende commented Jan 4, 2025

@ljharb I don't think my sentiment is quite as superficial as you make it out to be--but every new operator does add a tax of sorts on the language, so the number of new operators added per feature is certainly something to be conscious of.

Especially when there is an existing operator that already performs the same function. Would the DX really be so negatively affected by augmenting the existing one that it would outweigh the disadvantage of adding a new operator to do the same thing? I'm somewhat skeptical of the claim that it would. To my mind, there are 2 possible cases here where there is surface area for ambiguity:

  1. The ambiguity would result in a parsing error
    • This would cover nearly all cases, since the % operator would not be syntactically allowed anywhere but in positions after the first comma operand, so for instances where enclosing parentheses were forgotten (e.g., as an argument to a function or array literal), the parser would see a % not preceded by a prior comma operand.
    • In this case, DX is not significantly affected because developer would be alerted immediately to add parentheses around their expression to resolve this ambiguity
    • A concrete example: const singletonArray = [a, f(%), g(%)] would immediately become a parse error, since the parser would see f(%) and g(%) each missing a prior comma argument. This would immediately alert the developer that they must do const singletonArray = [(a, f(%), g(%))] instead (or simply extract the first element into its own variable). This is the same way that many languages behave, such as Python's tuple syntax, which everyone loves, despite the cases where it must be disambiguated via parentheses. The only difference between Python's tuple syntax and JS's comma operator in terms of negative connotation is simply that Python's tuple syntax is actually useful. JS's comma would quickly develop a more positive connotation if it too, were to become actually useful.
  2. The ambiguity would not result in a parsing error
    • The only cases I can think of where this could happen would be if the developer was not using the % operator subsequently to the first comma argument
    • In this case, linters could warn that a comma expression was used without making use of the prior arguments via the % operator (as they already do today!), so as long as the developer was using a linter, DX would probably not be significantly affected in many cases
    • Or, if the developer for some reason forgot the subsequent % operator and was creating an array literal, for example, without parenthesizing the element... this seems like an unlikely scenario. But regardless, now we are in the realm of the "plain old comma expression" as it exists today, without usage the % operator within subsequent arguments (which should be discouraged, just as it is today).

So in summary, I'd echo @gustavopch's slight confusion as to what the most significantly disadvantageous cons you see would be.

@snatvb
Copy link

snatvb commented Jan 5, 2025

The math here isn't as simple as "count the operators". 2 operators isn't inherently worse than 1, if it ends up with a better DX outcome.

2 different syntaxes that do the same thing immediately indicates poor design. It's not a good idea to complicate the syntax without a real need. C++ has already proved it.

@HansBrende
Copy link

Note to casual passers-by: the proposal in the issue description does not represent the syntax we are now discussing. The original issue suggests a syntax that nearly everyone here agrees is a bad idea.

Would it be worth opening a new issue to discuss the subsequent proposal, starting at this comment, to ensure it gets a fair shake with all the pros and cons listed before being closed? I worry that the later idea has not received due consideration (possibly in part from the clutter/confusion of the context of the original issue description).

@gustavopch
Copy link

@HansBrende I was thinking the same thing. IMO, it would be great if you could open a new issue summarizing the points that are actually relevant to avoid confusion. If possible, pass it through o1 and Sonnet 3.5 to further scrutinize the idea (like, ask them to behave as TC39 members) so that the new issue can immediately contain as much consideration as possible about pitfalls and pain points, including examples of real-world usage.

To be clear: I'm not saying this idea must be pursued, I just think pros and cons should be clear before it's dismissed, even if only for historical and documentation purposes.

@HansBrende
Copy link

@gustavopch glad for the positive feedback! I will begin crafting this issue later in the week if I do not hear objections from those with close privileges. (As, I wouldn't want to invest that amount of time into composing such a document if it would result in another quick closure without allowing the community a "clean-slate" opportunity to examine more deeply the relative benefits and specific, tangible, & precise drawbacks of the proposal.)

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 13, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants