-
Notifications
You must be signed in to change notification settings - Fork 14
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
Why not just let statements return value? #39
Comments
second syntax definitely causes ambiguity: let y = 1;
let flag = {
x: y // k: v
} let flag = {
label: y; // statement with label
} |
The ambiguity issue can be solved by saying "labels are not allowed in blocks-as-expressions, any such ambiguity will be resolved as an object literal" |
This suggestion would break this valid code: function f() {
{
// this makes a scope
let a;
if (true) {
a = 2;
} else {
a = 3;
}
}
}
f() === undefined With your change, |
I might be missing something, but the suggestion doesn't include anything about implicit return values. For the behavior you're describing, there would have to be a |
@pitaj the suggestion was for curly braced blocks to be an expression; that’s what would break. Anything that can be a RHS or a return value also has to be an expression. |
ah, i see tho, you’re saying that the function wouldn’t have a return value without return, that’s true. In that case, consider |
I suppose your point is that such behavior would require many exceptions to be backwards-compatible. Another example might be an arrow function: const f = () => {
3;
};
f() === undefined;
// under this proposal
f() === 3; |
In other words, my overall response is “the explicit |
The most consistent way to make this backwards-compatible would be to say "any syntax involving a statement in the place of an expression that currently results in an error is now treated as a valid expression". That's not very satisfying though, and would result in some weird inconsistencies (like in the arrow function case). |
Thus, not the most consistent way. |
Right, I was speaking within the confines of "without using do". |
I want to talk about ambiguites and 'legacy code'. Here by 'legacy code' I mean any code before this proposal. Of course we shoudn't break any existing code. However we should think about cases when meaningful code coudn't be broken. There are should be rules when implicit return for last statement should be disabled. I think these rules are:
So every last expression in a block should not break above rules: no assignments, not function calls, no method calls. Then we have meaningful defaults: function f() {
{
// this makes a scope
let a;
if (true) {
a = 2;
} else {
a = 3;
}
}
}
f() === undefined```
-- returns undefined cause of assignment.
```js
const f = () => {
3;
};
f() === undefined;
// under this proposal
f() === 3; -- using constant as last expression is useless in legacy code, what will be broken do you think? Only some strange snippets which show that legacy code has no implicit return. If you have any proof of usefullness in production please show it. With above rules the following will work because it has no possible side effects: f = {
a + b < c * 10
}
g = {
x = Math.floor(11 / 3.3)
x
}
// |
Assignment already returns something, and is an expression (although most style guides discourage using assignment as an expression). |
I believe I am missing some background information - is there anything that makes the very first example undesirable or ambiguous? My understanding is that, currently, you cannnot assign a keyword-based control flow statement to a variable, or use them where an expression is required. So, I'm assuming (that's a dangerous word) that there is no ambiguity there, and thus allowing: let a = if (true) { 'yes' } else { 'no' }
console.log (if (false) { 'whoops' } ......) ..."should" be fine. The potential footguns are things like let a = if (true) 'true'
else 'false' Which I would expect parsed as two separate statements. Are potential footguns like that the reason for having the generalised |
If it can be used in an assignment, it can be used in any position an expression can be used, which includes on the next line after a statement that omits a semicolon |
That was largely the intent, treating if/else as an expression, but I presume from the way you worded that, that that kind of ambiguity isn't going to fly in the case of EcmaScript. |
I very much doubt it. |
Can't any ambiguity, as well as backwards compatibility concerns, be resolved by preserving the following 2 rules (in addition to the one mention by @pitaj about labels in blocks):
As far as I can tell these rules already exist. That is why you can't declare object literals in arrow functions like this: () => {
key: 'value'
} Instead you have to do this: () => {
return {
key: 'value'
}
} By preserving the above rules, then this doesn't break because the block result isn't returned: function f() {
{
// this makes a scope
let a;
if (true) {
a = 2;
} else {
a = 3;
}
}
}
f() === undefined This wouldn't break because the body of the function now consists of the literal const f = () => {
3;
};
f() === undefined; |
@cdow so will Anything that can be an expression has to be able to be used anywhere any expression can be, or else you end up with the unfortunate ambiguity in concise arrow function bodies (that i hope nobody wants to repeat or worsen) |
That case would be covered by @pitaj's suggestion:
You might be able to formulate a rule is less strict about labels but that one seems pretty straightforward. Going by that rule, |
and |
Thinking about this further, there doesn't appear to be any reason to limit expression |
As per @pitaj 's suggestion, |
Then it is a breaking change. Currently it evaluates to an object with an |
I hadn't considered that case. That is a good point. I think the |
That would mean the following is allowed: let x = if (foo) if (bar) 1; else 2; else 3 I vote for allowing Edit: where does the arrow end? const sign = x => if (x > 0) 1; else if (x < 0) -1; else 0; console.log(sign(42));
// vs
const sign = x => if (x > 0) { 1 } else if (x < 0) { -1 } else { 0 }; console.log(sign(42)); Edit2: ...or simply allow |
I'm not sure I understand why this is a problem:
What is the concern beyond the currently allowed:
I would think that the arrow would end right before the
|
Because it adds semicolons which aren't actually ending the statement. Having semicolons inside expressions is weird |
@cdow. In the following program: if (x > 0) 1; else if (x < 0) -1; else 0; console.log(42); the semicolon after if (x > 0) { 1; } else if (x < 0) { -1; } else { 0; } console.log(42); That means that you would have to write: const sign = x => if (x > 0) 1; else if (x < 0) -1; else 0;; console.log(sign(42)); with two semicolons before |
I'm not sure these statements are actually true. Otherwise why is this valid JS (at least according to Chrome and Firefox consoles): if (x > 0) 1; else if (x < 0) -1; else 0; console.log(42); But this isn't: if (x > 0) 1; else if (x < 0) -1; else 0 console.log(42); If what you are saying was correct, shouldn't we have to already write: if (x > 0) 1; else if (x < 0) -1; else 0;; console.log(42); |
Consider this expression: const x = do { if (a) 0; }; console.log(1); There are not unnecessary semicolons (ie "empty statements". If we remove the parentheses, it keeps both the semicolon which ends const x = do if (a) 0;; console.log(1); It might be clearer with parentheses: const x = (do if (a) 0;); console.log(1); If I remove a semicolon, it becomes the equivalent of const x = (do if (a) 0;) console.log(1); or const x = do { if (a) 0; } console.log(1); |
I understand the reasoning for why one would expect to need two semicolons. My point is JS appears to already have support for using a single semicolon to end multiple things at once. Can't we leverage that same logic for For example, @nicolo-ribaudo, you can do your same reduction, in current JS, with just an if (a) { 0; }; console.log(1); If you remove the curly brackets you get: if (a) 0;; console.log(1); However, you can also remove one of the semicolons and it is still valid: if (a) 0; console.log(1); Does the above semicolon end the |
That's because the second semicolon in your first example is unnecessary. In your first example you have 3 statements:
You can safely remove the empty statement, since they have absolutely no meaning: if (a) { 0; } console.log(1); The following statements don't have a closing semicolon:
While these do:
In my first example ( |
Thanks, @nicolo-ribaudo, that cleared up my confusion. I guess that also explains why arrow functions without curly brackets don't allow semicolons. Couldn't you apply the same rules about semicolons to expression control structures? Wouldn't that clear up any issues with multiple semicolons? |
It's different: arrow functions without curly brackets don't allow statements, but only expressions. |
Shouldn't expression control structures without curly brackets only allow expressions as well? |
Yes, that would solve the semicolon problem. But by limiting them as such, I don't think that they would bring additional value to the language. const color = if (dark) "black" else "white";
// ->
const color = dark ? "black" : "white";
const name = if (person) person.name;
// ->
const name = person && person.name; Even is all the example in this thread use trivial example, the usecase for this proposal is to allow more complex program flows to be represented as expressions, and disallowing statements goes in the opposite direction. |
I don't think we should limit
|
For me, one of the major use cases for (Another fun example: let x = {
foo()
{
bar()
}
} is currently legal code: it makes an object with a As such, I think |
How about using unary
let x = if (true) { = 1 } else { = 2 }; It would be required to be the last expression in a block: let result = {
let tmp = fn();
= tmp * tmp;
} |
How would that interact with ASI and more statements in the do expression? |
@kornelski The problem with that is that you really want to know at the beginning of the block whether it's going to be an object or a block: let result = {
if (foo) {
// arbitrarily complicated stuff here
}
} is already legal (it creates an object with a method named let result = {
if (foo) {
// arbitrarily complicated stuff here
}
= 1
} (presumably an expression-block containing an |
@bakkot, I think you are right that turning statements in to expressions (
But you couldn't do:
However if you could create
Then you could do something like this:
|
I think @cdow is onto something. If our goal is to make Javascript more expression oriented, the most straightforward path would be to just provide expression versions of if-else and try-catch. I made a post here talking about that, and they rightfully linked me here, because this is a very similar idea. But, instead of having a do block, we could just have Here's some examples: const val =
do if (count === 0) "none"
else if (count === 1) "one"
else if (count === 2) "a couple";
else if (count <= 5) "a few"
else "many"
const result =
do try (
readFile('./myFile.txt')
) catch (err) (
err instanceof FileNotFoundException
? null
: throw err // I'm relying on the "make throw an expression" proposal here
) To have multiple statements, I'll use the made-up IIFE syntax const x =
do if (condition) iife {
const a = 2
return a + 3
} else 3 |
I mentioned in my other post how I like the idea of using do blocks to make "mini" scopes, in which I can initialize a value without introducing intermediate variables into the parent scope, to which @ljharb responded:
In the interest of moving the conversation here, I'll respond to that here. Curly brackets do work to make a scope (I use them), but they don't work well. For example: // Compare:
const x = iife {
const a = whatever()
const b = whatever()
return a + b
}
// with this:
let x
{
const a = whatever()
const b = whatever()
x = a + b
} You're forced to use a let binding, and it's less obvious looking at the do block that the purpose is to initialize the value of x. The other advantage to IIFE syntax is that we can use statements in expression positions, the same side-advantage do blocks have, but implemented in a much less complicated way. (The only thing we wouldn't get is the ability to use await, continue, break, etc inside the IIFE - which if that's also a motivating reason for the existence of do blocks, maybe that can be added in the README's "motivation" section - right now, the "do if" and "do while" will completely solve the motivation stated there in a simpler way). |
IIFEs, even with lighter syntax, don't address the need for a nested scope because they come with other baggage: most grievously, they do not allow you to So we really do need some syntax which just introduces a nested scope, and allows you to get a value out of it, without being an actual IIFE. And the simplest way of getting a value out of the nested scope is to just let you use the completion values. (Using I agree the README needs to be updated to talk more explicitly about why IIFEs are not adequate. |
Thanks @bakkot I do think we're still sort of mashing two different ideas into one proposal though. Here's are two separate goals:
That first scenario is best solved by simply providing an expression version of "if" and "try", similar to how most languages solve that issue. When coding, there shouldn't be a need to make a do block just to change an if to an expression-if - that's a lot of work to do one of the basic founding ideas of this proposal. The second scenario is now free to be solved without these issues that the current proposal currently has:
...
...
f()
g()
} else {
h()
... Later on, you realize this giant if-else was the last statement of a do block, and g()'s result was actually a completion value, it was not just a side-effect producing function. All of these issues can be easily solved by requiring an explicit completion value marker (e.g. using a keyword Aside: I don't want to drag the conversation of explicit completion value markers too much into this thread - it's already being discussed at length over here, so any thoughts on that side of things are welcome there. I merely want to point out that there are alternative formulations to the current proposal that are much simpler if we do have |
I ended up taking my Feel free to take a peek at it and leave any feedback. |
Statements in many Languages can return value directly, for example, Rust:
So why we need the keyword
do
?Even
{...}
is better thando {...}
These syntaxes will not cause ambiguity. Am I right?
The text was updated successfully, but these errors were encountered: