Statements as expressions

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.

2 Likes

You don't need the do keyword in that case. Just allow if or try as expressions.

Would parse as z = (x + y in processZ(z)).
You'd need to use a keyword that's not an infix operator, like:

  with const
    x = getX(),
    y = getY(),
    z = x + y
  do processZ(z)

I wanted to better explain why I think you don't need do before if/try. Then I realized nobody needs do if β€” JS already has conditional expressions. So I'll only discuss do try (but note that do if has an even worse case of the same issue, on top of being redundant).

do try expr catch (e) expr NEXT

If expr can be an object literal, then you might not be able to tell whether that's a do-try-expression or a do..while loop, until you get to the NEXT token. And you need to make sure it's unambiguous.

// currently valid JS
do try { foo: bar() } catch (e) { oops }
while (cond)
{ foo: bar() }
// parsed as
do { try ... catch ... } while (cond); // do..while loop
{ foo: bar() }; // object literal

You need to specify that the above must not be parsed as:

do try // do try expression 
  { foo: bar() } // object literal
catch (e)
  { oops } // object literal
;
while (cond) { // while loop
  foo: // label
  bar();
}

You can either:

  • forbid object literals;
    in this case, the do prefix does not do anything useful, because it's the token after try that decides whether it's a statement (left curly brace) or expression (anything else);
    or
  • adopt the rule by which do-expression proposal avoids ambiguity β€” wherever do can start a do..while loop, it does. If you want to force parsing a do-expression in a place where it would otherwise parse as do..while statement, wrap it in parentheses;
    in this case, you could drop the do and make that rule for try alone;
    edit: or
  • drop the whole do try prefix:
    res = expr catch (e) handler;
    
    or
    res = expr catch e => handler;
    // here => is not an arrow function, just a delimiter;
    // think of it like a ternary operator:
    // expr1 `catch` identifier `=>` expr2
    

do const is the weirdest of them all. Given that there is for(const x in y), it does things in surprising ways. I would expect the const bindings to somehow destructure the in expression, and I wouldn't expect the in to be the value of the whole expression.

@lightmare

Thanks everyone for the feedback.

Yeah, it looks like there's a number of parsing errors with what I just proposed.

Conditionals don't support else-if, which is the big motivation for them. Sure you can nest them, but a lot of people forbid doing so, as that can feel pretty confusing and tricky. do if provides a natural way of doing so.

This one I actually realized soon after posting this. Yes, we'll need something else. The with const ... do ... is a pretty intuitive way of doing it, I like that :).

Good point. Another option is to change the prefix. i.e. do.if/do.try. or with if/with try to be consistent with your with const idea. It would also be possible to drop the try prefix as you suggested and turn catch into a binary operator, though, it is nice having try there, making the boundaries of what's being caught very explicit (as opposed to relying on operator precedence), and the try looks nice when you're breaking it up over multiple lines, but I also see value in just having catch as a binary operator.

@kornel

You're right, it is a little confusing. Do you find @lightmare's suggested syntax of with const ... do ... more intuitive? I just automatically reached for the in keyword, as that's how many other languages do declarations as expressions, but I should have realized that in already has meaning and shouldn't be reused in this context.

There is no difference in expressiveness, it's purely syntactic. You just replace "x ?" with "if (x)" and ":" with "else".

const result = (
  do if (numb === 0) 'zero'
  else if (numb === 1) 'one'
  else if (numb === 2) 'two'
  else 'two+'
)

is the same as:

const result = (
  (numb === 0) ? 'zero'
  : (numb === 1) ? 'one'
  : (numb === 2) ? 'two'
  : 'two+'
)

I agree - it's purely syntactice. But do if/with if reads better, especially when you start nesting these things:

const f = (x, y) =>
  with if (y === 0) (
    with if (x === 0) throw new Error('Both x and y can not be zero!')
    else if (x < 0) throw new Error('x can not be negative')
    else x
  ) else if (x === 0) (
    y
  ) else (
    Math.min(x, y)
  )

