proposal-do-expressions: Implicit return is bad

implicit return makes the source of the value untraceable
it’s difficult to find where to return
may even not return a b

const a = do {
  if (c1) { 
    ...many lines
    a
  } else {
    ...many lines
    b
  }
}

explicit return has clear semantics
also easy to search in the editor

const a = do {
  if (c1) { 
    ...many lines
    break do a
  } else {
    ...many lines
    break do b
  }
}
const a = label: do {
  while (true) { 
    break label do v
  }
}

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 21
  • Comments: 71 (9 by maintainers)

Most upvoted comments

@ljharb you say “forcing”, I say “allowing”.

From where I sit JS has become a nanny language in multiple ways that bother me. It enforces things it doesn’t have to, and instead should have allowed more choice and left opt-in adherence up to linters.

Linters are for applying opinions on the “right” way to write some piece of code. The language itself should be more neutral and leave the opinion enforcement up to configurable tooling.

Confoundingly, many devs don’t want choice, so they opt for linter/formatting tools that aren’t configurable. That’s totally fine… for them. But when the language asserts an opinion on what’s “good” or “bad” code, then folks like me are left with fewer options.

An example I despise (and yes I know the reasons why, I still disagree) is not allowing a second let declaration of a variable in a block to just be skipped/ignored.

I bristle when I hear members of TC39 justifying language design choices based on how they think JS devs should write code. Just give us a powerful language with options for how we apply it (readability and maintainability wise).

@theScottyJam it’s taken me a while to get to this.

What functionality do you feel like we’re losing via explicit completion values?

Using Rust, I fell in love with statements-as-expressions. While do-expressions aren’t exactly the same, they’re the closest thing JS will likely ever get. While semantically different, they are aesthetically similar and allow for that “mental mode” of programming.

For instance, in Rust, when I see let y = if x { 12 } else { 54 }; this reads as “y is either 12 or 54, depending on x”. Likewise, let y = do { if (x) { 12 } else { 54 } }; reads the same way, same as a ternary.

Once you start introducing keywords, this interrupts how my brain interprets the code. Instead of choosing between different values, let y = do { if (x) { give 12; } else { give 54; } }; reads as “if x is true, return 12, otherwise return 54, and store that in x”.

In essence: the literal verbosity introduced by those extra words being on screen maps to cognitive verbosity in my mental model of the code. The impact of this decreases as the complexity within the do-expression increases, but the example I’ve been using is not contrived. For instance, this would have a huge impact on match, which is supposed to use the semantics for the right-hand side:

const val = match (count) {
   when 0 { "none" };
   when 1 { "one" };
   when 2 { "a couple" };
   when ^(count <= 5) { "a few" };
   else { "many" };
}

Is it really that much more verbose?

Subjectively, yes. A lot of it really comes down to this, and there may be no way to come to a consensus here.


@getify while the conversations may need to be moved back together eventually, I think it’s useful to keep the bikeshedding over syntax and keywords partitioned from the conversation here as much as possible.

That you’re confounded by the fact that most devs don’t want choice doesn’t change that they do, and that the language should serve most devs - by not providing choice, and mandating one way to do things whenever it fits with existing language idioms.

Imo a powerful language is one that helps users solve problems. The more choice is allowed in style, the more bickering to distract from actually solving those problems. Not every delegate likely agrees with me here, of course.

I’m following along here because I just for the billionth time tried to write a nested ternary expression and confused the heck out of myself. Just wanna say that while I get why folks want less verbosity, and languages like Rust do prove it can be clear and work well, I’m not sure that it would work well in JS. I think an explicit keyword would help in a lot of cases that languages like Rust do not have.

My opinion:

Just allow the statement to return the expression directly

const a = if (a) 1 else 2

block just simply block don’t mix two semantics

The focus of block should be to allow multiple statements, not to detour to resolve statements that cannot return expressions

const r =  do {
    const a = 1
    const b = 2
    give a + b
}

More importantly, the current block syntax has fatal flaws

in rust, the block returns the final expression

