This is intended to be a counter-proposal to do expressions.
Do expressions are really nice, and I would love it if we had them in the language, but they're also a bit ... odd. The original idea seems nice - turn any statement into an expression by putting it in a do block, until you realize that there's a bunch of things that should really not be turned into expressions, such as if without else, a for loop, etc, so they're currently adding a lot of exceptions to what's allowed at the end of a do block.
Turns out, if you actually look at the list of valid javascript statements, there's really only a small handful of statements that are actually useful as expressions. What if, we just create an expression form of useful statements (such as if and try/catch), instead of bringing the do block proposal into the language, with all of the oddities it brings with it.
There is actually tons and tons and tons of discussion on the do block proposal of people trying to change the syntax of do blocks, so that you can just use a statement in an expression position without creating an extra nested block. What I'm proposing here is based in part of some of the feedback I've left in those discussions. These ideas in general seems different enough that I thought it might be worth making a separate proposal out of it, instead of changing do blocks into something it's not.
The proposal
The javascript features I think would be most valuable as an expression would be: if, try/catch, and declarations, so I propose that we simply add an expression form for these three features.
do if
The syntax for do if is:
do if (<condition>) <expression> [else if (<condition>) <expression>] else <expression>
You're not allowed to have a do if
without an else.
Example:
const result = (
do if (numb === 0) 'zero'
else if (numb === 1) 'one'
else if (numb === 2) 'two'
else 'two+'
)
do try
The syntax for do try
is this:
do try <expression> catch (<identifier>) <expression>
Example:
const rethrow = err => { throw err }
const result = (
do try dangerFunction()
catch (err) err instanceof NotFound ? null : rethrow(err)
)
do const
This is inspired by functional languages such as haskell. Whatever declarations that go in do const
will only exist within the do const
expression. Whatever goes after the in
will become the completion value of this expression.
The syntax is:
do const <identifier> = <expression>[, <identifier> = <expression>] in <expression>
Example:
const result = (
do const
x = getX(),
y = getY(),
z = x + y
in processZ(z)
)
But what about...?
throw? Expression-throw is already being treated in its own proposal here. If that doesn't go through, we can always add on a do throw <expression>
syntax.
switch? The need for switch should be replaced by the pattern match proposal, which provides an expression construct.
break? continue? return? These are all things that the do block supports, but I'm not convinced that's a good thing. If others disagree, we can discuss it, and potentially add "do break/continue/return
" - some of them could potentially be added without a do prefix too.
for loops? How would you want this to behave? Potentially the most intuitive answer would be to have a for loop generate an array of values generated from each iteration, e.g. (do for (x of [1, 2, 3]) x + 1) === [2, 3, 4]
, but, we already have array.map() to provide this functionality. I currently don't see a convincing use case for a for loop as an expression.
while loops? While loops are a very imperative control structure that just don't fit well as an expression. I'm not sure how you would even begin to model this as an expression, but if you have any ideas, and you think a "do while
" would be valuable, we can discuss it.
Imperative programming in an expression context? This is probably the main thing that do blocks provide that won't be provided by this proposal. In a do block, you can do any kind of imperative programming, right in the middle of an expression. You can loop over an array, updating mutable state as you go along, then finally provide a completion value as the result of this imperative logic. I don't think this is a necessary feature though. Many functional languages out there don't provide any way to do imperitive programming, and yet they seem to get along just fine without it. You don't need a while loop to survive. I do think there are use cases for impleritive programming, but does it have to be right in the middle of an expression? If it really does, you can always use an IIFE.
What if we just toss the do prefix, and turn if/try/etc into expressions
There are some parsing concerns about this kind of idea, which was discussed here. Additionally, if
and do if
behave differently and have different grammar rules (do if
requires an else block while if
does not, and it can only contain expressions, not statements). Same is true with try
vs do try
and const
vs do const
. I think it makes sense to just leave them as separate control structures entirely, instead of morphing them into the same thing.
General example
Here's an example that combines many of these expressions together to form a more complicated function. Not everyone will choose to nest them to this extend, but you certainly can. In fact, if you wanted to, using these constructs you could make all of your function bodies be constructed entirely out of expressions like you would in a functional language.
// This snippet is relying on the throw-as-expression proposal going through.
const getUserInfo = userId => (
do const
userData = (
do try (
await fetchUserData(userId)
) catch (err) (
err instanceof NotFoundError ? null : throw err
)
),
dbData = await fetchFromDb(userId),
in {
name: userData.name,
birthday: userData.birthday,
dbId: dbData.id,
}
)
Feedback
What do you think? Does it feel intuitive? Is there anything else you can think of, that you wish you could do in an expression context? I dismissed many possibilities, such as return, break, while loops, etc - do you agree that these shouldn't be in an expression, or would you find these features as valuable in an expression context?
And most importantly, can you think of any code snippet that can be written cleanly with the do expression proposal, that can't be written as nicely with this current proposal? If so, it means we're not covering all of the useful features that do blocks provide. I personally can't think of anything.