tc39 / proposal-do-expressions

Proposal for `do` expressions
MIT License
1.11k stars 14 forks source link

Alternative proposal: Expression block #71

Open Jack-Works opened 3 years ago

Jack-Works commented 3 years ago

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:

expr ExprBlock

ExprBlock:

{ ExpressionOrDeclarationList }

Early Error:

  1. 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

  1. The lookahead ∉ do can be removed since we're using expr as the keyword.
  2. Those syntaxes are much more composable, if expr, try expr can be used alone.
  3. Still allow most of the cases of the current proposal.
  4. We can get rid of EndsInIterationOrBareIfOrDeclaration. They are naturally banned on the syntax level.
  5. We can get rid of the var declaration inside a do-expression because the var declaration 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:

  1. in the JSX example above, we're don't need expr block because of if expressions.
  2. We can still cover the temporary variable case because Declaration is 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?
}
Jack-Works commented 3 years ago

Extra benefit:

Once we have the concept of ExprBlock, we can use it in both the pattern matching proposals (without considering if we need a do keyword or not) and if expression/try expression.

theScottyJam commented 3 years ago

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.

// Current do-expression proposal
function f(x = do { return null }, y = 2) { ... } // Allowed
class { x = do { return } } // Dis-allowed

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.

ljharb commented 3 years ago

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.

Jack-Works commented 3 years ago

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.

Can you address what cases cannot be expressed in this way? In this thread I have the concept of "expression block" that can be used as the RHS of match clause.

ljharb commented 3 years ago

Right, but if we have "expression block", why would we need the individual statement-expression forms? The "expression block" parts just seems like the same thing as do expressions, but with restrictions that were already rejected in plenary (delegates unfortunately REQUIRE the ability to return, eg, from expression position)

theScottyJam commented 3 years ago

Well, I know earlier formulations of pattern matching toyed around with the idea of having both an expression and statement version of the match construct. This was thrown out in favor of just using do expressions in the match construct. But, who knew at the time how many nasty gotchya's do expressions would have. Maybe it's worth revisiting that original idea - have a statement version of pattern-matching for imperitve programming needs, and an expression form for functional needs, instead of trying to come up with a way to allow imperative programming in expression positions so that we only have one "match" construct. (I know having a statement and expression version of pattern matching isn't the prettiest thing, but it's arguably nicer and more intuitive than the do expression proposal as it currently stands).

theScottyJam commented 3 years ago

Right, but if we have "expression block", why would we need the individual statement-expression forms?

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 ....

ljharb commented 3 years ago

@theScottyJam none of those gotchas i consider a problem. Do expressions are the only way pattern matching can be an expression.

The most useful feature of do expressions is "turning a statement list into an expression", which this alternative does not do.

theScottyJam commented 3 years ago

The most useful feature of do expressions is "turning a statement list into an expression", which this alternative does not do.

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).

Do expressions are the only way pattern matching can be an expression.

Here's how pattern matching can be done in an expression position (this will be similar to many other languages that do pattern matching):

const result = match (data) {
  when ({ status: 200 }) expr {
    const x = 2
    const y = 3
    x + y
  }
  else (
    if (someCondition) resultA()
    else if (anotherCondition) resultB()
    else resultC()
  )
}

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.

@theScottyJam none of those gotchas i consider a problem.

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.

Jack-Works commented 3 years ago

The most useful feature of do expressions is "turning a statement list into an expression", which this alternative does not do.

"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, if expression, and pattern matching?

but with restrictions that were already rejected in plenary (delegates, unfortunately, REQUIRE the ability to return, eg, from expression position)

As @theScottyJam said, we can make return into an expression if it's really necessary. (In the current version do { return expr } has the same effect).

ljharb commented 3 years ago

Almost every single statement would need to be made into an expression, at which point, we're adding multiple new forms, versus the single do around a StatementList (modulo any minor restrictions it has that nobody will ever actually run into or need to learn in a practical sense).