let a = { if a { 1 } else { 2 } };
//             ^ return expr
//      ^ return expr x 2

but in this proposal, what the block returns is the side effect of the code running

let a = do { if (a) { 1 } else { 2 } }
//                  ^ not expr
//         ^ side effect

Perhaps we’ve reached the reason why we have different opinions then 😃. One of the major reasons I’m rooting for a required give keyword (not just an optional one) is precisely because it eliminates this confusion. With a required give keyword, you would be allowed to place a for loop, or anything at the end of the do block (no restrictions), and it will always behave in an intuitive way, because it’ll build off of people’s preexisting understanding of how functions behave.

If those same restrictions existed, even with a required give keyword, I’m not sure I would root for it.

Forcing linters to deal with multiple ways to do something is a high cost, and arrow functions’ issues with concisely returning an object literal are a great example of a pattern to avoid following. If the arguments for an explicit keyword are convincing, it should be required; if not, it should be prohibited.

The other nice thing about an explicit return is it makes a lot of the cases discussed in limitations less of an issue, so we can potentially lift them. If there is not a give in a branch, then the return value would be undefined for instance, which while not ideal compared to Rust’s implicit statement, would be very inline with the behavior of JavaScript as a whole and kind of fit the language better IMO.

And for folks who are feeling like give would ruin the benefits of do statements, and feel that ternary statements already fit the needs for things like conditionals, here are some examples of what really doesn’t parse well to me:

// Before
const COOKIE_WRITER_IMPORT = (
  BROWSER
    ? import('./writers/browser.js')
    : IS_PLAYWRIGHT_TEST_RUNNER
      ? import('./writers/playwright.js')
      : import('./writers/server.js')
).then((m) => m.default as (context?: BrowserContext | CookieRequestEvent) => CookieWriter);

// After
const COOKIE_WRITER_IMPORT = (
  do {
    if (BROWSER) {
      give import('./writers/browser.js');
    } else if (IS_PLAYWRIGHT_TEST_RUNNER) {
      give import('./writers/playwright.js');
    } else {
      give import('./writers/server.js')
    }
  }
).then((m) => m.default as (context?: BrowserContext | CookieRequestEvent) => CookieWriter);

// Before
let response: Response;

switch (url.pathname) {
  case internalConfig.routes.logout:
    response = await handleLogout(event, internalConfig, sessionKey);
    break;
  case internalConfig.routes.callback:
    response = await handleAuthCallback(event, internalConfig);
    break;
  case internalConfig.routes.login:
    response = await handleLogin(event, internalConfig, deviceIdFromCookie);
    break;
  case internalConfig.routes.currentSession:
    response = await loadSession(internalConfig, sessionKey);
    break;
  default: {
    let subRequestConfig = subRequestConfigMap.get(internalConfig.subRequestId);

    if (!subRequestConfig) {
      subRequestConfig = { config: internalConfig, depth: 0 };
      subRequestConfigMap.set(internalConfig.subRequestId, subRequestConfig);
    }

    subRequestConfig.depth++;

    try {
      response = await resolve(event as RequestEvent);
    } finally {
      subRequestConfig.depth--;

      if (subRequestConfig.depth === 0) {
        subRequestConfigMap.delete(internalConfig.subRequestId);
      }
    }
  }
}

// After
let response = do {
  switch (url.pathname) {
    case internalConfig.routes.logout:
      give await handleLogout(event, internalConfig, sessionKey);
    case internalConfig.routes.callback:
      give await handleAuthCallback(event, internalConfig);
    case internalConfig.routes.login:
      give await handleLogin(event, internalConfig, deviceIdFromCookie);
    case internalConfig.routes.currentSession:
      give await loadSession(internalConfig, sessionKey);
    default: {
      let subRequestConfig = subRequestConfigMap.get(internalConfig.subRequestId);

      if (!subRequestConfig) {
        subRequestConfig = { config: internalConfig, depth: 0 };
        subRequestConfigMap.set(internalConfig.subRequestId, subRequestConfig);
      }

      subRequestConfig.depth++;

      try {
        give await resolve(event as RequestEvent);
      } finally {
        subRequestConfig.depth--;

        if (subRequestConfig.depth === 0) {
          subRequestConfigMap.delete(internalConfig.subRequestId);
        }
      }
    }
  }
}

