[Pitch] Last expression as return value

I do love a good study on language!

I had some trouble sourcing the one you mentioned, though… and I do like to read original source material if someone thinks it’s worth citing.

Could this be it?

(Searches on redundancy and language turns up a variety of info, any extra key terms you could provide would be great!)

1 Like

I couldn't for the life of me tell you the exact studies were being discussed when I was in college some 15 years ago, but this relatively new one summarizes the point decently well. From what I remember, long story short: when you know a language well, redundancy helps a bunch (but mostly for speech, generally it'll be glossed over when typed out), and when you don't, it tends to be dropped when producing new utterances.

I am in no way suggesting that something like return complicates someone learning swift all that much (if anything, the lack of the keyword is likely more surprising to programers who know literally any other programing language), but leaving it out when unnecessary and relying on specifying a return type instead would simplify the amount of effort a newcomer needs to pick up the language, as it's one less thing they need to remember to do.

Since we are dealing with an artificial language where partial fluency results in a compiler error, the more we can simplify to one required mode, and other optional redundant modes, the better it is for everyone: proficient coders can write with redundancy built in (specifying types proactively, skipping syntactic sugar, documenting code), while novices can get their feet wet with a simplified grammar that also works, with many of those redundancies dropped.

7 Likes

I was skimming through this and the "old" pitch thread and figured maybe I should provide some clarification for some of the Scala callouts made throughout the threads. Since it's a prominent example of the last-expression-as-return feature and some have mentioned it causing unexpected problems; I've spent close to a decade with Scala code and was consulting developers of various skill level back then in their Scala usage.

I'll introduce the "known problems with this" in Scala and compare if we'd have the same problem in Swift or not:

--

The "gotcha's" in Scala:

The implicit return had two (or three I guess) very specific problems:

  1. unexpected return from outer scope from closures

Which produces:

WARNING: * Non local returns are no longer supported; use boundary and boundary.break in scala.util instead

  1. unexpected inferred function return type

Scala 2 (old):

Which leads to unexpected inferred types. The problem honestly only shows up as real issue at function boundaries because when you write smaller functions and ifs you rarely end up with weird types inferred. (Scala 3 solves this by introducing union types: in the above example the return type in Scala 3 is Int | String.

  1. (a) is a similar problem where randomly throwing in an early return can make inferred return types confusing... So nowadays doing this is even triggered as an error:

ERROR: method fun has return statement; needs result type

Nowadays both are either resolved or banned in Scala.


So... does this proposal lead to the same issues in Swift?

IMHO: No, it does not.

  1. return in Swift behaves in expected ways, returning from whatever scope it was in;
  2. Swift methods cannot omit return types and they must be spelled out.

I believe this proposal is a good positive addition, and solves issues that we have today:

  • early returns look easy to spot because the return keyword is a clear "things happening here!"
  • early returns are often paired with guard - I think it is good to keep guard+return pair. we signal that it's an early return.
  • unexpected return types getting inferred in methods is not a problem because Swift requires explicit return type annotation (this is good)
    • one might argue that it happens in closures; however closures are supposed to be small pieces of code -- if your closure is getting "huuuuuge" this is another good reason to make a method for it.

Overall I'm in favor of this because it rounds out the language.

Currently we're in a messy state and the "single line branch body can be used as expression but multiple not" is a confusing cliff in the language which breaks normal programming practices like introducing a temporary variable or even just breaking up a few calls that could be mashed into one horrible too-many-characters long line, and encourages writing clear code.

At least in my experience: In my 6~7 years of Scala use but also lots of teaching and consulting newcomers to Scala way back then before my days with Swift this specific part of the language wasn't really the difficult part and except those gotchas (which Swift won't suffer from), it has made code much less ceremonial and clear.

So, at least on a personal note, I think this is a very valuable improvement that makes Swift more clear to read and I personally look forward to it! I've been hitting the "eh, I need another line in this if" far too often recently... :sweat_smile:

31 Likes

Thank you for the Scala context.

I agree that the surface area of potential issues is much smaller in Swift vs. Scala because Swift requires explicitly declaring return types for methods.

While you acknowledge the issue still exists with closures, I believe you downplay the problem.

We can debate about if closures are “supposed” to be small. It is still the case that they often are not small in practice.

Furthermore, Result Builders and SwiftUI-like APIs strongly encourage large deeply nested closures. I and others have raised this issue extensively here and elsewhere in this thread. (Sorry I don’t want to beat a dead horse.)

Granted, we’ve discussed earlier in the thread that after this proposal, result builders will not behave in the same way as non-result builders. And so many of the ambiguities I raised earlier in this thread would not be much of an issue.

However, Result Builders are often implicitly applied and are invisible to the call site. So that increases the confusion of why last expressions appear to behave one way in some contexts (a non-Result Builder) but another way in other contexts (an implicit Result Builder).

4 Likes

I very much agree with the above sentiment and therefore I am a strong -1 on this proposal.

I already find the currently-allowed self and return omissions to be occasionally confusing. There are times when I see a variable and think to myself, “Where does this come from?” only to find it belongs to self. I already find myself looking through code sometimes and having to parse it very carefully to figure out where the return value is coming from. This proposal feels very much like saying, “Let’s take the more confusing parts of the language and double-down on those!”

