Pitch: Multi-statement if/switch/do expressions

This is a good idea for tooling. If "return" isn't returning from the outer function then coloring it differently would make it a lot easier to read.

1 Like

Hah, my usage of the term “color” here was a cultural reference to this. But it’s not a bad idea either when taken literally as you say.

1 Like

I'm using "natural" in the "ordinary" sense, not the "innate" sense. And this is simply based on observations in the "wild". I'm not asserting "natural laws" or anything like that, just that it's a common behavior I've observed in myself and team members trying to deal with the current expression behavior. Once you hit the "non-expression branch" error due to having more than one line, the first thought is to explicitly return there.

I don't recall anything like that in the previous proposal's discussion, nor do I see anything about it in this pitch. I recall the original version of 380 making return usage ambiguous by allowing returns through expressions, but I don't recall it being part of the original justification.

What good does this sacrifice give you? The absence of side effects - no. Predictive control flow - no.
There should be a good reason to make x = do { ... } and do { ... } behave that different.

I can see this as compelling at first, but I think once you extend this to "do" it starts feeling sketchier. It feels like it is really changes the meaning of the "do" keyword significantly just by it returning the value.

I was also unsure about this essentially turning branches of the if/switch in to imperative sub-scopes, but if full expressions are supported later that probably eases my concerns. Right now to do a complicated expression you need to use these sub-scopes to introduce let bindings, but that probably will not be necessary in the future. In other words, they will probably just be used for imperative code in the future when "Full Expressions" from SE-0380 are adopted.

SE-0380 was accepted with an amendment banning all non-error control flow in if and switch expressions.

The discussion about return ambiguity was in reference to the immediately-applied closure workaround for the lack of if/switch expressions:

Finally, a closure can be used to simulate an if expression:

let bullet = {
    if isRoot && (count == 0 || !willExpand) { return "" }
    else if count == 0 { return "- " }
    else if maxDepth <= 0 { return "▹ " }
    else { return "▿ " }
}()

This also requires returns, plus some closure ceremony. But here the returns are more than ceremony – they require extra cognitive load to understand they are returning from a closure, not the outer function.

1 Like

That's not ambiguity, that's just knowing how closures work. It's really no different than using return from within expressions, and is one of the reasons why I don't think there will be confusion here. Besides, in a language where this construct is still allowed, not to be mention having nested functions in the first place, I can't see the issue. Users will still have to know how all of these things work, so why not build on that knowledge rather than requiring a completely new keyword that only works in limited situations that look exactly like instances where you'd use return?

2 Likes

I'm not sure it would be confusing in all cases, however I don't want to debate that here. If we were just to allow return out from the "catch" branch of do/catch that would still be a problem if "return" were adopted over "then" inside expressions. I'd also like to leave this open for returning from function in "guard" blocks.

I think there is probably a chance of this amendment not applying to "do" blocks where it makes less sense.

I would prefer not using the same keyword and then getting stuck with that decision years later. This just doesn't feel set-in-stone enough for that.

Even without ability to return from function, I still prefer "then".

Basically the same thing that amendment was aiming to solve, but coming from the other direction. Returning from an expression is confusing so don't do it. Making it look like you are returning from an expression is also confusing. Particularly when the control statements look almost exactly the same.

This feels better to me:

let color = if selected {
  let x = fetchUserPreference()
  then x ?? .blue
} else {
  .grey
}

Then:

let color = if selected {
  let x = fetchUserPreference()
  return x ?? .blue // at quick glance looks like return from function
} else {
  .grey
}

Besides, "then" isn't such a bad keyword. It is generally avoided as an identifier since it is often a keyword in many languages. The lack of semicolons means Swift needs a lot of keywords, so we shouldn't be too critical of adding them.

return has always been "return from the called function". In order to return the code has to be called in the first place. But these expressions don't use parenthesis, so I guess it would be misleading to treat them as being "called".

4 Likes

I think we should allow this, for the same reason I think we should allow throws and Never returning functions on the right hand side of a ?? If control cannot continue past a point in an expression then that expression should not be reuired to resolve to a vaue after that point.

For this reason, I am against using the return keyword to yield a value from an expression, since it would prevent this sort of short circuiting. I would prefer the implicit last line approach, but if that's infesable the. I'd settle for then. Not because I think it's a good keyword, but because I can't think of anything better.

4 Likes

To the vast majority of users the distinction between returning from a function and returning from an expression using then (boy, that rolls of the tongue) doesn't exist.

As an aside, what do you call the act of providing a value as the result of an expression that's not "returning" or some similar form?