I encounter a lot of places where I avoid nested ternaries or switch statements because of the fact that they’re just really hard to parse visually, so I think the explicitness is really worth it here.

I’ve been writing a whole lot of rust lately which does have implicit returns. It took a while but I’m getting used to it. By default rust’s “clippy” linter actually tells you off if you use an unnecessary return and so it pushes you to get used to the implicit return case.

It does have some nice ergonomics to it - I’ve come around and I do find that being able to break / return from inside an expression to be really nice.

That being said, I think that rust has a few things going for it that JS cannot achieve:

First implicit returns MUST NOT end with a ;, and all other statements MUST end with a ;. This syntactic enforcement means that there’s always a syntactic marker as to which “expression statement” is the implicit return and makes it really easy to read code. For example a human or a linter can look at code like identifier; vs identifier and immediately say “the former is an unused expression but the latter is an implicit return”.

This sort of thing is missing from and (pretty-well) impossible in JS because semicolons are “always” optional (based on ASI rules). I would be happy to see it spec that semis are required inside expression statements, except for the implicit return - but somehow I don’t think that’ll fly 😅.

The second is that rust has a very strict and correct type system which does many things to prevent accidental footguns. For example if you write code like if condition { 1 } else { '2' } in rust it’s an error because rust requires all branches of a statement to return a “compatible” type. This means you have compile-time enforcement against accidentally implicitly returning the wrong thing and can’t accidentally do something like “forget” to return a value from a branch.

This safety also means that you can’t easily accidentally use an explicit return when you wanted an implicit one – because the return type on the function is checked. OFC we have TS for this - however TS does support union types - meaning it’s possible to accidentally do a lot of potentially broken things accidentally.

Rust also allows you to write break <value>; to exit a “loop expression statement” and return <value> - which is a nice property that adds symmetry to the features. I don’t think this is available to JS because the syntax is already reserved – break label means “go to label” and I don’t think you can change the meaning of it now (maybe you can - I’m no expert on what’s allowed in a spec). Which is sad because it means we can’t early exit from a loop expression statement without a variable to collect the value.


Personally I think that an explicit “return” keyword fixes a number of these problems.

  • it’s syntactically unambiguous which thing is being returned - closing off a number of footguns.
  • easy to lint / parse-time error against footguns like “multiple returns” or “missing return”.
  • easy to identify exactly which thing is the return without having to learn the whole eval implicit return rules.
  • allows “early-exit with a value” from a loop construct in a way that break cannot.

Sure an explicit keyword isn’t as cool as the “black box magic” that is the implicit return - but if the trade-off for the magic is easily broken code… I would rather lose the magic, personally.

This means, for example, you can choose to subdivide a longer function into do-blocks

If you’re trying to divide a longer function into smaller parts, presumably part of the idea is that the smaller parts are… smaller. So, again, adding a required keyword makes this less attractive.

(I should also say that I’m coming at this from the perspective of other languages, like Rust, which seem to me to have pretty well proven that a keyword is not required for clarity.)

@getify there is a difference between my example and yours. Your example is definitely difficult to understand - nested ternaries suck! But parsing it is a straight forward algorithm because you know each ternary has exactly three positions.

I think there is a huge difference between “difficult to parse code with multiple completion points” and “difficult to parse code with multiple returns”.

An explicit return keyword adds a clear marker that says “this is the value that the programmer wanted to return”. Implicit completion points do not have any marker - you have to parse the entire block to understand which expressions might be a completion point.

My example was contrived for sure, but subsets of it are constructs people will put within do-expressions.

When I was writing the above “bad” code I had no idea what each branch would actually do until I saw the transpiled output that babel produced. This is my issue with this concept and why I say it’s not intuitive.

