proposal-do-expressions icon indicating copy to clipboard operation
proposal-do-expressions copied to clipboard

Are braces required?

Open domenic opened this issue 8 years ago • 34 comments

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 :)

domenic avatar Sep 13 '17 01:09 domenic

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

dherman avatar Sep 13 '17 05:09 dherman

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" }

phaux avatar Sep 14 '17 11:09 phaux

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 avatar Sep 14 '17 14:09 yuchi

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' };

yuchi avatar Sep 14 '17 14:09 yuchi

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 avatar Sep 14 '17 15:09 allenwb

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?

allenwb avatar Sep 14 '17 17:09 allenwb

I find these discussions fascinating.

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

scripting avatar Sep 14 '17 18:09 scripting

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();

bmeck avatar Sep 14 '17 21:09 bmeck

@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.)

bakkot avatar Sep 14 '17 22:09 bakkot

@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.)

tabatkins avatar Sep 14 '17 23:09 tabatkins

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 { }

allenwb avatar Sep 15 '17 00:09 allenwb

@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.

yuchi avatar Sep 15 '17 07:09 yuchi

@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.

Jamesernator avatar Sep 15 '17 08:09 Jamesernator

@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.

allenwb avatar Sep 15 '17 14:09 allenwb

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.

claudepache avatar Sep 22 '17 23:09 claudepache

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.

pitaj avatar Dec 04 '17 00:12 pitaj

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.

ljharb avatar Dec 04 '17 01:12 ljharb

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?

Jessidhia avatar Dec 04 '17 01:12 Jessidhia

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?

pitaj avatar Dec 04 '17 01:12 pitaj

@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.

ljharb avatar Dec 04 '17 05:12 ljharb

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'];

claudepache avatar Dec 04 '17 05:12 claudepache

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

pitaj avatar Dec 04 '17 13:12 pitaj

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

jkrems avatar Jul 22 '20 15:07 jkrems

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

ljharb avatar Jul 22 '20 18:07 ljharb

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.

BasixKOR avatar Jun 30 '21 12:06 BasixKOR

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 avatar Jun 30 '21 14:06 theScottyJam

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?

theScottyJam avatar Aug 14 '21 03:08 theScottyJam

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)]).

nicolo-ribaudo avatar Aug 14 '21 08:08 nicolo-ribaudo

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 avatar Aug 14 '21 13:08 theScottyJam

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)

theScottyJam avatar Aug 14 '21 15:08 theScottyJam