tc39 / proposal-do-expressions

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

Violating TCP? #33

Closed JHawkley closed 5 years ago

JHawkley commented 5 years ago

Based on what I've seen of the Babel plugin and the examples provided here, I feel that the do expression is causing a massive violation of Tennant's Correspondence Principle. Let me go through an exercise to try and explain my concerns.

The proposal currently asserts the following: do { <expr>; } is equivalent to <expr>

So, then these two blocks should evaluate to the same value. do { if (true) "foo"; else "bar"; } (if (true) "foo"; else "bar";)

But they do not. Refactoring the contents of the do out changes the behavior of the if-else statement. Inside the do it evaluates to "foo" while outside the do it becomes a syntax error.

This would mean that: do { if (<cond>) <a>; else <b>; } is not equivalent to if (<cond>) <a>; else <b>; ...and that is a contradiction of the original assertion.

Am I just misunderstanding something here?

jridgewell commented 5 years ago

(if (true) "foo"; else "bar";) is a syntax error because if-statements are not expressions. This is the reason we're creating do-expressions.

But, if (true) "foo"; else "bar"; evaluates to "foo". We're also aware of other TCP violations, there should be open issues for them.

bakkot commented 5 years ago

Yes, specifically:

The proposal currently asserts the following: do { <expr>; } is equivalent to <expr> So, then these two blocks should evaluate to the same value. do { if (true) "foo"; else "bar"; } (if (true) "foo"; else "bar";)

Because if (true) "foo"; else "bar"; is not an expression, the claim you've highlighted from the readme does not apply.

JHawkley commented 5 years ago

So, then why should if-else suddenly act like an expression in a do block?

bakkot commented 5 years ago

I'm not what you mean by "act like an expression".

JHawkley commented 5 years ago

The if-else construct is a statement and not an expression. It shouldn't evaluate to anything, whether in a do block or not. According to the claim I highlighted from the readme, using if-else in a do as done in the examples I provided should be an error, since it is not an expression.

So, am I to understand that this proposal is actually to create a special dialect of ECMAScript that only exists between the brackets of a do block? A dialect where the various flow-control structures are expressions instead of statements?

ljharb commented 5 years ago

@JHawkley it already does; see eval("if (true) 'foo'; else 'bar';").

JHawkley commented 5 years ago

The code I provided was not intended to be executed in the context of eval(); the specialized behavior of eval() should not apply. When executed in a normal context, ECMAScript does not behave the way it does in eval().

ljharb commented 5 years ago

"should not apply" seems like an arbitrary designation; eval is part of the language.

JHawkley commented 5 years ago

eval() is a function. It is defined within the language.

bakkot commented 5 years ago

According to the claim I highlighted from the readme, using if-else in a do as done in the examples I provided should be an error, since it is not an expression.

No, the claim you highlighted does not say anything about statements, positive or negative. But it is immediately followed by one:

(do { <stmt> };) equivalent to { <stmt> }

which holds.

So, am I to understand that this proposal is actually to create a special dialect of ECMAScript that only exists between the brackets of a do block? A dialect where the various flow-control structures are expressions instead of statements?

They continue to be statements. They have completion values, which is already the case, but are not usable in expression position. But more broadly, it is true that the goal of this proposal is to allow you to use statements in expression position by wrapping them in do, yes.

eval() is a function. It is defined within the language.

Separately, no, eval is magic and cannot be defined within the language; it must be a core part of it, although its magic isn't really the relevant part here.

JHawkley commented 5 years ago

No, the claim you highlighted does not say anything about statements, positive or negative.

Indeed. But a statement is still not an expression, so the current behavior of statements within a do is an assumption and that assumption goes against how the rest of the language operates. Is that not a problem?

(do { <stmt> };) is equivalent to { <stmt> }

I wasn't surrounding my do in parenthesis. Why would this assertion apply?

Separately, no, eval is magic and cannot be defined within the language; it must be a core part of it, although its magic isn't really the relevant part here.

To be clear, my argument is: eval executes ECMAScript code differently from the way the language actually specifies it should be executed. eval's behavior of evaluating a statement to its completion-value is not part of the semantics of the language itself.

The existence of eval may be specified by the language, but its behavior is a special-case. The whole rest of the language does not work as it does in an eval() context.

Using this one thing to justify the behavior of do while the rest of the language screams "no!" seems foolish. And it still wouldn't correct the TCP violations. In fact, this behavior would be their cause.

ljharb commented 5 years ago

is not part of the semantics of the language itself.

Yes, it is; the spec includes Completion Records from evaluating every statement; it's just that it's currently only exposed for non-expressions in eval.

JHawkley commented 5 years ago

Yes, the spec includes the notion of completion values. But the issue is that other part: "it's just that it's currently only exposed for non-expressions in eval."

Developers don't build their applications in an eval context, though, so the behavior of code in that context would not be familiar to them. And to have a block where the semantics and behavior of the language suddenly change is irresponsible. This is why TCP is touted as a big deal in these proposals; to avoid bad language design like this.

Why not just expose completion-values for non-expressions everywhere? If the different semantics are applied everywhere, there is no TCP violation. As was already stated elsewhere, we don't need do if we do this.

There could probably be a feature for opting in to it (and other breaking language features) with something like an import evalLanguageFeatures at the top of your source file. That would keep the proposal from breaking existing code on the internet, and we would get something better than just do.

The only other thing I would be comfortable with is to just formalize IIFEs. These are already the idiomatic way to solve the problem that do is trying to solve anyways.

It isn't as deep into expression-oriented programming as some people would like, but the language would continue to work the same and all the semantics and behaviors that a developer is already familiar with remain unchanged.

All the problems with implementing the current do syntax go away when you just make everything work the same way arrow-functions already work, and having to type return isn't that bad.

Something like:

const input = 2;
const command = do => {
  switch(input) {
    case 1: return "run";
    case 2: return "jump";
    default: return "idle";
  }
}

This would be a very familiar syntax for any developer familiar with arrow-functions. And to avoid function-call overhead, you just assert that the body of the do => must be inline-optimized in the proposal.

But, this discussion has so far not convinced me that the current proposal would be a good change for ECMAScript. I definitely want something like this; I'm a Scala developer so I know how nice expression-oriented programming is, but the current implementation feels too hazardous. The do block is just too "special".

One of these two options would be much more responsible.

devsnek commented 5 years ago

The point you bring up is valid, but not a violation of TCP. do expressions do not change any semantics of the surrounding language. It's literally like Return the result of evaluating BlockStatement.

The real issue you're having is that to most users of JS, statements have no "return" value. But even in this case, it has always been exposed in one way or another. The language has eval and Realm.eval, node has vm, etc.

JHawkley commented 5 years ago

Aye, I now agree. After analyzing it for a day, I realize my misunderstanding.

Since I no longer consider this an issue, I suppose I'll close this.

However, I did come to some new conclusions and wrote up a document describing them. I'll put these into a new issue, though. It's a long read, but I hope it will provide useful information.