I think it’s fine to have multiple completion points - I’m talking about the contrived examples where it’s difficult to at-a-glance reason about where those are. This is the same as it already works for returns.

Just wanted to mention that JS developers have widely embraced => arrow functions, which are the prime example of “implicit return”. Yes, it’s expressions rather than statement-completion-values, but… I think it partially demonstrates that JS developers really like “implicit return” semantics – whether that’s a good or bad thing. I don’t think extending that to statements will be nearly as big a jump as we made in moving from function to concise => form.

But, it’s one thing for a REPL to inform you what the completion value of statement/expression is, and it’s a very different thing to have to read code, that depends on your understanding of how completion values work. In the first case, if you don’t really understand how it evaluates the completion value, there’s really no harm in that. A REPL in any language will output the value of an expression, but I wonder how many people actually notice that JavaScript REPLs will also output a value when you place control structures in there - those values tend to be extra noise that we quickly learn to ignore.

So yes, there’s a small degree of precedence for completion values, but the amount is so low, that it’s sounding like people are even willing to make breaking changes to how completion values work, if they think different semantics would be more intuitive for do blocks. What we’re doing here is turning completion values from a minor concept you might stumble upon in a couple of dusty corners of JavaScript, into something that’s blessed with syntax, and that everyone must understand if they want to learn the main features of JavaScript.

I do believe that completion values are not that difficult to learn, but like @bradzacher said, there’s really not much precedence for this in the language (at least, not compared to a language like Rust), and turning them into something you have to learn to understand the basic syntax of JavaScript doesn’t seem worth it to me.

@theScottyJam I don’t see any consideration for the option of optional give… a compromise such that the expressions where it’s helpful (or just more readable) keep the keyword and where the most concise ones don’t.

I liken that to how arrow functions can have full blocks or concise expressions, depending on the needs/style preferences of the author. Sometimes the curly braces and return keyword are helpful, many other times, they’re skipped.

Optional give would let most usages skip it (if desired), but in certain cases (like returning function expressions) would actually be syntactically advantageous. The subjective areas of usage/not, thus, are handled by linters instead of being baked into the language.

I understand the reasoning for splitting these topics into separate threads… however, I have to point out that I think the question of “optional vs mandatory” is not an independent topic to “what should the syntax look like”, IMO. I may very well have a very different feeling on the syntax if it’s going to be everywhere vs going to be optional or only used in certain cases.

So, I think eventually we may have to merge these conversation threads back together.

You just add a dummy statement at the end, and voila, you can use the loop however you want.

The readme doesn’t go into this level of detail, but no, that would not be legal. Empty statements and blocks aren’t considered when determining if the do ends with a loop. See the actual spec text.

If those same restrictions existed, even with a required give keyword, I’m not sure I would root for it.

The restrictions exist because of the completion value semantics, so if there were a required keyword - i.e., no completion value semantics - then we would not have those restrictions.

(On the other hand, I personally don’t think this proposal is worth pursuing with a required keyword, so I don’t want to spend too much time talking about what that would look like.)

It’s possible that if we decide to have an optional keyword we might lift the restriction on ending a do with a loop, with the semantics that loops produced undefined. (This would make it unlike eval, but I don’t really care; I don’t think eval is a case where people do or should have experience.) Right now the restriction is there because the only way to get a value out of a do is from the final statement, and “the value produced by a loop” is confusing, but with give you might reasonably have a give in the body of the loop and avoid that confusion.

@bradzacher - I think what you had would be correct in earlier iterations of this proposal, but from what the README has now, ending a do block in a for loop will result in an early error (perhaps the babel implementation hasn’t been fully updated to support this?). Surprisingly, the README doesn’t mention anything about switch though, so I guess you’re allowed to end with a switch? I had assumed that would be disallowed as well. (though, I haven’t looked at its more complete, technical explanation)

Indeed, if the proposal changes to have a give keyword (however it’s spelled), given that making do { if (true) { give 3; } } be anything but “3 or undefined” would be pretty confusing, i believe that it’d have to map to undefined when a give is omitted on the code path.

