proposal-do-expressions: Alternative proposal: Expression block
Thanks for the discussion started by @theScottyJam in https://github.com/theScottyJam/proposal-statements-as-expressions/ and https://es.discourse.group/t/statements-as-expressions/894. I’d like to propose my alternative design inspired by him.
Proposal(s)
Expr block
ExprExpression:
exprExprBlock
ExprBlock:
{ExpressionOrDeclarationList}
Early Error:
- It’s an early error if the last item of ExpressionOrDeclarationList is Declaration.
If Expression
… the expression version of if statement, with the requirement of no missing else branch. We should create a new proposal for it.
Throw expression
https://github.com/tc39/proposal-throw-expressions
Switch expression Pattern matching
https://github.com/tc39/proposal-pattern-matching
Try expression
… the expression version of a try statement, with the requirement of no finally branch. We should create a new proposal for it, or https://es.discourse.group/t/try-catch-oneliner/107
For loop
Use Array methods like forEach of map. For iterators, use https://github.com/tc39/proposal-iterator-helpers instead.
Return, continue, break
Don’t do it.
Benefits
- The
lookahead ∉ docan be removed since we’re usingexpras the keyword. - Those syntaxes are much more composable,
if expr,try exprcan be used alone. - Still allow most of the cases of the current proposal.
- We can get rid of
EndsInIterationOrBareIfOrDeclaration. They are naturally banned on the syntax level. - We can get rid of the
vardeclaration inside a do-expression because thevardeclaration is a Statement that is not allowed.
Example
let x = expr {
let tmp = f(); // yeah
tmp * tmp + 1
};
let x = expr {
if (foo()) { f() }
else if (bar()) { g() }
else { h() }
};
return (
<nav>
<Home />
{
if (loggedIn) {
<LogoutButton />
} else {
<LoginButton />
}
}
</nav>
)
Notice:
- in the JSX example above, we’re don’t need
exprblock because ofifexpressions. - We can still cover the temporary variable case because
Declarationis allowed (in the non-end position).
Early errors
expr {
let x = 1;
};
expr {
function f() {}
};
Declaration in the end. This is an early error.
expr {
while (cond) {
// do something
}
};
expr {
label: {
let x = 1;
break label;
}
};
Syntax error: only expression or declaration is allowed.
expr {
if (foo) {
bar
}
}
Syntax error: If-without-else is not a valid if expression.
Edge cases
var
Totally not legal. No hoist ✨
Empty expr {}
undefined.
await and yield
Inherit.
throw
We use the throw expressions proposal!
break, continue, return
No no no, things like this in an expression position are bad!
Conflict with do-while
We don’t have this problem 🎉
B.3.3 function hoisting
Sloppy-mode function hoisting is not allowed to pass through a do-expression.
What do we miss?
Case: re-generator
Before:
const result = do {
let r = 0
for (const i of x) {
r = yield r
}
r;
}
After:
const result = expr {
yield* Iterator.from(x).reduce((sum, value) => value, 0)
// is this example identical to the before?
}
About this issue
- Original URL
- State: open
- Created 3 years ago
- Reactions: 11
- Comments: 42 (21 by maintainers)
It seems like you could support a
doshorthand wheredo <statement>desugars todo { <statement> }. Example:This gets you a shorthand for when you want to use statements in expression position.
That really isn’t much.
expr { ... }ifexpressionvar)/IterationStatement/ContinueStatement/BreakStatement/WithStatement/LabelledStatement => No expression form and I think it’s bad to have an expression form for it.(Iterations? Recursive functions/iterator helpers/Array.* methods can do that!)
That’s almost all statements we have. Once we having this in the language, we can do really powerful things with expressions
I really doubt this. I think it will be very common for
ifto be appeared at the end of the do expresion. How can it be a edge case?And e.g.:
if-expressionhas a clear rule: You must haveelsebranch. But the current early error check is very awkward: You must haveelsebranch when you inside a do expression, and when it is the last statement (and you need to check it in the nested way).if-expressionhas a more clear rule and it’s easier to teach.I think that if we get something like the current proposal we are unlikely to want to also add expression forms of
ifandtry(ifespecially, given that we already have ternaries). So, while it’s possible, I don’t think it’s all that likely.More importantly, I definitely don’t think we should design this proposal around a possible future extension. This proposal would be decidedly incomplete if it did not allow you to put
tryas the final statement, so either we should go with the current proposal or we would need to add those as part of (or prior to) this proposal.Yes, that is my position. Though in any case, from a user’s point of view, that’s really not all that different from “allow only expressions, but also change some statements into expressions”.
It seems very strange to me to restrict what items can appear prior to the final statement in the list - significantly stranger than the current proposal’s limitations, which I don’t think people are likely to run into in practice (except for the unfortunate
ifone). Since items other than the final statement are obviously not used for their result value, there’s no reason to disallow loops or try-statements or anything else.For a concrete example, it really seems like you ought to be able to write code along the lines of
I think your proposal would not be viable if it forbids the above code.
So let’s say you remove that restriction, and say that you can put any statement before the last line, there being no reason to restrict those. At that point you have exactly this proposal, except that
varis forbidden, which seems like a completely unrelated changeifor atryon the last line, which seems like an unfortunate limitationbreak/return/continue, which I had initially excluded but added in at the explicit request of the committee - the proposal cannot advance without allowing those.Each of those things is something we could discuss independently of the others.
Here’s another scenario I thought of, where someone may try to use a do block incorrectly, and run into these issues:
Even in a code snippet like the following:
I would guess that if y were truthy, then x would be equal to ‘z’, otherwise, it would be equal to undefined. I wouldn’t expect that to be a syntax error. I would have to of had learned about how each statement operates when placed at the end of the do block, and learned about these special rules to know that that wouldn’t be valid syntax. I think it’s simpler to, at the very least, only allow expressions at the end of a do block. if, try-catch, etc can be turned into expressions using the line of argument from my previous comment.
“Turning a statement list into an expression” is the way of approaching the final target, but it is not the target itself. What we really want is to have enough expressiveness in the expression position. Can you give an example about what cannot be expressed without a statement once we have
try,throw,ifexpression, and pattern matching?As @theScottyJam said, we can make
returninto an expression if it’s really necessary. (In the current versiondo { return expr }has the same effect).You’re right, this alternative instead turns “statements into expressions”. Many languages, such as Elm, Haskell, etc don’t even have a concept of statements. Everything inside a function body must be an expression. Once that’s done, there’s no need for a feature that turns a statement list into an expression.
(btw, if you want something like return to be an expression, we could simply make that happen, like we’re doing with “throw”. I wouldn’t want it that way, but it could still be a possible conversation. That’s the point of this all, whatever we think should be an expression, let’s make it happen. Everything else should not).
Here’s how pattern matching can be done in an expression position (this will be similar to many other languages that do pattern matching):
In other words, after the “when” you require an expression. Because we’ve turned any useful statement into an expression, you should be able to do anything you need to in the body of the match arm.
They’re not deal breakers, but they certainly increase the learning curve of do expressions.
With all of this said, I do agree with you that it would be a loss to not have do expressions be the body of a pattern matching arm (unless you’re using a statement version of pattern matching). But, I feel like a proposal like this expr block one does a better job at providing a more general-purpose and easier-to-learn solution. In other words, if pattern matching is the only thing that’s keeping do expressions in its current shape, then perhaps we haven’t found a general-purpose-enough formulation for it yet.
#1
OK - let’s try this idea on for size.
Currently, a do expression looks like this:
What if, we made a divide right down the middle of the semantics of do blocks, and split it into two separate things, each of which is simpler than the current do proposal, but together provide equal power.
In other words, if the concern is that we’re now introducing four+ syntactic forms, lets simplify what’s being proposed here so that we’re only proposing two syntactic forms. This would give us the power to do things like this:
See how this works? The “with” keyword can receive any expression-compatible statement afterward such as if (with else), return, yield, etc. The do block itself just expects an expression at the end. This is pretty similar to what was originally proposed, but now we’re only conceptually adding one new expression construct instead of many.
I think there will be a lot less appetite for adding four+ syntactic forms, rather than just the one added in this proposal. Also, @ljharb has expressed strong opposition to that idea.
@theScottyJam what i mean is,
const x = if.expression (…) { … };would be a change,const x = if (…) { … };would be unchanged syntax. The unchanged one is the nonstarter for me.In a new construct, there can’t possibly be anything someone “knows” will work or fail, only expectations. I’m completely comfortable with a new construct, like
classorasync functionor Modules ormatchhaving new rules, as long as they’re easy to figure out and hard to silently do the wrong thing.“expression position” isn’t a new construct, and i don’t think it’s appropriate for anything to suddenly become eligible to be there that wasn’t before. I feel the same about the throw expressions proposal, but decided not to obstruct it since indications were that it was going to be the only statement converted into an expression, with do expressions handling the rest.
I thought about this case, it’s awkward:
I agree. If we change the expression block to
{StatementOrDeclarationExpression}and have expression version oftry,ifandthrow, we will have the same semantics ofdo expressioncurrently. And it will be a nature result to have the same semantics ofEndsInIterationOrBareIfOrDeclarationdoes, in extra, we can havetryandifuseful on it’s own.What about this scenario? I’m sure people will try to do this:
If pattern-matching is intended to also be used in a statement position to enable procedural code, then people will try to do procedural logic within the branch arms of pattern matching, and find they must place a dummy “undefined” or something at the end of the implicit do block, just to make it work. This is another argument in favor of splitting pattern matching into both a statement in expression form.
Because that’s taking one of the most useful features of do-expressions and putting it in the spotlight. I’ve seen a countless number of people on this proposal asking for this kind of thing - they’re just wanting to use “if” or “try” in an expression position, without the extra noise that “do { … }” creates. If you have the ability to use those constructs in an expression position, then really, the only thing left to allow people to use Javascript in an entirely expression-oriented fashion is some way to create declarations in an expression position. This expr block gives precisely that, and nothing more. There’s other syntax ideas out there that could also do the same thing (e.g. many functional languages use a “let x = 2, y = 3 in x + y” sort of syntax to accomplish this - it’s really exactly the same as the proposed expr block, but different syntax).
Another thing, is that the expression block purposely does not give you power to use statement versions of “if” or “try” to keep things simple and intuitive. The last line of an expression block has to simply be an expression. That’s it. The do expression proposal says the last line of a do block is any statement, except if without else, and for loops, and while loops, and …
Pattern matching relies on do expressions for the RHS of a match clause, and to be able to put a statement list there. This alternative proposal wouldn’t meet that use case.
Another benefit:
Doing odd things like “return inside a function parameter list” won’t be allowed anymore. The do-expression proposal currently doesn’t have a good way to address some of the inconsistincies related to return.
In general, the current do-expression proposal has a whole lot of “you can do this, unless …” in it. This “expr” proposal seems to be a lot more self-consistent and predictable.