I can't help but think of the axiom, “Things should be kept as simple as possible, but no simpler.” Also, ”readability counts.”

14 Likes

By using a keyword to return from an expression we would be able to use control flow statements like guard in a do block (or any other multiline expression), which would make it consistent with functions and closures: I'd argue that this would round out the language, while accepting last expression as return value will instead introduce another corner of specific limitations.

Also, any argument like "things should be small" is, to me, just plain wrong, because replacing a long expression with a separate function call will make code harder to follow and understand: a specific keyword will also help in crafting and understanding longer multiline expressions.

I quickly surveyed some of my code to check for opportunities of using do expressions, and I found some cases that would look very good with this pitch, but many others simply wouldn't work because of the need of guard, so I would need to use the plain old immediately executed closure, that as we know makes things slightly more confusing due to the return used inside. Having the possibility to use a specific keyword to return from a do block will guarantee better clarity thanks to the distinction between that keyword (return just from the enclosing expression) and return (return from the whole function).

2 Likes

Personally, against this change. The return keyword makes the code clearly explicit on where and when code blocks exit. One of the goals of Swift was to reduce bugs, having code the automatically exit at the last line/expression creates enormous opportunities for incomplete code the correctly compiles.

For example, I routinely break long calculations up into smaller blocks of code that are easier to read/check, but I don't want to return anything until I'm done with my calculation. Having to specify the specific value I'm returning means that that my code won't compile until I've completed my calculations with the correct, designated output.

Additionally, Swift was designed to be easy to interpret and read. The return keyword makes it explicitly clear what the code will do and what it is intended to do. The goal wasn't to make the smallest possible language with tons of must-learn rules before someone can interpret the code.

I think this pitch is mistaking brevity for clarity and needlessly introduces new opportunities for buggy code.

14 Likes

Personally, I believe we should not implement this change. Using "return" makes perfect sense when looking at unfamiliar code, as it allows for a quicker understanding of what’s happening. The same goes for omitting 'self'; I still think it was a mistake to allow this. I often find myself needing to double-check which variable I’m currently working with.

15 Likes

Sorry, that I didn't read whole 449 posts, but a couple of questions.
What is the status of this pitch? Will it become an SE proposal, or the conclusion was against it?

In general, if Swift was developed from scratch, or if we would be willing to break existing source code more willingly, last expression as a return value would be excellent idea. A problem is that it might break some existing code.
For example - an often used pattern for a function with side effects to return a value (which is often discarded) and to be called without caring for the return value. I do believe it is not a good idea in general, but doesn't change the fact that it is happens often. And it pretty common pattern when calling C code for example.

func produceSideEffects() -> Something { ... } 

Now, every closure of type

{ ( // parameters) -> () }
// do stuff
produceSideEffect()
}

will suddenly change it's type from (parameters) -> () to (parameters) -> something.
All such code is expected to break.

However, I do believe that it would make sense to limit this to if/else and switch expressions. At the moment, if/else expression is just a ?: operator with much more verbose syntax that can't be used everywhere ?: is used, so it is kinda useless (except is someone prefers if/else to ?: for aesthetic purposes). Allowing several statements within if/else expression would make it a much more powerful and useful construct. Same for swtich expression - it is slightly more useful that if/else as it allows avoiding nesting ?: but again - allowing several statements within it would make it much more useful.
It could make sense for functions to make return word optional as well - as the type of the function is known and is not inferred, but would cause mess and confuse people, and make the code less readable (if same pieces of code do the same thing and equivalent, why make some words optional)?
For closures - I believe it would be a huge headache.

But if possible - please make it available for if/else and switch expressions. (Or remove if/else expression, it is almost useless in it's current form)

As the migration could be easily automated by the compiler and this feature has no ABI concerns, if this pitch were proposed and accepted then compatibility concerns could be addressed by limiting it to if, switch, and do expressions and leaving the full version behind an upcoming feature flag.

Having used this feature in both Ruby and Rust, I can say that the only problems with I've ever had with implicit return have been solved by static typing.

I am sure this has been asked before, but wouldn't do expressions on their own solve this almost as well, but with none of the downsides? At least it seems that way to me, looking at the motivating examples.

2 Likes

This is actually one of the features I hate most about Rust. From my experience with Swift the main place this is a problem is with switch/if expressions, so can we just focus the solution on the actual problem? Also agree that do expressions could further help (but would need a similar solution), but the overall clarity that explicit return statements provide would be really sad to lose

6 Likes

I’ve never enforced features disabled using linting rules with Swift. But disabling this would be one I’d opt to enforce. I’ve already had done many discussions on the controversial one-line return statements. I think those are fine given how they’re used in situations like map. This is not necessary however - or I don’t see why it would be.

7 Likes

After all the "Why the heck did this come to formal review?" dramaturgy regarding InlineArray sugar, I just want to say that I'm really hoping this particular issue is still bound for formal review.

I'm not even necessarily a +1 on this. I sympathize with many of the reservations expressed in this thread. But as uncomfortable as this pitch might be, the current regime of single-expression-only automatic returns is equally peculiar. The issue deserves a definitive resolution one way or the other.

4 Likes