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

Are braces required? #1

Open
domenic opened this issue Sep 13, 2017 · 34 comments
Open

Are braces required? #1

domenic opened this issue Sep 13, 2017 · 34 comments

Comments

@domenic
Copy link
Member

domenic commented Sep 13, 2017

We had some discussion in IRC and there are arguments for both sides. E.g. it is nice to be able to just do

let x = y ? z : do throw new Error("foo");

(this largely obviates throw expressions in my opinion)

But others pointed out that cases involving e.g. if are pretty confusing.

I'm curious to see where the champion comes down on this :)

@dherman
Copy link
Member

dherman commented Sep 13, 2017

Thanks for registering this one. I'm not ready to come down on either side of this one just yet. :)

@phaux
Copy link

phaux commented Sep 14, 2017

It would be nice to be able to write

const json = do try { JSON.parse(data) } catch (err) { ({status: 500}) }
const message = do if (json.status == 200) { "success" } else { "error" }

@yuchi
Copy link

yuchi commented Sep 14, 2017

The main point in favor of removing braces from do expressions are JSX interpolations, of course:

function MagicButton(props) {
  return (
    <button>
      {do if (props.useIcon) <MagicIcon />}
      {props.children}
    </button>
  );
}

function MagicList(props) {
  return (
    <ul>
      {do for (let incantation of props.incantations) {
        (<li>{incantation.name}</li>);
      }}
    </ul>
  );
}

(I’m trying to simulate prettier output here)

But more generally as prettier or similar is adopted and enforced in projects having “useless” braces makes the feature less appealing:

function SomeComponent(props) {
  const onClick = do if (props.disabled) { props.onClick; } else { null; };
  return <MagicButton onClick={onClick} />;
}

With braces and prettier formatting would result in

function SomeComponent(props) {
  const onClick = do {
    if (props.disabled) {
      props.onClick;
    } else {
      null;
    }
  };
  return <MagicButton onClick={onClick} />;
}

@yuchi
Copy link

yuchi commented Sep 14, 2017

One main argument in favor ok keeping braces is clearance (IMHO) of semantics. Braces have major visual impact and when formatting is enforced has even more estate to help.

But, still IMHO, this gives no actual help since an if statement in an expression position is usually way enough to signal the presence of a do-expression.

This line could not be confused with anything else:

const type = do if (value === 42) { 'answer'; } else { 'not-answer' };

@allenwb
Copy link
Member

allenwb commented Sep 14, 2017

This line could not be confused with anything else:

const type = do if (value === 42) { 'answer'; } else { 'not-answer' };

but this one could:

if (oracle()) do if (value === 42) { return 'answer'; } else {return 'not-answer' };

@allenwb
Copy link
Member

allenwb commented Sep 14, 2017

what about:

let foo = [do 3+4, 5+6];

is foo [7,11] or [11]?

similarly,

f(do 1,2)

How many arguments are passed to f?

@scripting
Copy link

I find these discussions fascinating.

I would err on the side of consistency with previous constructs, assuming there's a new construct here.

@bmeck
Copy link
Member

bmeck commented Sep 14, 2017

let foo = [do 3+4, 5+6];

I'd err on how [()=> 3+4, 5+6] handles it. So, [ 7, 11 ]. Arrow function handling gives an existing framework on how dealing with , should work I think.

if (oracle()) do if (value === 42) { return 'answer'; } else {return 'not-answer' };

I am not clear on a simple way to look at this, but I feel like it has a similar answer to how:

do {
}
while (true);

Needs to remain unambiguous. That while(...) needing to be attached to the do {} leads me to think the else above might make more sense being attached to the inner if. This would also match:

if (true) if (false) {c();} else {d();}

calling d();

@bakkot
Copy link
Collaborator

bakkot commented Sep 14, 2017

@yuchi, off topic, but

function MagicList(props) {
  return (
    <ul>
      {do for (let incantation of props.incantations) {
       (<li>{incantation.name}</li>);
      }}
    </ul>
  );
}

does not do what you want - you'll only end up getting the last incantation.name.

(This type of misunderstand does make me a little worried about this feature - very few JS developers today are familiar with completion values, and rightly so.)

@tabatkins
Copy link

@bmeck The fact that we can come up with an answer for these does not inspire me with confidence that we should come up with an answer for them. ^_^ Braceless-if is, except in a small number of cases, a huge mistake that has caused untold millions of dollars worth of damage thruout its history. More braceless-if-a-single-statement constructs need a pretty strong motivation, I think; stronger than just "it's possible to define a consistent grammar".