I think this alternative proposal is exceedingly more complex and confusing than the current one.

Jack-Works commented 3 years ago

Almost every single statement would need to be made into an expression

That really isn't much.

(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

any minor restrictions it has that nobody will ever actually run into or need to learn in a practical sense

I really doubt this. I think it will be very common for if to be appeared at the end of the do expresion. How can it be a edge case?

And e.g.: if-expression has a clear rule: You must have else branch. But the current early error check is very awkward: You must have else branch when you inside a do expression, and when it is the last statement (and you need to check it in the nested way).

if-expression has a more clear rule and it's easier to teach.

ljharb commented 3 years ago

For an elseless if to appear at the end? I don’t think so. I think the most common will be ending with an if/else, so it can produce a const without a ternary (or without a let).

Jack-Works commented 3 years ago

And don't you think splitting them make them much more composable? You can use the necessary syntax without the extra do {} when it doesn't contain Declarations.

const a = do { if (expr) expr1; else expr2 }
const b = if (expr) expr1; else expr2 // syntax not important here
theScottyJam commented 3 years ago

For an elseless if to appear at the end? I don’t think so.

What about this scenario? I'm sure people will try to do this:

match (data) {
  when ({ x }) {
    console.log('value was', x)
    if (x > 0) console.log('it was positive')
  }
  else {
    console.log('unknown value')
  }
}

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.

ljharb commented 3 years ago

No, i don’t. I also think that this alternative is much harder to teach about “converting code back and forth”. The current proposal is “wrap it on do { } / remove the wrapper”, modulo syntax errors in the wrapped form. This one requires diving into each statement and making a different kind of conversion, in either direction.

@theScottyJam if they do, and they get a syntax error, then they won’t, problem solved.

We could also potentially remove those restrictions from the RHS of pattern matching (and even do expressions) when it’s in statement position, since when it’s in expression position, i think it’s clear nobody would do that in the first place, since they’d want the match to result in a value.

theScottyJam commented 3 years ago

We could also potentially remove those restrictions from the RHS of pattern matching (and even do expressions) when it’s in statement position, since when it’s in expression position, i think it’s clear nobody would do that in the first place, since they’d want the match to result in a value.

If you do that, then you're pretty much got a statement and expression for of pattern matching. "match" in a statement position uses regular blocks on the RHS, while in the expression position it uses whatever expression block we decide to go with on the RHS (be it do blocks, this expr block proposal, etc).

Alright, so lets follow this line of reasoning:

Why should we provide an expression variant of pattern matching at all? Well, you've got to agree with me that it's pretty useful to be able to do things like the following:

const fn = x => match (x) {
  ...
}

const result = match (data) { ... }

// etc

Alright. So, what if we copy-paste the arguments for having an expression form of pattern matching, and apply them with other syntax constructs, such as "if" and "try/catch"?

const result = try { ... } catch (err) { ... } // This would be really nice to have

const numb = (
  // So would this
  if (...) { ... }
  else if ( ... ) { ... }
  else { ... }
)

If we find pattern-matching valuable in an expression position, shouldn't we also find "if" and "try/catch" also valuable as expressions? And we can do it by following the exact same behavior we're doing for pattern matching. If "if" is in a statement position, we use statement blocks, and if it's in an expression position, we use whatever form of "expression" blocks we come up with. I don't see how we can argue for pattern matching to be an expression construct, without arguing the same for "if" and "try", unless there isn't a consistent way to do so.

Alright, now the final question is - what should these expression blocks look like? Well, we can continue with the do expression proposal, or, we could make things simpler, knowing that we're now able to use the most important constructs in a statement an expression position, and don't really need to have a way of turning statements into expressions inside the chosen expression block. Whether or not the "expr block" proposal really is simpler to understand is certainly arguable, but I feel like there's less rules involved with it, so it's quicker to learn - yes any mistakes with the do block would become syntax errors and not runtime errors, but I would still rather have syntax constructs where it's easy to apply them without being unsure until you run the code whether or not you've made a syntax error.

theScottyJam commented 3 years ago

Here's another scenario I thought of, where someone may try to use a do block incorrectly, and run into these issues:

async do {
  const x = f()
  if (x) console.log('whatever')
}

Even in a code snippet like the following:

const x = do {
  f()
  if (y) 'z'
}

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.

ljharb commented 3 years ago

@theScottyJam yes, if we did that we'd have two forms, but it'd be pretty easy to control it in the spec with a grammar flag, i think.

The separation between statements and expressions is imo a good and necessary thing, and I think muddying the waters by making more statements implicitly into expressions would be a very bad idea.

In your latter code snippet, another perfectly reasonable intuition is that if y is falsy, x is whatever f() is.

theScottyJam commented 3 years ago

yes, if we did that we'd have two forms, but it'd be pretty easy to control it in the spec with a grammar flag, i think.

Wouldn't this also be true for "if" and "try/catch"? We would just need a grammar flag to control how it behaves when in a statement or expression position? With both "if" and "match, when they're in statement positions, their bodies can behave like block scopes, and when they're in an expression location, we can use whatever grammar flag, and make their bodies behave like whatever form of expression body we go with.

The separation between statements and expressions is imo a good and necessary thing, and I think muddying the waters by making more statements implicitly into expressions would be a very bad idea.

This also sounds like an argument for making pattern-matching a statement-only construct. Doesn't having pattern-matching behave differently in statement vs expression positions muddy the water just as much as it would with "if" or "try"?

I do agree that it's a good idea to try and keep a clear distinction between statements and expressions - for this reason, in earlier revisions of this proposal, we were using "with if" and "with try" for the expression forms, so they could conceptually be thought of as "slightly different constructs" rather than "the same construct in an expression location". I would similarly argue that with pattern matching, it might be good to make the statement form just be "match", and the expression form be something like "with match", so as to not muddy the waters, and make it clear why the expression form has minor differences in behaviors from the statement form.

ljharb commented 3 years ago

@theScottyJam yes, but unchanged, if and try etc are not viable to suddenly become expressions. That would not achieve consensus.

A new construct doing this, however, would not have the confusion of suddenly having different semantics after decades.

hax commented 3 years ago

@ljharb As we see, many js programmers want if expression, so i'm not sure what's the real blocker issue of if expression?

ljharb commented 3 years ago

Many people want many things; that's a nice precondition but not an automatic qualifier. I think it would be confusing, and would cause many bugs, and i don't want any statements to directly become expressions - only inside an explicit wrapper, like do { } or expr { } (or any other acceptable spelling).

Jack-Works commented 3 years ago

I thought about this case, it's awkward:

const val = expr {
    if (val2) console.warn('what')
    // syntax error, expected token "else"
    val2 + 1
}

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.

I agree. If we change the expression block to { StatementOrDeclaration Expression } and have expression version of try, if and throw, we will have the same semantics of do expression currently. And it will be a nature result to have the same semantics of EndsInIterationOrBareIfOrDeclaration does, in extra, we can have try and if useful on it's own.

theScottyJam commented 3 years ago

yes, but unchanged, if and try etc are not viable to suddenly become expressions. That would not achieve consensus.

Sorry, maybe I'm just slow, but the arguments don't seem to be lining up in my head...

By unchanged, are you referring to the fact that we're changing their behavior by, in the case of "if", dis-allowing if without else when used as an expression? If so:

pitaj commented 3 years ago

I'm pretty sure the change he was referring to was "if..else can now be used in expression position"

ljharb commented 3 years ago

@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 class or async function or Modules or match having 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.

theScottyJam commented 3 years ago

Ah, gotcha

So how do you feel about other syntax changes to the if construct? e.g. I've seen the idea get thrown around of allowing if (const x = f()) { ... } behavior - would that also be a non-starter because it's adding new behaviors to if?

Either way, we do both seem to agree that if expression-if would exist, it should have different syntax, to distinguish it from statement-if. We just disagree on the reasons why 😁️. We'll just assume that we're pushing a "with if"/"with try" thing for now (exact syntax can be bike-shedded later). And, let's run with what @jack-works said and keep do expressions as they are, but changing it so the last line only accepts expressions, not any statement.

So now, the question is, which is easier to understand and more useful? Do expressions as they currently stand? Or, only-expressions-at-the-end + "with if" + "with try"?


Aside: My personal vote is that we stop it with the implicit returns, and go for a completion-value-marker keyword such as give 2 + 2 (similar to a "return" but for the block), as discussed in #55. This removes the special treatment for the last line of the do block, the last line can just be a regular statement, an if without else, etc. This also removes the need for "with if" and "with try", because you can just use a statement "if" instead, and end each branch with this completion-value-marker, the same way you currently use "return". I would still hope for a "with if"/"with try", but it would have to be a different proposal if this route is taken, because do blocks wouldn't have any need for it

bakkot commented 3 years ago

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 if one). 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