Aside from feeling this language is strangely becoming a better C++ ((C++)++ if you will) in terms of complexity (I wonder if Swift itself had been written in Java or C or something else if that would have impacted its DNA, not saying it could have been… just a thought…), how could this logic for the use of then and return be better than overloading return?

Anyways, for now I will happily still use separate functions ( {}() ) for expression that are longer than XYZ (like the complex cases here… what is the danger of {} () aside from feeling odd? Is it worth creating yet another keyword that looks like a return but it cannot call itself as such?).

————

Thinking about it again and the rationale of SE-380: https://github.com/apple/swift-evolution/blob/main/proposals/0380-if-switch-expressions.md

This was seen as having a high cognitive load (because return is returning from the closure passing the result to the variable instead of the function containing it):

But not we are looking to extend that proposal to fill the gaps with something that judging by the length of this thread does not have an easily reachable consensus/ shared understanding and still begets more refinements (see future directions for guard, etc…):

It is impossible so I will not repeat it but it kind of puts into question both this pitch and SE-380 for me… aside from a bit questionable additional cognitive load the {}() hack seems to do all we need multiple evolution proposals and new syntax expressions to do. SE-380 seems to open new problems up more than closing them :/.

It is me misunderstanding how the dangers of the closure “hack” are much much more complex to reason about than what we are discussing here, so please educate me here as I am missing something… please.

3 Likes

It also not odd and makes sense if you used to, like in all this examples people shared it’s basically adding a side effect to expression and closure makes sense.

And also, I know return been answered already several times that’s it’s for functions, but think Swift new learners will just to try return first instead of then.

I will search, but I do not recall it explained more than it is because it is… yes it is meant for functions, now. I am trying to understand the problems with overloading.

Then again I keep going back to SE-380 and the following refinements maybe making a problem that was not really there…

it’s basically adding a side effect to expression and closure makes sense

I am trying to understand the justification beyond the one I found in the proposal for why we have to add the complexity and discussion SE-380 and this and follow up proposals add… but :man_shrugging:

1 Like

I've read the proposal and read a few of the posts, and while I support the motivation behind the change the addition of the new keyword then for this makes it feel incredibly janky.

I don't want to say no to the proposal entirely as I'm all for the motivation, but I would prefer exploring the alternative of omitting the keyword entirely and relying on the final expression rule making the whole thing feel more natural. Swift omitted semi-colons because they weren't needed (for the most part), I think adding a keyword here has the same sentiment and just adds to the visual noise and makes the whole thing harder to read.

For whatever it's worth, I'm pretty sure linters would soon support rules that prohibit this kind of nested inline expression-evaluation with side-effects. It seems that the expressions quickly become unweidly and error-prone. Or at the very least, hard for humans to read at a glance.

2 Likes

Thanks for the suggestions on how to interpret the braces
(I had also tried to see it like that). Another way to see
it could be to treat the whole block as if it intrinsically
preludes with a then.

Here's an example using then, to illustrate the redundancy:

inputFromUser = if ready {
    then getInputFromUser() // also sets global variable userInput
    handleInputFromUser() // this function uses userInput
} else {
    nil
}

First of all, it's a tautology. Secondly, in this particular case it
doesn't help to see then as indicating something that happens
later on (as was suggested by @cmonsour).

In the above example, then is being doubly redundant – both in
terms of when something happens (because that is apparent from the
order of the statements) – and in terms of constituting a consequence
(that's what the braces do). It is not only doubly redundant, but
also misleading if we interpret then as "later on".

The keyword should have only one purpose, and that is to point out
the result to be sent back for the assignment. The word then doesn't
signal that we are sending something back.

Here's the same example, this time with use:

inputFromUser = if ready {
    use getInputFromUser() // also sets global variable userInput
    handleInputFromUser() // this function uses userInput
} else {
    nil
}

Keyword use simply sends back the result – it doesn't have to be
interpreted one way or the other. That means less cognitive load.

From the proposal:

So the example where then is not the last line in the branch would not be permitted.

Oh, I see. It feels a bit limiting, but I guess there
are good reasons for doing so. Thanks!

Swift omitted semicolons because it used keywords to start new lines so semicolons were unnecessary.

Implicit return is somewhat counter to the original language design. Expressions and result builders created an exception to this long held rule. I don’t think SE-0380 is a bad design, but it should be reserved for pure expressions without any imperative multi-statement code which this isn’t. Swift really needs keywords on every line of a block- ideally at the start of the statement.

It is possible we could allow implicit returns on the last line of a block when using semicolons, but I think that should be in addition to this proposal instead of supplementing it. Implicit return just isn’t otherwise possible without majorly reworking the language. I think we just need to live with the fact that implicit return is for pure expressions only. For consistency this should use a keyword at least as an option.

2 Likes