const f = (x, y) =>
  y === 0 ? (
    x === 0 ? throw new Error('Both x and y can not be zero!')
    : x < 0 ? throw new Error('x can not be negative')
    : x
  )
  : x === 0 ? y
  : Math.min(x, y)

I see people use ternaries in the format you showed, but I've never seen them nest nested ternary like in the above example, and I'm not sure that kind of pattern will really catch on. A lot of people already dislike the idea of nested ternaries (and many style guides forbid them), even though a normal if - else if - else is just a nested if else in the same manner. There does seem to be a desire out there for an expression form if "if" - just reading through different motivating examples people give in the do expression proposal, many of those examples seem to be featuring how nice it is to do an if/else if/else as an expression.

But, you're also right. Even if we simply add an expression-try-catch and expression declarations, we should end up with just as much power as the do expression proposal has. The expression-if would be more of an optional nice-to-have, not a have-to-have-in-order-to-make-javascript-expression-oriented.

On second thought, the simpler we make this proposal, the better. We can just ditch expression-if for now - it can be discussed more at a later point if we want it. But, for now, we can focus on the minimum number of things we need to convert to expressions in order to replace the do expression proposal, and that's just declarations and try-catch - declarations likely being the most important thing.

I think the separation between statements and expressions is very important, and I strongly think that the do { } is a necessary signal that statements will appear in expression context. I would not be in favor of converting anything to an expression that's not already one (throw expressions, at stage 3, being the notable exception).

That one's unfortunately still at stage 2 :slightly_frowning_face: - unless the README is outdated.

Could you clarify this a bit? I assume from this statement that you would be opposed to something like this:

const x = try f() catch (err) err.value

as we're giving "try" two different meanings depending on whether or not it's a statement or expression.

Are you comfortable with it, if we're prefixing the "try" with some other keyword in order to distinguish it as a completely different operator?

const x = with try f() catch (err) err.value

Or would you prefer we try to entirely change the semantics of whatever expression-try-except we come up with, so it feels very different from the statement version? Maybe we even find different keywords instead of "try" and "catch" to use, to help make the distinction clearer?

Ah, you’re right, it’s still stage 2.

Yes, i would be. I’m comfortable with do expressions, no more and no less.

Well, that's a shame :disappointed_relieved:

