Closed dustbort closed 3 years ago
Doesn't this just provide another way to write an immediately invoked function?
No. Among other differences, you can use await
within the do
, which you can't do in an immediately-invoked function. That is, this works:
async function f() {
const x = do {
const tmp = await foo();
tmp * tmp
};
return x;
}
but this doesn't:
async function f() {
const x = (() => {
const tmp = await foo();
return tmp * tmp
})();
return x;
}
(You could use an async arrow instead, and await
it, but that involves an extra microtask tick and even more syntax.)
The main limitation I have noticed is that variable bindings are not permitted inside a comma-operator expression
The comma operator only allows expressions; it does not allow you to write a try-catch
or if
or any other sort of statement. The point of this proposal is to allow you to put statements in expression position. They're not very closely related, that I can see.
@bakkot Thanks for the info about async. But, in terms of semantics, why introduce the concept of statements as expressions and omitting the final semicolon? I love that about Rust, but is it necessary to address in this proposal? I saw some other issues related to early returns, and it seems easier to understand in terms of IIFE's syntax for the body, instead of new syntax.
Statement as expression is one of the primary motivations behind this proposal. Without that, it's just a normal block.
why introduce the concept of statements as expressions
Well, that's basically the whole proposal. Many people find expression-oriented code clearer and cleaner: it allows you to group your code in a simpler way, so that a temporary variable or a try-catch
(for example) which are only used during the initialization of a single variable can be scoped to that initialization, as in
const x = do {
const tmp = Math.random();
tmp * tmp
};
or
const x = do {
try {
JSON.parse(blob)
} catch {
null
}
};
instead of having to write your code in a way which obscures the structure, as in
const tmp = Math.random();
const x = tmp * tmp;
// tmp is still live here; a reader needs to read the rest of the block to know it's not actually used for anything else
or
let x;
try {
x = JSON.parse(blob);
} catch {
x = null;
}
// note that `x =` is now duplicated, and `x` cannot be const.
and omitting the final semicolon
I'm not sure where you're getting this from. I've been omitting the final semicolon as a matter of style, but it's strictly stylistic; this proposal doesn't say anything about semicolons.
OK, got it. So I can wrap my entire program in do
to get Rust semantics. Eventually we can make do
the default. j/k
@bakkot Rust treats a final semicolon as returning unit () instead of the statement value. It's helpful to pinpoint one way or the other.
I'm familiar with Rust's semantics. This proposal does include anything along those lines.
If you have to put do
to make the block an expression, then wherever your "last" statement is, you can put return in front of it. Then use IIFE semantics.
Can you clarify if this is allowed?
do {
const name = if (customer) {
customer.name
} else {
"unknown"
};
}
If you have to put do to make the block an expression, then wherever your "last" statement is, you can put return in front of it. Then use IIFE semantics.
That is a thing we could do, yes.
Can you clarify if this is allowed?
No.
Why is that not allowed?
do
allows you to take a block, such as you could write anywhere else, and use it as an expression.
{
const name = if (customer) {
customer.name
} else {
"unknown"
};
}
isn't legal as a statement, and so nor is it legal as the body of a do
.
I thought the point of this proposal was to achieve statement as expression. Otherwise, it's just IIFE body semantics, with a request to omit return
from function bodies, essentially. The same thought has to be put to it as if it were that.
do
allows you to put a block which you could already write in expression position. It does not, and in my view should not, introduce a new context in which you can write things which are not currently legal.
Thank you for clarifying that the proposal is about block as expression, which is covered by IIFE semantics, not statement as expression, as in functional programming. I'm not dismissing the astute observation that async in do
is more efficient than in IIFE. I'm not trying to be pedantic either. Seeing the issues raised, just clarifying this seems to make the request simpler. It's asking for IIFE semantics within the block, which everybody understands already. :heavy_check_mark:
Additionally, it asks for omitting return
, but only in do
blocks, not in all function body blocks. And we still don't really get if
or try
as an expression, only their return value when wrapped with do
. Why do we need that without statement as expression? We need to wrap every statement with do
to be functional?
Block as expression is not covered by IIFE semantics. You cannot yield out from an IIFE. You cannot await out from an IIFE. You cannot return out from an IIFE.
It's asking for IIFE semantics within the block
It is not IIFE semantics, as mentioned above.
Why do we need that without statement as expression?
I discussed the motivation for the proposal above. As to why it's necessary to have do
at all, rather than just allowing every statement to be used in expression position, please see discussion in https://github.com/tc39/proposal-do-expressions/issues/39.
My point is that if return is kept, the block's semantics are familiar. You're bringing up issues about yield, etc that are going up past the do
block to the calling scope, and I'm already trying to acknowledge you for recognizing the advantages of do
for such things. await
, yield
:+1:
You can return the return of an IIFE; And you don't need to omit return
to do that. I didn't see that return (or omitted return) in the proposal takes a label and can return up multiple levels or something. So, please clarify.
Omitting return
introduces other issues, limited to do
blocks, but possibly generalized to function blocks.
My point is that if return is kept, the block's semantics are familiar.
Can you give an example of what you mean?
You can return the return of an IIFE; And you don't need to omit
return
to do that. I didn't see that return (or omitted return) in the proposal takes a label and can return up multiple levels or something. So, please clarify.
Short-circuiting
const foo = do {
const response = something();
if (response.err) {
return { err };
}
response.foo
};
You can't do that with an IIFE.
Omitting return introduces other issues, limited to do blocks
Omitting return is half of the proposal.
possibly generalized to function blocks.
This proposal does not say anything about function blocks.
Statement as expression is one of the primary motivations behind this proposal. Without that, it's just a normal block.
We get block as expression with this proposal, not statement as expression. Unless you mean to wrap each statement with do
.
Can you give an example of what you mean?
Yes:
const foo = do {
const response = something();
if (response.err) {
return { err };
}
return response.foo;
};
is akin to
const foo = (()=> {
const response = something();
if (response.err) {
return { err };
}
return response.foo;
})();
We didn't need to omit return
to get statement block as expression. and short-circuiting.
You can't do that with an IIFE.
I just showed that we can, aside from omitting return
.
Omitting return is half of the proposal.
Would you like me to compile a list of the issues caused by this?
This proposal does not say anything about function blocks.
Sure it does. If you're asking for a special block context in which return
can be omitted, then we will ask, can we generalize this to function blocks to make the language consistent? That's implicit. Otherwise, let's propose Rust blocks. We will add the keyword rust
and then everything inside the block will be Rust language. No need to ask for language consistency, because it's Rust inside the block after all.
With this proposal,
const foo = do {
const response = something();
if (response.err) {
return { err };
}
response.foo;
};
is equivalent to
const iifeResult = (()=> {
const response = something();
if (response.err) {
return { type: "RETURN", value: { err } };
}
return { type: "NORMAL", value: response.foo };
})();
if (iifeResult.type === "RETURN") {
return iifeResult.value;
}
const foo = iifeResult.value;
As you can see, the iife version is much more verbose. And if you bring in labeled break
and continue
, the iife version gets even worse.
Also note that if you want "functions where you can omit return
", this proposal gives you this:
const addAndSquare = (a, b) => do {
const sum = a + b;
sum * sum;
};
With this proposal,
const foo = do { const response = something(); if (response.err) { return { err }; } return response.foo; };
is equivalent to
const iifeResult = (()=> { const response = something(); if (response.err) { return { type: "RETURN", value: { err } }; } return { type: "NORMAL", value: response.foo }; })(); if (iifeResult.type === "RETURN") { return iifeResult.value; } const foo = iifeResult.value;
As you can see, the iife version is much more verbose. And if you bring in labeled
break
andcontinue
, the iife version gets even worse.Also note that if you want "functions where you can omit
return
", this proposal gives you this:const addAndSquare = (a, b) => do { const sum = a + b; sum * sum; };
Quick correction - I think your first example should end with response.foo;
instead of return response.foo;
Oh thanks, fixed. I copy-pasted the input from the wrong place.
Ah, I see now. The return
is not returning from the do
block, it is returning from the higher calling scope. So omitted return is the value of the do
expression, but return
not only exits from the do
block but goes one level higher on the call stack. Right?
I wasn't grasping just how Rusty this is.
Exactly, return
always returns from the enclosing function and not from the block.
Doesn't this just provide another way to write an immediately invoked function?
Isn't that the exact semantics? In that case, we don't need to try to copy Rust and exclude the final semicolon, or worry about early returns. Simply say:
do { <body> }
:=(() => { <body> })()
Done. Alternative name:
call
. Or, can you make a block callable?Syntactically:
{ <block> }()
:=(() => { <block > })()
Can the runtime can optimize this, but use the same semantics?
I wouldn't go the route of excluding the final semicolon as implying a
return
. The comma operator is already somewhat similar to Rust's ability to have the last expression of the block be its value. The main limitation I have noticed is that variable bindings are not permitted inside a comma-operator expression, although assignments are allowed. I guess this is because of the ambiguity between a comma separated list of variables in alet
/const
binding, versus a binding in a comma operator expression. For example init is ambiguous where the
let
binding ends and the comma operator begins.Fine, let's replace the comma with a semicolon to resolve the ambiguity. But, now we create another ambiguity, e.g. parsing
for
Now we say, let's add this Ruby-like
do
expression that omits the final semicolon like Rust, to make something like comma operator. Phew!