tc39 / proposal-do-expressions

Proposal for `do` expressions
MIT License
1.12k stars 14 forks source link

`do { }` vs `do { } while ();` confusion #53

Open davethegr8 opened 3 years ago

davethegr8 commented 3 years ago

I'm worried that these two similar constructs are going to cause a lot of confusion in JS developer land. I foresee people being very confused with the differences, especially if a do block / do while loop is very log and they miss the end of it.

Also, I can see confusion / problems come up because of ASI, namely code like this:

do {
    // something
}
while(...)

If the loop was written poorly, I could see it becoming infinite. I could also see the other side, the above code being an intentionally a do block followed by a while loop.

ljharb commented 3 years ago

This would only be a problem when used in a statement position - but the value of do expressions is to use them in expression position, where a do..while loop is invalid. In your above example, without the while it'd be an error, no?

ghost commented 3 years ago

hmm...

do { 1; } while (1) { console.log(1); }

is valid code, so do blocks would break web compatability, or at least be weird to look at.

...without the while it'd be an error, no?

Currently, it would be, but expressions are valid where statements are, but not vice versa.

Valid statement:

1;

Therefore, "do blocks" should be valid anywhere:

do { 1; }

Maybe there's a better name for these?

nicolo-ribaudo commented 3 years ago

"do expressions" are designed to be similar to "block expressions": you cannot use them to begin a statement.

// invalid
{ foo() {} };

// valid
({ foo() {} });

// invalid
do { 1 };

// valid
(do { 1 });
ghost commented 3 years ago

Oh, my apologies for not noticing that. Then as was already said, this problem wouldn't occur without attempting to put a do while in a rhs position.

mikestopcontinues commented 3 years ago

I do think the syntax is poorly named. I understand the usefulness of creating a temporary scope, but perhaps let x = run {'val'} would be less ambiguous?

pitaj commented 3 years ago

The reason for using do is that it's already a keyword which makes parsing the new syntax much easier.

Jack-Works commented 3 years ago

I don't think do is a good name to use. Actually it increase the burden. You can not use it as the start of ExpressionStatement cause it will conflict with do statement.

If we change to another keyword we can avoid this problem, no need to add ( ) outside.

pitaj commented 3 years ago

Why would want to use do at the start of an expressionstatement? Just use a block instead.

mikestopcontinues commented 3 years ago

@pitaj Parsing the new syntax for current JS programmers? Future JS programmers? Parsers? It has a very different use from do while, so I think it's likely do is more ambiguous for all three groups.

ljharb commented 3 years ago

The concrete advantage to using an existing keyword is that a “no line terminator here” restriction wouldn’t be necessary (to avoid ASI issues).

mikestopcontinues commented 3 years ago

@ljharb I was under the impression run and a bunch of other standard words were already marked as keywords, but had no use in JS. But I just looked at that list, and run isn't on it. What about eval { 'val' }? Since eval() is provided by the language, it would be easy to overload. It's a less ambiguous overload for the parser, and it also conveys to coders much more how the new syntax would work since it has the same behavior as eval().

ljharb commented 3 years ago

there’s no way anyone would want to make more uses of the word that shall not be named.

bakkot commented 3 years ago

@mikestopcontinues eval is also not a keyword. let eval = () => 0; eval('1'); /* 0 */ works fine.

mikestopcontinues commented 3 years ago

OK, I'll stand by run or some other new keyword. If the functionality isn't worth the trouble of a new word, is it really worth being added to the language?

pitaj commented 3 years ago

Adding a new keyword causes backwards compatibility issues, new keywords are very unlikely to be accepted.

pitaj commented 3 years ago

Especially something as common as run

ghost commented 3 years ago