In particular, if we really want throw expressions as easily as possible, just allowing braceless do throw might be a worthwhile way forward, while requiring braces for everything else. (I'm not convinced it's worthwhile even for that; braces for everything works for me.)

@allenwb
Copy link
Member

allenwb commented Sep 15, 2017

I'd err on how [()=> 3+4, 5+6] handles it. So, [ 7, 11 ]. Arrow function handling gives an existing framework on how dealing with , should work I think.

But it actually doesn't. Since the whole point of do-expression is to allow JS statements and statement lists to be used as expression elements, bracketless do-expressions would presumably be defined using a grammar rule like this:

DoExpression : do Statement

where DoExpression itself is defined as a RHS of some Expression component production. Probably PrimaryExpression.

But the definition of Statement, includes:

Statement : ExpressionStatement
ExpressionStatement : Expression ;

(I've simplified these BNF definitions by leaving out little details that aren't relevant to this issue.)

So, when parsing something like do 3+4, 5+6 Expression says what follows the do will be recognized as an ExpressionStatement consisting of a comma operator whose left operand is 3+4 and whose right operand is 5+6. Similar do ()=>3+4, 5+6 is also recognized as a comma operator with the arrow function as the left operand and 5+6 as the right operation. Essentially, all coma separated expressions to the right of do will be recognized as part of a single ExpressionStatement. Any intuition derived from the precedence of arrow function bodies would be wrong.

But there is actually a further complications. Notice that semicolon at the far right of the definition for ExpessionStatement. That says that an ExpressionStatement must end with a ;. In fact all statement forms end with an explicit ; except for those statements that are defined to end with an explicit } or another embedded Statement,

So, both of these statements should cause syntax errors.

let foo = [do 3+4, 5+6];
f(do 1,2);

You would have to write them as:

let foo = [do 3+4;, 5+6];
f(do 1;,2);

or, depending on what you actually intended

let foo = [do 3+4, 5+6;];
f(do 1,2;);

or you could just use {} and you won't need any extra semi-colons:

let foo = [do {3+4}, 5+6];
f(do {1,2});

(for why, see the rules for ASI. It's easy to forget that ASI isn't just about end-of-line semicolons.)

I suspect that even if we allowed bracketless do-expressions various communities would end up with style guidance that says some like: To avoid syntactic confusion and errors, always enclose the statement part of a do-expressions with { }

@yuchi
Copy link

yuchi commented Sep 15, 2017

@bakkot Totally right. I honestly misunderstood the actual scope of the proposal. As an old coffee-scripter I inferred (wrongly) that do-expression would bring full statements-as-expressions in the language. This is indeed instead a proposal to bring completion values instead (the REPL or eval results if I’m not wrong again).

@allenwb Sorry if it sounds stupid, but could explicit grammars be made for statements that can have a {} after them? So do if, do for, do while and similar actually trigger a do expression but do 12 does not.

@Jamesernator
Copy link

@bakkot Completion values can be weird sometimes, like the fact that for-of's completion value is the last evaluated expression rather than the { done: true, value } value or the completion values of all iterations is something that always surprised me, this is one of the reasons I had the idea (see option 2) that do would be almost equivalent to an IIFE but also automatically returns the completion value instead of undefined.

By having them as IIFE's would open possibility for generator based ones as well e.g. the example from before could become:

function MagicList(props) {
  return (
    <ul>
      {do* for (let incantation of props.incantations) {
       (yield <li>{incantation.name}</li>);
      }}
    </ul>
  );
}

I think this should be split into a separate issue though so I'll make one.

@allenwb
Copy link
Member

allenwb commented Sep 15, 2017

@yuchi In theory we can define any syntax we want for each unique context. But from a human factors perspective it would be a terrible language design to have logically equivalent "statements" whose syntax differed solely based upon the usage context.

@claudepache
Copy link

Assuming that braceless do-expression syntax is do <statement>, the declaration

let x = y ? z : do throw new Error("foo");

is valid only because of ASI; otherwise you’d have to write:

let x = y ? z : do throw new Error("foo");;

Worst case is when the programmer will scratch their head wondering why

let x = y ? z : do throw new Error("foo");
['foo','bar'].forEach(f);

doesn’t work as expected.

@pitaj
Copy link

pitaj commented Dec 4, 2017

This is related to #9 and #11, if common statements like if, try, etc are converted to expressions, which I can't think of any way that would be breaking, then you wouldn't need this shorthand do syntax at all, instead of do if (thing) { stuff(); } you could just have if (thing) { stuff(); }.