const val = expr {
  let sum = 0;
  for (let i = 0; i < 10; ++i) {
    sum += f(i);
  }
  sum;
};

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

Each of those things is something we could discuss independently of the others.

theScottyJam commented 3 years ago

@bakkot - I also find the original expr {} construct a little weird for that reason. There are other ways to formulate the syntax (which were discussed in the TC39 form post), such that it looks less like a standard block, thus making people less likely to think that they can throw a for loop in the middle of it.

As mentioned previously, many other languages use a let x = 2, y = 3 in x + y type of syntax to accomplish similar objectives to the do block proposal, which really is the exact same thing as the expr block, but with different, arguably more intuitive syntax. I, personally, think a syntax like this would work great in Javascript:

const groups = (
  with user = await getUser()
  with groups = user.groups
  with allRoles = await getAllRoles()
  do pickRolesFrom(allRoles, { roles }) // groups will be assigned to the value of this final expression
)

The general syntax for this is: one more more "with" declarations, followed by a "do", then an expression. Note that this is really the same as expr {}, just with different syntax. All of the preceding lines must still be declarations, and the last line must still be an expression.

I like this sort of syntax construct, as it forces all imperitive-ness out from the expression. At a glance, you know that many surprised won't be found within the with-do construct (at least, the syntax naturally discourages it, people can still find ways around it). For example, you're unlikely to find code modifying object, local variables, or globals hiding in the middle of this all (e.g. it's awkward to use obj.x = 2 in the middle of this syntax construct). People are less likely to be calling state-modifying functions, because the expression-nature of this construct forces users to always make use of whatever gets returned (e.g. it's awkward to use map.clear(), because you're forced to do something with the return value, and its return value is pretty useless). In short, it provides some really nice (pseudo) guarantees, that makes it really easy to tell what a chunk of code does at a glance, without hidden surprises, and it encourages good coding practices.

I do, however, understand the desire to have imperative programming in an expression position. But, it seems like a shame to pass up an opportunity to provide a syntax construct as powerful as this one.

Jack-Works commented 3 years ago

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

  • var is forbidden, which seems like a completely unrelated change
  • you can't put an if or a try on the last line, which seems like an unfortunate limitation
  • you can't use break/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.

For my newer presented idea, it's in the form of StatementList Expression, so

"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.

That's what I think about return inside do expression. It allows return inside an expression and I don't see big benefits of it. if and try are more appropriate to become the candidate of turning into expression by a first class support.

ljharb commented 3 years ago

@Jack-Works i agree - I’d rather not ever allow return inside an expression. But if we don’t, this entire proposal is blocked, so it’s a requirement no matter what.

Jack-Works commented 3 years ago

@Jack-Works i agree - I’d rather not ever allow return inside an expression. But if we don’t, this entire proposal is blocked, so it’s a requirement no matter what.

https://github.com/tc39/notes/blob/master/meetings/2021-01/jan-26.md I think I can try to present my idea in the meeting and try to make a new consensus?

Especially, this part:

Having a ReturnExpression is the same thing as do { return } currently. If that is really required, why not just add ReturnExpression?

(cc @waldemarhorwat 👀)

bakkot commented 3 years ago

why not just add ReturnExpression?

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 commented 3 years ago

Though, looking at the future of Javascript, is it possible that at some point we would want to add expression forms of "if" and/or "try" (even if it ends up being a separate proposal)? If so, do we really think it's best to allow some statements at the end of "do" and not others, vs simply only allowing expressions, and letting whatever expression forms of "if" and "try" that may come in, now or later, fill in those missing needs?

bakkot commented 3 years ago

is it possible that at some point we would want to add expression forms of "if" and/or "try" (even if it ends up being a separate proposal)?

I think that if we get something like the current proposal we are unlikely to want to also add expression forms of if and try (if especially, 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 try as 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.

If so, do we really think it's best to allow some statements at the end of "do" and not others

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".

theScottyJam commented 3 years ago

OK - let's try this idea on for size.

Currently, a do expression looks like this:

do {
  <statement>
  <statement>
  <statement that's capable of having expression semantics>
}

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.

do {
  <statement>
  <statement>
  <expression>
}

with <statement that's capable of having expression semantics>

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:

let x = (
  with if (foo()) { f() }
  else if (bar()) { g() }
  else { h() }
);

return (
  <nav>
    <Home />
    {
      with if (loggedIn) {
        <LogoutButton />
      } else {
        <LoginButton />
      }
    }
  </nav>
)

const stuff = do {
  const x = 2
  const y = 3
  with if (x === y) {
    4
  } else {
    5
  }
}

function f(x = with return) {} // If you really want to, it's possible ...

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.

bakkot commented 3 years ago

That seems strictly more complicated than the current proposal, to my eyes, which accomplishes the same goal while only adding a single new piece of syntax.


It's also not entirely clear to me what you mean by "expression-compatible statement". Would you be able to write

with if (condition) {
  let sum = 0;
  for (let i = 0; i < 10; ++i) {
    sum += f(i);
  }
  sum
} else {
  0
}

If not, why not? If it's spelled if, then certainly looks like I should be able to put a for-loop in its arms. Would you envision having to put a do in the first arm of the above if? That seems like it's adding a bunch of epicycles for no obvious-to-me benefit.

pitaj commented 3 years ago

It seems like you could support a do shorthand where do <statement> desugars to do { <statement> }. Example:

do if (x === y) {
    4
} else {
    5
}

This gets you a shorthand for when you want to use statements in expression position.

theScottyJam commented 3 years ago

Yes, your example would be correct. Basically, you would make the blocks in the "if" into implicit do blocks, similar to how pattern matching is doing it.

Alternatively, we could run with what @pitaj suggested - I beleive there's a whole issue dedicated to that sort of thing.

pitaj commented 3 years ago

Alternatively, we could run with what @pitaj suggested - I beleive there's a whole issue dedicated to that sort of thing.

1

Jack-Works commented 3 years ago

It seems like you could support a do shorthand where do <statement> desugars to do { <statement> }. Example:

do if (x === y) {
    4
} else {
    5
}

This gets you a shorthand for when you want to use statements in expression position.

I like this idea

knight-of-design commented 4 weeks ago

Good stuff. I'm really not a fan of using the keyword do since I believe it semantically entails procedural side effects so I lean towards the "expression" naming for this.

I would suggest using this expression structure but replace expr with my expr arrow idea and replace word with to use


const x = >> {
    use if (y) 
         3
    else 4
}

<Home>
    >>{
        use if (loggedIn)  <LogoutButton />
         else   <LoginButton />

}
<Home>