'Yielding' Conditional and Loop Expressions

It's common to declare a variable only to immediately mutate it in a following statement:

let result = 3
if (conditionA) result = 1
else if (conditionB) result = 2

This is typical JavaScript because support for conditional expressions that yield values is limited. But it's not ideal -- now we have a mutable result variable despite never planning to mutate it again. An existing alternative is the ternary expression, but it quickly becomes hard to read when nested:

const result = conditionA ? 1 : (conditionB ? 2 : 3)

What if we could combine these two concepts, yielding a value directly from our if statement? This would combine the best of both options, maximizing readability while avoiding mutation:

const result =
  if (conditionA) yield 2 
  else if (conditionB) yield 3
  else yield 1

There's no reason we have to stop at conditionals. What if we could yield Arrays directly from loops? We could effectively combine the concepts of mapping, filtering, and flattening without having to introduce an entirely new syntax for comprehensions:

const newUsers = for (const user of users) if (user.age > 5) yield user.id

Flattening comes naturally:

const newUsers =
  for (const users of userGroups)
    for (const user of user)
      if (user.age > 5) yield user.id

The concept also works asynchronously:

const resultsPromise =
  for await (const response of requests)
    if (response.success) yield response.data

Because they are just expressions they can be used anywhere an expression can be used, like as the return value for arrow functions:

const allJson = async requests => 
  for await (const response of requests)
    if (response.ok) yield await response.json()

Or in JSX expressions:

const ListOfItems = ({items}) =>
  <ul>
    { for (const item of items) if (item.isVisible) yield <li>{ item.name }</li> }
  </ul>

Specification

The 'rules' around this concept would be fairly straightforward:

  1. A conditional or loop statement is 'yielding' when used where an expression is expected.
  2. A yielding conditional resolves to the yielded value and stops executing when a yield statement is encountered. If none are encountered, the expression resolves to undefined. A yield* statement is a SyntaxError in a yielding conditional.
  3. A synchronous yielding loop always resolves to an Array. When a yield statement is encountered, the yielded value is appended to the array and execution continues. A yield* statement individually yields each value in the yielded iterable. If no yield statements are encountered, the expression resolves to [].
  4. An asynchronous yielding loop (for await...of) always resolves to Promise<Array>. Otherwise it acts similarly to a synchronous yielding loop.

Alternative syntax

An alternative version of this proposal would be to introduce a new syntax for these expressions rather than inferring whether or not a statement is a yielding expression based on context. We could introduce new keywords inspired by generator function syntax, like for*:

const newUsers = for* (const user of users) if (user.age > 5) yield user.id

This has the advantage of being more explicit and better distinguishing between expressions & statements, but the concept is trickier when thinking about conditionals. You could have if*, but then what about else? Maybe the whole expression is a generator conditional if it starts with if*:

const result =
  if* (conditionA) yield 2 
  else if (conditionB) yield 3
  else yield 1

I don't particularly love this since it's easy to lose track in the later branches.

Prior art

This proposal is heavily inspired by Scala. In Scala (and in Ruby), all statements are value-yielding expressions. if statements always yield the result of the last executed expression. This concept is nice but doesn't fit very well with JavaScript design patterns which tend to be more explicit (ie, these languages also both support implicit returns). So I think yield works better for JavaScript as the more explicit option.

Yielding loops are very similar to Scala's 'for comprehensions', and result in a very similar syntax.

https://github.com/tc39/proposal-do-expressions

Thanks, I hadn't seen that. do solves the conditional case decently. I had actually considered another alternative, 'yielding blocks':

let x = *{
  let tmp = f();
  yield tmp * tmp + 1
};

Which is pretty much the same thing but a little more explicit about the result value.

do doesn't solve the looping case at all though. Maybe it makes sense to have both do and "generator loops" with the for* syntax.

And while do-exprs will solve the problem in general, pattern-matching https://github.com/tc39/proposal-pattern-matching is probably closer to ratification and solves this problem as well:

const result = match(null) {
  if(conditionA): 1;
  if(conditionB): 2;
  default: 3;
}

(Or if the condition is actually based on some value, you can pass it to the match() instead of null and actually use a pattern on it.)