I think that's the preferable case. Relieves the need for extra keywords but still allows multi-step do expressions.

@ljharb
Copy link
Member

ljharb commented Dec 4, 2017

I don't think that's preferable; I prefer the explicit indication that different rules are applying.

Separately, currently:

if (true) { ({ a: 2 }) }
['a'] // yields `['a']`

However, if if suddenly became a statement, then it would function like this:

eval('if (true) { ({ a: 2 }) }')
['a'] // yields `2`

Thus, it would, in fact, be breaking, thanks to ASI.

@Jessidhia
Copy link

Yeah, I expect that the biggest difficulty would be dealing with ASI.

However, it should be possible to forbid the "expression if" and similars from being a primary expression, no?

@pitaj
Copy link

pitaj commented Dec 4, 2017

If it is absolutely breaking, that is, there's no way around it like what @Kovensky suggested, then why are those issues being left open?

@ljharb
Copy link
Member

ljharb commented Dec 4, 2017

@Jessidhia I'm both not sure it'll be possible; and also I don't think it's worth it. do imo is a critically necessary syntactic marker for statements-as-expressions.

@claudepache
Copy link

No, it is not the fault of ASI. It is because a block does not have a semicolon terminating it. Today, the following is valid and evaluates to ['a']:

if (true) { ({ a: 2 }) } ['a'];

@pitaj
Copy link

pitaj commented Dec 4, 2017

Seems like that could be fixed by making it so statements as expressions are only evaluated as such where there would currently be a syntax error: in the right hand side of arrow functions, variable initiation, and inside a pair of parenthesis, etc

@jkrems
Copy link

jkrems commented Jul 22, 2020

One random note because of a Twitter thread: If braces are optional, would it imply that there's no block scope for the do expression if it skips the braces? In other words, would the following work:

do let x = 42;
x === 42; // true

@ljharb
Copy link
Member

ljharb commented Jul 22, 2020

I think it would indeed be very confusing if there was a block scope where there were no block boundaries (curly braces).

@BasixKOR
Copy link

How about defining an alternative form that accepts common use case like IfStatement, ThrowStatement, TryStatement etc? It isn't an elegant solution, but it would be able to cover common use cases without heading into ASI or comma operator issues.

@theScottyJam
Copy link

theScottyJam commented Jun 30, 2021

I think there are really only two use cases for do expressions without braces: if and try. Maybe "throw" too if this proposal to make "throw" become an expression doesn't go through. Switch would be the other useful one, but it'll just be superseded by Match, when pattern-matching comes out.

This seems like a great idea, and there's similar discussion for it going on here.

@theScottyJam
Copy link

There seems to be a lot of talk about how if we had a "do " semantics, then we would also be required to place an extra semicolon at the end of the chosen statement, e.g. like this:

let x = y ? z : do throw new Error("foo");;