I do know many other people are hoping the do expression proposal goes a different direction (as I've linked in to in my O.P.), so I'll try to keep pursuing this idea and see where it leads.


On another note, I was remembering this "catch pipeline operator" proposal recently. It's intended to be a follow-on proposal to the pipeline operator that provides catch-handling semantics. i.e. if the pipeline operator is a sync version of promise.then(), then this proposed operator is a sync version of promise.catch().

Example:

const result = getId()
  |> getUser(#)
  |^ # instanceof NotFoundError ? null : throw #
  |> getGroups(#)

In the above example, the "|^" operator will catch any errors on the left-hand side and pass that error to the right-hand side. It'll follow whatever syntax the pipeline operator follows (F# - where the right-hand side expects a function, or hack-style, where the right-hand side is an expression with sigils (#) that are replaced with the LHS's value).

I really love this proposal, and hope it goes in. If it does, then the only remaining thing that we would need is the ability to create declarations in an expression position.

Sounds like it's time to go back to coffeescript.

sure do!
The do expressions will benefit from the catch pipeline operator / error dejection operator.
I recently had a thought about it, how will it handle Syntax Errors? I mean if the syntax is incorrect the operator itself isn't going to get parsed, let alone executed.
It's not the right place to discuss this so I'll post the rest in the corresponding discussion.

Brw regarding "do proposal" I'm wondering why use "do" keyword instead arrow "=>" which already partially has expression-based semantics combining with some control flow statement?
do proposal:

const abs = do {
  if (x < 0) x else y 
}

new (arrow based):

const abs = if (x < 0) => x else y 
const abs = if x < 0 => x else y // parentheses are optional

do proposal:

(do {
  while (cond) {
    // do something
  }
});

new (arrow based):

while (cond) => {
   // do something
}

and etc

An arrow would imply it's a function body, which has an impact on scoping, and await/yield/return, etc.

1 Like

As implementor (in Typescript), I think this is the good way to do. Pick useful things into expression (this thread) to make expression powerful, instead of providing a statement wrapper (do expression block).

break, continue, return in the do expression open the gate of changing control flow (excepts throw) in the expression position. I don't know if it's a good idea.

And the forbidden-for or if-without-else problem, those problems does not exists in this thread.

The only problem I can see in this thread is how to handle temp variables. The syntax's looks not very suitable.

1 Like

Could you expound on your worries with handling temp variables? Are you mostly saying that the "do const" syntax idea is a bit clumsy to use? (I would agree with that)

Yes. That syntax is a little strange.

Maybe we can combine these to get a better form of do expression:

  1. The first step is the same as the main thread (make if and try to become expressions)
  2. Add a new syntax expr ExprBlock. ExprBlock is a new kind of Block that can only contains Expression and Declaration, and Declaration cannot be in the last item of the block.
  3. ExprBlock: {} (base case, evaluated as undefined)
  4. ExprBlock: { Expression } (same as Expression)
  5. ExprBlock: { DeclarationOrExpression; Expression } (note: const and let are not statements!)

So it will resolve this problem in a nice way:

const val = expr {
    function r() { return Math.random() }
    const rnd = r()
    // Declaration in non-end is OK
    rnd * rnd
    // Expression is OK
}

const val2 = expr {
    var a = 1; // No! var is statement
    for (const a of b) a // No! for...of is statement
}

Furthermore, we can use this ExprBlock in if, try expression.

const a = if (expr) {
    // This is a ExprBlock
    for (const a of b) a // No
    b.map(x => x) // Ok
} else {
}

How do you think?

@JackWorks - you've got some good ideas there. My main concern is that within an expr block everything looks like normal code, but it's actually a highly restricted block, and it may be unintuitive with which things can go in it and which can not. e.g. var can't be inside, but a function() {} declaration can - those make sense when I think about it, but that wouldn't have been my initial impression. One way to solve that is to limit it further, and make it so only const declarations can be used. Functions can be declared with const, and there really shouldn't be a need for a mutable binding here, so we can just disallow them.


I'm going to go back to my initial idea for a bit, with this example:

const result = (
  do const
    x = getX(),
    y = getY(),
    z = x + y
  in processZ(z)
)

I think the most awkward part about this is the fact that it uses two layers of indentation, and the resulting in processZ(z) part is very separate from the preceding declarations. It just feels cumbersome to use. I think I've found a way to solve many of these awkward points with some minor adjustments.

const result = (
  with x = getX() do
  with y = getY() do
  with z = x + y do
  processZ(z)
)

The syntax for this expression form of declaration is:

with <identifier> = <expression> do <expression>

This syntax only lets you bind one identifier to a value. To bind multiple, you have to chain them together, like I've done in the above example. Now we only need one level of indentation, and the resulting processZ(z) expression is right next to the declarations.

Here's another syntax idea that also solve the same problems:

const result = (
  with x = getX()
  with y = getY()
  with z = x + y
  do processZ(z)
)
with <identifier> = <expression> [with <identifier> = <expression>...] do <expression>

This particular format has a pretty similar shape to your expression block idea, but its rules feel a little simpler to me.

const result = expr {
  const x = getX()
  const y = getY()
  const z = x + y
  processZ(z)
}

Thoughts? Does that still feel cumbersome to use? I like that a lot better than my original idea (technically the original idea supported chaining and indenting like this too - I just didn't realize it)