all the branches would be return points, the same as in a ternary

Not quite. There is a dichotomy here. A branch can both be and not be a statement conpletement point. You don’t know if it is unless you parse the entire do expression

For example: do { if (X) { 1 } else { 2 } }. This follows your argument - each branch is a return point. However in do { if (X) { 1 } else { 2 } 3 } no branches are a return point because there is an expression after the if statement.

This is the complexity I am talking about. The branches of the if are statement completion points, until they’re not. And similarly - loops and switches have the same complexity.

A bodyless arrow function doesn’t have this same complexity unless you’re really writing purposely contrived code by chaining ternaries with other expressions using a sequence expression. Which is possible with the language, but unlikely (like eval, few people purposely use sequence expressions)

With an explicit keyword for the return, this ambiguity is gone. You know exactly which branch terminates and which branch doesn’t. It’s clear and easy for anyone to understand.


At this point we are talking in circles. It’s clear we are in different camps on this issue, which is perfectly reasonable.

In my opinion implicit returns in a do-expression isn’t a good addition to the language. Statements-as-expressions with implicit returns is one thing I can definitely get behind, but having it in the do-expression is confusing and complicates the syntax for little gain, IMO.

The difference is that you know that a chain of ternaries awlays returns a value from one of its branches.

But a chain of ifs in a do-expression might not actually return a value. It might - whether or not it does is entirely dependent on whether or not there are additional expressions after the chain.

Bodyless arrow functions are easy to reason about because you only have to parse an expression, which means if you can read any other JS, you can read a bodyless arrow function.

You can apply that same awful ternary from the body of an arrow function and put it a for loop variable initialiser and it holds the same. Adoption of bodyless arrow functions worked so well because it was taking an existing concept in JS and put it in a new location.

But let’s not get hung up on the ugly ternaries you could write (which you could also similarly put in the statement completion location of a do expression). Expand the scope to loops, switches, and declarations too - all of which are not valid in a bodyless arrow function.

My point: concise arrows don’t have that

As I’ve stated, bodyless arrow functions don’t need it because there is one and exactly one return position - the expression that it encapsulates.

Do expressions have 0…n “statement completion locations”.

There is a big difference.

I think there is a huge difference between…

I don’t see there being a “huge” difference. An arrow function returning a nested ternary has multiple branches, and there’s no return keyword anywhere to grab your eye and say, “hey, this thing could be returned here”. A do-expression with a bunch of if..else if..else logic branches similarly has no return keyword drawing your eye to each potential result expression. I don’t see how a ternary and an if..else if..else structure are "huge"ly different with respect to what we’re discussing.

Whatever “algorithm” (mental or otherwise) you imagine that can traverse a ternary, it can also traverse an if..else if..else construct with, IMO, similar difficulty. In fact, it may even be easier to glance-scan because tokens like if and else stand out visually more than ? and : do!

An explicit return keyword

My point: concise arrows don’t have that – and adoption/love of them has been quite substantial community wide – so why would a do block need it, to achieve similar reception?

@bradzacher

By “0 or more returns” - I am specifically referring to return locations.

I can construct a “bad looking” arrow function with multiple “locations” for a “return point”:

arr.map(v => (
   v % 2 == 0 ?
      v > 100 ? 24 :
         v < 24 ? v * 2 : v
      : v + 1
));

The fact that I have to mentally traverse/execute a complex nested ternary to figure out the result of that arrow, because there are 4 different possible locations, does NOT mean that arrow functions themselves are bad. Devs can always write complex/hard-to-mentally-figure-out code.

I agree with @ljharb that the do expressions will offer choices for improving readability, which responsible developers (who care about readability and maintainability) will tend toward.

if it is valid code - people will write it that way

Of course they will. They’ll write code with with and eval, since those are still in the language. But there’s no reason why the language should hold itself back because of its worst usages. We should keep pushing and encouraging and enabling better code.

FWIW, I personally don’t really think do expressions with if..else if..else conditional logic are a “good” usage, regardless of what syntax we end up with. I think pattern matching is the better construct for that.

By “0 or more returns” - I am specifically referring to return locations.

An bodyless arrow function houses a single expression. You are limited by what you can do in an expression location - but ultimately it is equivalent to exactly what can fit into the expression of a single return statement.

A do expression, OTOH, can have 0 (let x = do {}) to n return positions. Here’s an example with 4 (valid according to the current babel plugin):

  let x = do {
    let y = 1;
    if (condition) {
      y = 2;
      y; // 1
    } else if (condition2) {
      while (y < 50) {
         y += 1; // 2 
      }
    } else {
      switch (condition3) {
        case 'a':
          3; // 3
          break;
          
        default:
          4; // 4
      }
    }
  }

This is where the complexity I’m talking about is. In order to parse and understand this code you have to really, really understand how the “implicit return” semantics work. For example - for most people I’m sure it wouldn’t be clear that while (y < 50) { y += 1; } would return the value of y at the end of the loop, or that the break; after (3) does not void the fact that (3) is the statement completion value.

Having an explicit return keyword removes all confusion from such examples. No longer does anyone need to learn the bespoke logic around the statement completion value. They just need to understands plain old javascript.

I disagree that bodyless arrow functions are similar to the implicit return of the do expressions

An arrow function supports exactly one expression and thus one return. You cannot add if/while/etc statements, only an expression.

A do expression can theoretically have 0 or more returns and can have variable declarations, statements and non-implicit, unrelated returns.

They are very different things and conceptually they are very different things to learn and understand.

@bradzacher you assert that designing a feature with linters in mind is “smelly” design. I disagree, but I understand where that’s coming from. I agree with you in that I similarly dislike when features are designed where they’re only sensible/reasonable if TS is in play, for example.

Why I disagree, and feel that linters can be a valid consideration in the design process, is: I look at linters not as “making a feature useful or readable”, but allowing adaption in competing styles/points of view.

For example, many people really like having a linter rule to disallow any usage of var. That’s fine for them. I however vehemently disagree, and prefer the option to use var when I want its behavior. I know many members of TC39 would prefer var to go away – in fact, maybe even a super majority of them feel this way? – but if they had just banned var in modules or classes or something like that, I feel it would have been a HUGE detriment to the language.

What they did was add let and const – in my mind, these augment var rather than replace it – and then leave it to user-land style guides and linters for people to choose what usages of each of those they want.

I think that’s actually superior language design, to give devs powerful tools and let them mold it to their own needs.

As it relates to the ideas being discussed here: if TC39 made give optional (like they made let and const optional), there’s plenty of room for teachers, bloggers, and linters to all define their own “best practices” for when give should be used (or not), and let different feelings and use-cases adapt.

I am a teacher of JS (and author) so I sympathize with larger surface areas of a language creating more to teach and more for learners to grok. But there’s been plenty of precedent for such teachers/thought-leaders defining their own “good parts” subsets of the language and getting their followers to jump on the wagon. Optional give would be no different, IMO.

Thanks @pitaj for your insights.

So, here are some of my thoughts around this.

Your argument about readability of do { if (x) { 12 } else { 54 } } vs do { if (x) { give 12; } else { give 54; } } is certainly a valid one. In the former, the “12” and “54” are simply expressions that you’ve pieced together. In the latter, “give 12” and “give 54” are explicit instructions, they’re statements, and makes the code feel more like a list of instructions rather than a single expression.

So, I can concede on that point and agree with you there.

However, I do feel the value of implicit-give is tied to a very limited scope of use cases, which I’ll like to quickly explore. First of all, the “implicit give is nice, because it keeps code expression-oriented” idea quickly breaks down when you put, even a touch of procedural code into the do expression (which, you did acknowledge that your idea breaks down as complexity increases). The moment you place a statement within the if or else block, you’ve turned what was, conceptually, a single expression tree into a procedural step-by-step list of instructions. For example, In the following scenario, I don’t feel the implicit “give” is providing any value (unless you disagree here? If so, feel free to elaborate why.)