Does it really have to be this way though? I don't know much about the Javascript grammar, but this feels more like an artifact of how the grammar is currently defined, more than a hard requirement. Shouldn't it be possible to say that when you're using the shorthand form do expression, and you're placing a single statement afterward, it should not expect a terminating semicolon to end the statement? (and in fact, I would argue that such a semicolon should even be made illegal, the let foo = [do 3+4;, 5+6]; example looks really weird.

Can anyone think of any potential parsing ambiguities that could come if we forbid the semicolon after the do-shorthand statement?

@nicolo-ribaudo
Copy link
Member

The syntax can always be made unambiguous, but not how humans read code. As mentioned earlier in this thread, the specific example you mentioned would be parsed differently if you remove ; (it would be parsed as [do (3+4, 5+6)]).

@theScottyJam
Copy link

theScottyJam commented Aug 14, 2021

I don't see why it would or should. The trick seems to be to make the "do" not so greedy so it doesn't try and consume as much as possible. It will need some sort of precedence, even when a statement is used in its shorthand form. If we can give it less precedence so as to not have it consume comma operators or comma delimiters, then everything should check out, and work in an intuitive way.

do x + 2, y + 3
// same as
(do x + 2), y + 3
// just like how `() => x + 2, y + 3` == `(() => x + 2), y + 3`

[do x + 2, y + 3]
// same as
[(do x + 2), y + 3]
// just like how `[() => x + 2, y + 3]` == `[(() => x + 2), y + 3]`

@theScottyJam
Copy link

theScottyJam commented Aug 14, 2021

Some more thoughts on how the do shorthand could work:

Firstly, what if we just made it so the do shorthand only operates on statements, not expressions? There's no reason to have it work on expressions, so we could just make it a syntax error. If we want to allow it, let's just make sure it's done in a way that makes the "do" act like a no-op, 100% of the time. The "do" should never affect the meaning of the expression, however we implement this logic, so [do x + 2, y + 3] should always be interpreted as [x + 2, y + 3]. If you must, you can think of do as being a unary operator with an extremely high precedence, whose completion value is the same as whatever it receives. So [do x + 2, y + 3] is the same as [(do x) + 2, y + 3] - but this concept should only extend to the case when expressions are placed after do, when do has a statement afterwards, it's probably better not to think of do as an operator, rather, it's just part of the syntax of that statement-turned-into-an-expression. Scratch that, that would cause some odd behaviors. When a do block operates on an expression, do itself should act like an operator with the same precedence of, say, yield. When it operates on a statement, it should act more like part of the statement-turned-into-an-expression syntax rather than an operator.

Next, let's make each statement have its own precedence. This precedence does not do anything unless the statement is placed in a do expression. When this happens, the statement will "consume" as much as its precedence allows. For example, in do throw 2 + 3, 4, if we give throw the same precedence as yield, this would be interpreted as (do throw 2 + 3), 4.

In general, I would recommend that we give all of the statements the same precedence as yield - I can't think of any reason why we would do anything different. Here's some more examples:

do if (false) 2 else 3 + 3, 3
// same as
(do if (false) 2 else 3 + 3), 3

do return 2 + 2, 3
// same as
(do return 2 + 2), 3

[do throw x + 2, y + 3]
// same as
[(do throw x + 2), y + 3]

// I vote we just disallow declarations, the same way they're not allowed when
// you do `if (true) let x = 2`. But if we allow them, we should probably make any
// following commas be interpreted as further declarations.
do let x = 2, y = 3
// same as
let x = 2, y = 3
// (i.e. that comma is not a comma operator)

@claudepache
Copy link

@theScottyJam That would introduce distinct syntax and precedence rules for throw, return, if, etc. when used in statement or in expression position, which is technically possible, but might cause confusion.

For example:

if (false) throw 1, 2; else return 3, 4;
// equivalent to:
if (false) { throw (1, 2); } else { return (3, 4); }

Under your proposal, when you prepend it with do, in order to keep the semantics, you must add parentheses and remove semicolons:

do if (false) throw (1, 2) else return (3, 4)

@pitaj
Copy link

pitaj commented Aug 14, 2021

I think the ambiguity can be solved by restricting the do shorthand to only work on block statements like if and try. You could even restrict it further to require that do if must use braces when that is currently optional.

Allowed

do if (x) {
  3
} else {
  4
}

do try {
  f()
} catch {
  def()
}

Not allowed

do let x = 5

[do 4+1, 7]

do if (x)
  3
else
  4

do throw err

do return x

do switch (c) {
  ...
}

@theScottyJam
Copy link

theScottyJam commented Aug 14, 2021

Good call @claudepache

I guess another option would be to give all do + statements have a precedence lower than the comma operator, like statements already do in a way.

Another option would be to make a comma operator without parentheses be a syntax error, as is being explored in the throw expressions proposal here.

@mrjacobbloom
Copy link

mrjacobbloom commented Dec 30, 2022

I see some discussion here of do let as an interesting yet useless combination of features, so I'd like to share a use case I've been toying with. I've been looking for a way to sidestep the "parallel arrays" required by Promise.all, and if this were combined with the async do-expressions proposal then you could potentially declare and set variables inside of Promise.all using something like async do let:

async function initialize() {
  await Promise.all([
    async do let foo = (await request('foo.json')).data, // Ignoring the comma precedence issue for the sec
    async do let bar = (await request('bar.json')).data,
    async do let baz = (await request('baz.json')).data,
  ]);
  render(foo, bar, baz);
}

...which feels a little cleaner to me than doing the same thing today with IIAFE's and early declaration:

async function initialize() {
  let foo, bar, baz;
  await Promise.all([
    (async () => { foo = (await request('foo.json')).data })(),
    (async () => { bar = (await request('bar.json')).data })(),
    (async () => { baz = (await request('baz.json')).data })(),
  ]);
  render(foo, bar, baz);
}

I'll admit that the 1 line of declarations saved isn't a huge deal, and async do let is too verbose and hard to remember. But still, this is a case where do let isn't completely meaningless.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests