Open theScottyJam opened 1 year ago
Pattern matching is an expression. Expression values can be ignored ("like a statement"), and this is fine and has no impact on anything.
Pattern matching will likely not be adjusted at all for do expressions; you'll just be able to put one in place of the RHS expression of a match clause.
I'm very confused what this is asking for.
I think the updatePassword()
example above illustrates the issue best.
As the do expression proposals currently stand, you can not write that example in its current form. It would be an error, because you're ending the do block with a for loop, and that's not allowed.
Having a for loop at the end of a block was forbidden due to the potentially bad assumption that a do block should never be used purely for side-effects purposes - if you're wanting side-effects and don't care about the completion value, you can just use a normal block instead - at least, that was the theory.
In practice, this assumption breaks with pattern matching, where a normal block isn't an option - if you want statements, you have to use a do block, which means you may resonably want to use do blocks purely for side-effect-only purposes (ignoring the completion value), which in turn means you may find it surprising that you can't end your do block with a for loop if you don't even care about the completion value.
You could do ugly hacks to get around the limitation (i.e. putting, say, a 0;
after the for loop just to add another statement, so the last statement isn't a for loop anymore), but it would be unfortunate if we were actively encouraging hacks like that.
I am, however, realizing that pattern-matching isn't the only scenario that breaks this assumption. async do blocks will break the assumption as well - it would be very reasonable for someone to use an async do block purely for the side-effects, and not caring about the completion value, which means, again, someone may be surprised to find that they can't use a for loop at the end of the async IIFE, and may be forced to hack around the limitation, like this:
async do {
for (const task of tasks) {
await task();
}
0; // Putting this here simply because we can't end this do block with a for loop.
};
So, I guess in a nutshell:
There are valid reasons someone may want to place a for loop, or if-without-else, etc, at the end of the do block - and that reason is that they're using the do block purely for side-effect purposes (they don't even care about the do block's final completion value). Because of this, we probably shouldn't ban these types of situations.
A do block inside a match expression inside a for loop would not necessarily have the same syntax restrictions as a do block directly does. That's a problem for whichever is the second proposal to advance to solve.
As for the nutshell part, that there are valid reasons to do X, when doing X is a bug most of the time, is definitely an argument to ban X, especially when there's alternative ways to write it. Nobody has to use a do expression.
A do block inside a match expression inside a for loop would not necessarily have the same syntax restrictions as a do block directly does. That's a problem for whichever is the second proposal to advance to solve.
It seems weird to give the do block different behaviors depending on where it is found, but I guess that works. Expect, it doesn't solve the async do block issue.
As for the nutshell part, that there are valid reasons to do X, when doing X is a bug most of the time, is definitely an argument to ban X, especially when there's alternative ways to write it. Nobody has to use a do expression.
The "alternative" for many wouldn't be to revert to an ugly IIFE, it's to circumvent the "helpful" restrictions by putting a dummy statement, like 0;
at the end of the do block. And, of course I don't know for sure, but I suspect this would become a fairly common pattern if things continue the way they are.
"If you're using an async do block purely for side effect purposes, and you want to put certain control structures at the end, but the language won't let you, just remember to put a 0;
afterwards. Maybe just get into the habit of always putting a 0;
at the end of side-effect async do blocks so you don't have to worry about what kinds of control structures are and aren't allowed at the end of the block".
Perhaps we'll even get linter rules to helpfully remind us to add this 0;
when it's necessary. Do we really want to be encouraging such a hacky-feeling behavior?
I don’t think it would become common; but if it did the restriction could be loosened later.
What i think will happen is in the exceedingly rare case where someone is doing this, they’ll simply not use a do expression, no linter required.
Out of curiosity, what exactly do you feel is "exceedingly rare"? And perhaps I'll focus on async do blocks here.
And, just to be clear, when I talk about "not caring about its completion value", I'm talking about situations such as this:
function reader(size, callback) {
if (promise) {
async do {
try {
await prepare(await promise);
reader.call(this, size, callback);
} catch (error) {
callback(error);
}
}
return;
}
...
}
This specific example I pulled from version 5.1.0 of the into-stream package, from their index.js file, but I converted their async IIFE to an async do block. This specific example only shows point 2, "using async do blocks and not carrying about its completion value" - it doesn't actually show what it would look like to also end the do block with a forbidden control structure. If it would help, I could look around for an example of that too.
Using any kind of do expression (i'm not familiar with the term "do block") in one of the forbidden positions.
I don't think that conversion makes sense really - it should be:
function reader(size, callback) {
if (promise) {
return prepare(await promise).then(() => {
reader.call(this, size, callback);
}).catch(callback);
}
}
and not need a do expression regardless.
Sorry if this was stated somewhere and I missed it but why does it have to be a do expression as in:
let x = do {
return 2
}
I would think it should mimic Rust which is extremely terse and elegant:
let x = {
2
}
Also more generally, it seems like features are starting to finally get added to js after what seemed like a few years of stagnation. Recently groupedBy, toSorted and friends, at, and a few others have made it to stage 3 are already implemented in browsers. When do you estimate do expressions (and pattern matching) would eventually make it to javascript (if it ever does)? 1-2 years? 5? Is there some kind of outstanding blocker or some kind of scrimmage like the pipeline operator with the f# and hack pipes? Any insight would be highly appreciated!
let x = {
y
}
is already legal, so that doesn't work.
There is no concrete blocker except time and interest from people working on it.
@55Cancri The thing you suggested means that you are creating an object with a key called "y" with the value of the variable of "y". The reason the "do" has to be there is becasue there's no way to tell it apart from an object. Also those two aren't the same, you don't have to use the return keyword, that would return from the current function, just like in Rust too.
The rules for do-expressions are assuming that you will use it as an expression, so if you don't use it an expression, then those rules are pointless. I'm not sure about Rust, but I remember in Java if you use a switch-case with the "->" instead of ":", it can be an expression or a statement, if it's statement it doesn't care but if it's an expression you have to provide a clear value for it to evaluate to. You can tell if it's an expression by if it needs a semicolon at the end. I assume it's the same in Rust because neither have objects like JavaScript (and because Java copied it from Rust).
It just kinda depends how you want to think of it, is it purely an expression or is it like an arrow function? And is "{}" also an expression and it just happens that objects have the same syntax and we need "do" as a marker to tell them apart or is it that "do {}" is a completely new thing?
I'm still in favour of an arrow function type style because:
Completely new unrelated solution: no ":" to use as a code block which evaluates to an expression, use a ":" like in the password example for it to be purely an expression. We could apply this to stuff to, for example, the top 3 evaluate to the string 'expression', the bottom 3 would evaluate to an object.
if (test) {'expression'}
when (test) {'expression'}
(test) => {'expression'}
if (test): {'object': 123}
when (test): {'object': 123}
(test) =>: {'object': 123}
The pattern matching proposal is currently being designed with the expectation that do expressions will help fill in some functionality gaps (in its current form, it is encouraging the use of IIFEs until do expressions get standardized).
I think it's worth discussing how we want these proposals to live together, because if we assume they'll coexist, it may influence some of our design choices in regards to do expressions.
My biggest concern that I mostly want to focus on is the following: If pattern matching is being used like a statement (i.e. its completion value is getting ignored), then do expressions in turn will be getting used like a statement (i.e. nothing will use its completion value). Do expressions, as currently designed, is assuming that this will never happen in any practical code, which is why the design was fine with disallowing for loops and what-not at the end of the do block. Here's an example of what I'm talking about:
Possible solutions:
undefined
. (Would some find this confusing, or is this intuitive behavior? This might be adding to the cognitive load of what you have to learn to use do expressions)Anyways, just hoping to start that discussion a bit, mostly because the pattern matching proposal seems to just assume that do blocks will work just fine for its use case, and do blocks, at the moment, seems to assume that they would never get used in a context like a "statement pattern match", i.e. there's a bit of a mismatch between these two proposals as they're currently defined.