SE-0380: `if` and `switch` expressions

This example is a bit misleading, because you are essentially starting from a position where the workaround is already applied.

There is a big difference, however: if we were to start with this proposal, and the author wrote

let result = if condition {
  3
} else {
  4
}

then, in the case when the user adds an additional statement above the 3, the compiler has what it needs to produce a fixit, rewriting their code in the DI-based form. Without this feature, there is no hook to introduce this to the user. They must just know it.

I disagree with this framing, as I believe even if the final state was that only single expressions in if were supported, a brace would be the correct spelling. A brand new syntax seems entirely unnecessary, just another syntax to learn. This was already the conclusion reached in SE-0255. In that case, return-less functions are limited to a single expression, but a new syntax for that form was rejected.

Choosing a brand new single-expression-only syntax like if p => e1 else e2 or var x: Int => e on the other hand would close off avenues, suggesting that a multi-statement expression syntax was never coming. Or if it did, would land us with an oddly duplicative syntax.

So this leads to the question: must deciding on multi-statement expressions be a precondition of this proposal (despite the precedent of 0255).

Let’s run through the options for multi-statement expressions. I think this list is comprehensive of all the coherent approaches seen so far:

  1. Use return to mean “make this expression the block’s value”. I think we need to rule this one out as definitively a bad direction. It is already a problem that closure-based control flow like forEach { } or 5.times { } lead to confusion between return meaning continue. Repurposing return specifically within if or switch would make that worse. And it conflicts with the goal of allowing explicit function returns.

  2. Introduce a new keyword instead of repurposing return. This is the path Java chose (though their choice – yield – conflicts with another potential use in Swift). This seems too heavyweight a solution to me, to introduce a whole keyword just for this purpose. I admit I don’t have a good argument for this other than that “adding a keyword for syntactic sugar” seems self-defeating.

  3. Adopt the “last expression” rule, the choice of Ruby and Rust. The big win here is it solves not just if but also extends SE-0255. But I see that part as a bug rather than a feature. Again this is a feels-based argument but whenever I see a function in Rust end with a bare true or nil or result I find it very unsettling. I much prefer Swift’s current requirement of an explicit return.

  4. An idea that @Joe_Groff put out there, I believe as a joke that I then started to take seriously, is a variant of the “last expression” rule where you must join the expressions with ;. So if p { log(“true branch”); 3 } else { 4 } would be allowed, as would func f() { log(“f”); 3 . This is more explicit than 3), but re-purposes ;` to give it more meaning, which is maybe not a good idea.

I think 1-4 are all not great solutions to the problem we face today with this proposal. Maybe I am in the minority regarding disliking 3, but it would undeniably be a big change for the language that I’m reluctant to rush into purely for the purposes of solving multi-statement if expressions.

This leads to what I think might be a plausible path forward:

  1. The last expression is the value of the branch, but only within if or switch expression branches. This is essentially option 3), but not for function returns, which would still require an explicit return keyword when longer than 1 expression.

The discomfort I feel about implicit function return doesn’t apply here. Making an if an expression, by putting it on the right-hand side of an assignment or an explicit return, is explicit enough for my liking. I also suspect it will in nearly all cases be used for short expressions where it’s clear the if is an expression, whereas my experience looking at rust code is that it can be common to have relatively long functions that just end in an expression.

This also leaves the option of expanding implicit return of SE-0255 open by implementing 3), without requiring that direction.

There is an implementation, along with a tool chain to try this out, available on this PR.

14 Likes