// Without "give"
return do {
  if (x) {
    const y = f(12)
    format(y)
  } else {
    format(54)
  }
}

// With "give"
return do {
  if (x) {
    const y = f(12)
    give format(y)
  } else {
    give format(54)
  }
}

(Forgive the contriveness of this example, I realize it’s dead simple to just nest the function calls to create a single expression tree)

Both of these code snippets require procedural, step-by-step logic. The explicit “give” doesn’t pull us out of “expression-only” land and into “procedural” land anymore, because we’re already in procedural land. For the omission of a completion-value marker to provide any value, we would have to ensure our do block contains a single expression tree (A statement at the end of an implicit-give do block is effectively an expression in my book, so when I talk about expressions, you can mentally lump those in as well).

Ok, so we’ve (arguably) established that implicit-give will only provide value when the do-block contains a single expression tree. It would be stating-the-obvious to also mention that, in this scenario, we must be using at least one statement within the do-block, otherwise, why would we use the do block at all? (the exception is the pattern-matching scenario you brought up, which I’ll discuss lower down). The four non-side-effecty statements that functional-minded people may wish to turn into expressions are as follows:

  • throw (which will hopefully become an expression anyways via the expression-throw proposal)
  • switch (which will be superseded by the match proposal)
  • if (assuming you’re not using a single if-else, which can easily be done via a ternary)
  • try-catch (a less common need in an expression location, but still valuable)

In other words, an implicit-give is really only providing value for two specific scenarios - turning if-else and try-catch into an expression.

Here, again, I’ll concede and say that I would really love to have an expression version of if-else - it just reads nicer than nested ternaries. An expression try/catch would be nice as well, but I wouldn’t be too bummed out if we don’t get one, as I don’t use them often enough for it to matter too much to me.

I don’t know if you agree with everything I’ve said thus far or not, but if you do, this means we’ll be left with the following decision:

  1. Implement do-blocks with implicit-give
  2. Implement do-blocks with explicit-give, and find some other solution to solve the expression-if (and maybe expression-try-catch). I know a number of solutions have been proposed on this repo, I hope something will come through.
  3. Implement do-blocks with explicit-give, and don’t provide a solution for expression-if. (I know you’re adamantly against this, and I would certainly be bummed out by this as well).

The downside to option 2 is the fact that we have to add an additional feature to the language, while option 1 lets us roll this idea into do-expressions (at the cost of making do expressions a little more complicated - I won’t enumerate the reasons why, as that’s been extensively discussed in the other thread). I’m not a fan of the complexities that implicit-give adds to do-blocks, which is why I root for option 2. On the other hand, you seem to be much less worried about those complexities, probably to the point that the complexities of a new language feature (option 2) exceeds, for you, the complexities of implicit-give.

Do you agree with this assessment? Disagree? Partially agree?


Finally, I want to briefly mention this pattern-matching thing, which could cause people to use do-blocks that contain a single expression-tree without any statements, like you showed in your patter-match example. It is certainly true that your pattern-match example is nice and concise, and an explicit “give” in each of those blocks would unnecessarily make it feel a lot more procedural-like. However, it’s still an open question whether or not plain expressions would be allowed on the RHS of a match. If you are allowed to provide simple expressions (by omitting the curly brackets), then there wouldn’t be a need to have expressions without statements in a do block.

const val = match (count) {
   when (0) "none";
   when (1) "one";
   when (2) "a couple";
   when (^(count <= 5)) "a few";
   else "many";
}

If do-blocks decided to go after an explicit-give, I would half-expect pattern-matching to then make sure their syntax provides a way to have simple expressions on the RHS.

Update: It looks like the pattern-matching have been closing in on the desired syntax and semantics for that proposal. They’ve now ruled out a number of syntax options from that github issue I linked to earlier. The remaining options all have a way to put a bare expression on the RHS of a pattern-match arm. In other words, pattern matching will not be a reason for someone to put a bare expression-and-no-statements in a do block anymore.