What about how get and set are contextual keywords? eval could be the same: if it's followed by a '{', then it could create a block expression.

ljharb commented 3 years ago

@00ff0000red that's already valid syntax, due to ASI:

var anyNonKeyword = 3;
var foo = anyNonKeyword
{ /* this is a block */ }

that would break if anyNonKeyword { suddenly became "the result of the block", unless there was an NLTH restriction.

pitaj commented 3 years ago

That still causes ambiguous grammar when considered with other language features and automatic semicolon insertion.

const f = () => eval
{
  12;
}

Currently this is an arrow function declaration followed by a block. But it becomes ambiguous under your proposal. If you replace eval with do it is unambiguous.

hax commented 3 years ago

Personally I don't think ASI is very important in this specific case, even we use do, the mainstream coding style will be always do {(no newline between do and {), so there is no much difference in real-world practice.

Actually, the inconvenience of do expression can't be the start of ExpressionStatement may be worse:

foobar
do { ... } |> f  // syntax error!

So you are forced to write

foobar // <- ASI!
(do { ... }) |> f 

and introduce a real ASI hazard.

nuts-n-bits commented 3 years ago

Just spitballing.

"use expr";  // modifies current context so that if statements becomes if expressions
const conditional = if (age<21) { false } else { true }
// the if expression can be spec'ed elsewhere however you want

I feel like this is the most non-breaking way to introduce new expression syntaxes without overloading keywords or introducing new ones.

Jack-Works commented 3 years ago

Yeah, actually I think we don't have to stick on the do keyword. Why not a new one just like in pattern matching.

`match` <No LineTerminator here> Block

For example, we can use

`expr` <No LineTerminator here> Block
Jack-Works commented 3 years ago

And some of my friends usually think "do expression" is "do notation" (Haskell) in JS

bakkot commented 3 years ago

For example, we can use

`expr` <No LineTerminator here> Block

Yup, that's a possibility I'm considering. (If I go with that, I'll might also change async do { to just async {.)

Jack-Works commented 3 years ago

That also removes the limit of do expression cannot appear as an ExpressionStatement strat

bakkot commented 3 years ago

Technically true, though there is no reason to do that ever.

hax commented 3 years ago

Yeah I like the idea of expr {...}, and maybe we could just use eval {...}, so people could simply understand it as sugar for eval("{ ... }") (though we make the eval {...} more stricter to avoid some confusion).

ljharb commented 3 years ago

It's very much not sugar for that, so eval would be a very misleading name. eval's connotation is "string input", which is not the case here.

hax commented 3 years ago

Whether it's a sugar or not, is depend on the final semantic we choose. If the semantic is closed enough,let's say, if almost all do {CODE} could be replaced by eval("{CODE}") and return the same result except those throw syntax errors, it will be treated as sugar anyway by most people in the community.

eval's connotation is "string input"

I think it's just like the case of import x from mod and import(mod).


Note I only think eval {} is a possible solution if the semantic is closed enough. If not, it of coz should not use eval.

ghost commented 3 years ago

...we could just use eval {...}, so people could simply understand it as sugar for eval("{ ... }")

Sugar for eval... that legitimately sounds awful, and I would lose all faith in ECMAScript and TC39, should the members of TC39 go forward with that idea.

But let's not have a full blown debate about eval and it's semantics, those are not what are being proposed here.

ljharb commented 3 years ago

I very much don't agree that it would be treated as sugar. eval is bad, using eval is bad, and "turning a list of statements into an expression" is not bad and won't be lumped together with eval by anyone in the community.

Even if the semantics are identical, it would be hugely harmful for tc39 to in any way equate, or imply equation with, do expressions to eval.

Jack-Works commented 3 years ago

The eval function is bad because it is converting an arbitrary string into code. But if we take the literal meaning of eval, "evaluate a piece of code", that is totally not bad.

BTW I still like the idea of expr <No LineTerminator here> Block.

ljharb commented 3 years ago

@Jack-Works yes, the program evaluating code is indeed bad - only the engine should be doing that. do expressions are just a syntactic marker around a statement list that produced an expression value; equating this to eval doesn’t make any sense to me.

adrianhelvik commented 3 years ago

What about going for if-expressions instead? It seems like "ternary hell" is a large part of the reason why do-expressions are requested.

If-expressions are less powerful, but also less verbose. Implementating them should be relatively easy as they would be syntactic sugar fo the much hated ternary expressions.

And in either case, expression-like if-statements are needed to implement do-expressions. if-expressions could be a stepping stone for do-expressions.

const html = `
  <main>
    ${if (user) `
      <strong>Hello ${user.name}</strong>
    ` else `
      <strong>You need to sign in to use this service!</strong>
    `}
  </main>
`

The syntax error could be a bit confusing though.

const html = `
  <main>
    ${if (user) {
                ^ Maybe this should throw "SyntaxError: Object literals must be wrapped in parentheses in if-expressions"
      `
        <strong>Hello ${user.name}</strong>
      `
      ^ Or here with "SyntaxError: Unexpected template string" as this would be an object literal and not a block
    }}
  </main>
`

It wouldn't support highly scoped temporary variables. But IIFEs are not that complex and I do not believe this will be the primary use case for this proposal in real life. Can anybody who have used do-expressions extensively comment on this?

const unsubscribe = (() => {
  let value = null
  return onChange(nextValue => {
    if (value !== nextValue) {
      value = nextValue
      doStuff()
    }
  })
})()

vs

const unsubscribe = do {
  let value = null
  onChange(nextValue => {
    if (value !== nextValue) {
      value = nextValue
      doStuff()
    }
  })
}
ceigey commented 3 years ago

Are any of these options viable?

in do { ... }

do => { ... }

// I'm assuming these are NOT viable
// only because they steal useful future
// syntax opportunities from identifiers

as { ... }
from { ... }

As far as I know, all of these are currently SyntaxErrors.

But, the last two specifically I imagine have controversial implications; just thought I'd throw them out there because I like how they read.

ljharb commented 3 years ago

The arrow one also implies it’s a function body, which it’s not.

xp44mm commented 2 years ago

If the return result of do must be assigned to a variable. Can we redesign the syntax from the assignment statement, for example, using the following syntax.

let x := {
    1
}

// or `= get` as combine keyword
let x = get {
    1
}

If we want to only performs some side effects or hide some variables in local block, then we can replace the do with ignore.

ignore {
let x = 1;
console.log(x);
}

// or hack
if("print local x"){
let x = 1;
console.log(x);
}
xp44mm commented 2 years ago

do expression another usage case:

let x = true?(do {
1
}):0
ljharb commented 2 years ago

It doesn’t have to be assigned to a variable; that’s one of the whole points of it.

xp44mm commented 2 years ago

Using get {} instead of do {} is better both on semantic reading and on compiler design.

coolCucumber-cat commented 1 year ago

I think it's just like the case of import x from mod and import(mod).

import in import(mod) is still a reserved word, it just happens to look like a function. eval is just a function.