And if, instead of "bye", you had a @discardableResult function call, there would be no warning.
I personally like old/newValue much better than the $n variables (the latter are quite ugly to my eyes, I'd prefer something like parameters.0 or parameters[0] instead). YMMV
@discardableResult functions are quite niche, we could even make them follow another exceptional path so that:
func cook(vegetable: Vegetable) -> Vegetable {
print("Bzzzz")
vegetable.cook() // Error: discardableResult functions need explicit return!
}
Likewise we could go as far as promoting those warnings to errors if it is "return value" that's at stake...
If in a well-working code a change makes you to add more changes to cover for its downsides further and further, than I would reconsider the initial change. I feel the same about this proposal and promoting a warning to an error has the same vibe, just in terms of the language. There is nothing inherently bad in either of these changes, but the chain they form looks alerting to me.
I wouldn't say it's subjective in the case of implicit self either. As an example: look at this. A log function took a level parameter, and then ignored that parameter, instead accidentally referencing an instance variable of the type. The result was that everything was always logged at info level, regardless of the intended level of the log. This package was used by the AWS SDK for Swift, and as a result, logging was a complete unusable mess with that SDK, with critical errors and warnings impossible to spot in the flood of informational messages. And this was AWS. By Amazon, one of the largest tech companies in existence. And it was like that for years. Wouldn't have happened without implicit self.
I think this pitch identifies a genuine pain point. The question is what is the best way to address it.
@ben-cohen mentioned here that the thread has proposed a continuum of options:
- Introduce a new yielding keyword like
then- Introduce
doexpressions with a last expression rule, use them to solve theif/switchlimitation- Use trivia like
;as Rust does- Introduce a last expression rule for
if,switch,do, but not functions/closures- Go all in on a last expression rule
To me each option has tradeoffs, and none of them feel like the one. My opinion keeps changing, but I tend to lean on the side of the first two.
In the last version of the pitch, the consensus seems to have been that the community doesn't want another keyword. I'm somewhat sympathetic to this. Swift has a lot of keywords and it can be quite daunting.
But I think at times, it can be problematic to implement a new feature with no new keyword, or reusing existing keywords for new purposes. (Sometimes reusing can be a good solution. for await loops are a good example. But reusing is not guaranteed to be a good solution.)
This test may seem silly but I think it can be helpful to ask ourselves:
"Is this new language feature Google-able?"
In other words, when I see this feature in real code, if I don't know what it is, will I be able to search for that information?
For example, if we hypothetically added a new then keyword then that would be very easy to search. Type in then keyword swift and you'll probably find something helpful.
If it's not Google-able, then it's often difficult for me to even ask a human colleague to explain it, because I may or may not know enough info to phrase the question correctly?
Some may say, "well just read the documentation", but this is still a similar problem. If I don't know the name of the feature, it is harder to know where to look in the docs.
Some may say "Well you could just ask an LLM." but LLMs are not great at this problem either. They will usually be trained on prior versions of the language and won't realize which feature is valid for which version.
Whatever solution the community chooses to address this pain point, a very large portion of users will first encounter the new feature in the wild, in code that doesn't work the way that they expected it to work. Their first encounter with the feature will most likely not be in the docs or in an official language blog post etc. How difficult will it be for them to learn the new feature, and how hard will it be for them to find the resources necessary to learn it?
Also relevant: Operator symbols can sometimes be very not-Google-able. Many times I've encountered a new operator. I don't know what it's called. I just see the symbol. I search for the symbol, and I get completely wrong search results because the symbol actually conflicts with the search engine itself. (For example there's a band named !!!, pronounced ChkChkChk. If you google "!!!" you actually get 0 search result. How is anyone supposed to find that?)
(This is certainly not the only consideration, or the largest, but I think it should be highlighted now.)
I don't really support allowing omitting the return keyword everywhere in the language. This doesn't improve clarity or usability.
I would support this functionality just in if/switch expressions specifically since that addresses a real usability cliff. I see the appeal of that direction over a new, niche then keyword. I also love the addition of do expressions, and think that stands alone independently from the rest of this proposal.
If we do move forward with the proposal, I have some concerns about the carve-out specifically requiring returns in guard statements:
Implicit returns inside
guardstatements are not proposed:func f() -> Int { // error: 'guard' body must not fall through, consider using a 'return' or 'throw' to exit the scope guard .random() else { 0 } 1 }Guard statements can contain more than just returns â they can continue/break, abort, or throw, and so an explicit return is still required.
To me it seems unreasonable to argue that omitting the return keyword results in equally clear code everywhere in the language except for guard statements. Every function, block, and closure can contain more than just returns (continue, break, abort, etc), not just guard statements.
If there are usability and clarity concerns with omitting the return keyword in guard statements, then this feels like a concession that omitting return hides control flow and makes the resulting code more difficult to understand. To me, the desire to carve out an exception here is a strong indication that this proposal isn't the right direction.
If do we accept this change, because we believe omitting the return keyword doesn't harm clarity, then I think we should also allow omitting the return keyword in guard statements.
Iâm not sure if I fully agree that this is totally subjective. Swift gives certain amount of freedom to write the code, and as has been pointed out the the topic, omitted returns can be the subject of issues in the code you might not be aware of. To me this is more like treating null/nil as boolean in C-family languages â nice feature that looks harmless, but proved to be error-prone in practice.
This is precisely right. There is (1) no need to make the last line automatically the return value â it ads absolutely no functionality and provides very little expressiveness to the language (if at all). But more importantly (2) it makes it more difficult to actually evolve frameworks and read other peopleâs code. The very second you have to extend the function and add more code you had better remember that itâs no longer the last line and add the very return that you elided! And explicitly marking it as return and not having the compiler guess what the return value could be, is precisely a core principle of Swift â clarity and reducing unsafe code.
The very idea of moving away from a system that is explicit only to introduce a possible path for error prone code is counter intuitive.
I am certainly hoping this gets rejected but if it doesnât, at the very minimum it should be an error to elide return in âSafeâ build mode that is being pitched.
I absolutely support a new keyword to exit control flow statements that âreturnâ values.
IMO people making an argument against implicit returns should also disclose their stance on implicit/explicit
self, and then also argue for the deprecation of implicitselfaltogether for that argument to make sense in the first place.
Iâm against implicit self.
I donât think keywords are noise either.
Iâm also against trailing closures because they create inconsistent syntax between call site and the declaration.
I think that builder functions are a mess.
The later two were designed to allow an interesting new way of using Swift, to sort or hijack it have declarative constructors. So at least they added a lot of value to Swift and we can get a lot of functionality out of it.
Eliding âreturnâ allows the compiler to guess that your last line is the return value in cases where itâs not explicit and essentially validate a function you may not have even finished writing but worse may return a value you did not intend. A person reading your code is not immediately made aware of your intended return type and if you end up extending a function youâd end up having to write the return keyword anyway.
It adds no expressiveness, and creates a new error vector.
One very much can elide âawaitâ or âtryâ. The compiler absolutely knows when those functions being called are throwing or asynchronous. But I wouldnât recommend allowing it because like return it has big side effects that could cause bad assumptions on the part of the programmer and one reading the code in the same way eliding Return would. But eliding âtryâ and âawaitâ could theoretically be safer because the compiler isnât guessing anything like eliding return does.
I advocate that allowing eliding return in single line closures be permitted because it is very very obvious what is happening and that we introduce a keyword for early exits in if-case, et cetera that are acting as pseudo-closures.
To me it seems unreasonable to argue that omitting the
returnkeyword results in equally clear code everywhere in the language except forguardstatements.
The parenthesis at the end of the âguardâ statement do not mark a closure but are part of the control flow of the surrounding closure (or function). A âreturnâ always escapes from the surrounding closure (or function). And a guard statement is never the last statement inside it.
but worse may return a value you did not intend
Thatâs type inference, which is a feature of Swift and which can go âwrongâ also in other cases where you then need to add a type annotation. So this is not a new problem.
I think that builder functions are a mess.
Iâm also against trailing closures because they create inconsistent syntax between call site and the declaration.
Some people here seem quite opposed to result builders, or is it just the way it is implemented? I would not be using Swift if there wasnât result builders and the trailing closure notation.
(But result builders are quite interesting in the context of this topic, as result builders use all expressions inside the parenthesis, while closures or functions with the optional last âreturnâ just âuseâ the last one at the end. So an important issue would be, can you decide in which situation you are? I think so. And exactly this difference could be a good introduction to the optional âreturnâ feature.)
I am certainly hoping this gets rejected but if it doesnât, at the very minimum it should be an error to elide return in âSafeâ build mode that is being pitched.
Do not confuse âsafeâ with some âeasy syntax modeâ. It make no sense to forbid valid syntax that is not âunsafeâ in a safe mode.
Ah, good point, that buries the idea then, doesn't it.
Unless of course if it were made to only apply to if statements without else clauses ![]()
Some people here seem quite opposed to result builders⊠I would not be using Swift if there wasnât result builders and the trailing closure notation.
(Sorry I am citing myself.) It generally seems a little bit that some people value clarity or a kind of âsafetyâ (no sarcasm, it is a valid issue) over âexpressivenessâ and vice-versa. (I had this impression also with an older pitch of mine.) This description is maybe a bit simplified, but the point here is I think the pitch in this topic is very hard to be eventually solved only by argument, as both sides are somehow valid and good arguments were made hereâŠ
If there are usability and clarity concerns with omitting the
returnkeyword inguardstatements, then this feels like a concession that omittingreturnhides control flow and makes the resulting code more difficult to understand.
I think I agree with the question being raised here, but I come down on the other side of it. Iâd love to just throw a value in the else clause, or a log message and a value with minimal noise.
func calculate(value: Double) -> Double {
guard value >= 0 else { defaultValue }
value * value
}
func calculate(value: Double) -> Double {
guard value >= 0 else {
log.info("Skipping negative value")
defaultValue
}
value * value
}
That would make it annoying to re-introduce return to early exit in void-returning functions, but maybe you could just elide the whole else clause? (No doubt Iâve missed a million reasons that couldnât work!)
func configure(with user: User) {
guard isSystemActive
guard isOtherStuffConfigured
fetchCredentials(for: user)
refreshEnvironment(for: user)
}
If there are usability and clarity concerns with omitting the
returnkeyword inguardstatements, then this feels like a concession that omittingreturnhides control flow and makes the resulting code more difficult to understand.
While I am personally against the pitch, I have to state that this argument is unsound. To understand why, just consider a function returning Void
func myFunc(_ mustBeTrue: Bool) -> Void {
guard mustBeTrue else {
return // Mandatory, even though nothing is returned
}
// Do other stuff...
// return // Possible to use, but makes no difference
}
When it comes to the last line in a function, exiting the function is the only thing that can happen after that. It's the default action, and there's no other logical default: There's no "next line to continue to", throwing might not be possible (if the function itself is not throwing), there's nothing to break out of, and a function certainly shouldn't loop to the start when there isn't even a for or while. If the function returns Void, then the use of return is superfluous. If it returns any other value, then return simply marks what that value is, without changing the program's flow.
In a similar fashion, an if's default action for reaching the last line is to proceed to the next statement after the if block (and else block). If the if is used as an expression, the next statement, based on expression execution order, would be assignment to the variable in which it is used.
By contrast, guard does not have a default. A guard with an empty else block, even in a function that returns Void, is an error. Based on the similarity to if, the default would have been proceeding to the next statement, except the definition of guard disallows it. With that option removed, there are a bunch of other options for what should happen at the end of a guard's else block, and none seems more useful or common than the others. And all break the function's overall control flow, in that the line immediately following the guard's else block is not executed.
+1 on the full-fat option.
I've always felt that the requirement to include an explicit return was at odds with the spirit of the language. I empathise with those who feel it would impact readability, but these arguments feel reminiscent of those who initially dismissed implicit typing coming from languages such as Java.
The key things bringing me to this position are that:
- We have a number of other control-flow keywords such as
break,fallthrough,contunue, that mark points where there is a deviation from the sequential control flow. It feels thatreturnis the exception to this, and I'm a great believer that consistency is the key to simplicity. - The function signature of a method is already a verbose expression of the behaviour of a method. It seems that sequential straight line code, together with keywords (and code blocks) for control flow exceptions mentioned above mean that the reader has everything they need to understand what a particular function is doing.
- I'm not a fan of introducing yet another piece of syntax for what amounts to an alternative dialect in piecemeal sections of code. Now we have to look for both
returnandyield/bindwhatever keywords to understand what a method is doing. It's actually this that will impact code readability for the worse. (see consistency.)
We all argue about readability, usability, consistency, etc., but apparently there are different ways to achieve them and different end results, which shows that these properties are quite relative. One may consider that one syntax has a general feature A, which appears also in another syntax together with a general feature B, so adding B to the first syntax seems to be an obvious and consistent extension. Someone else, in another line of thought, does the same with a third syntax, which has features A and C together, so adding C to the first syntax seems to be equally obvious and consistent.
My view regarding consistency (which is relative to my line of thought): an optional feature like a return statement which may or may not contain return, is by definition inconsistent. There are two ways to do the same thing, which is different than "the one and only consistent way". Even more when it is about one of the most often used keywords.
Having already some kind of inconsistency somewhere, we can repeat it somewhere else, which makes our inconsistency consistent. This is a second order of consistency.
The parenthesis at the end of the âguardâ statement do not mark a closure but are part of the control flow of the surrounding closure (or function). A âreturnâ always escapes from the surrounding closure (or function). And a guard statement is never the last statement inside it.
Arguably the code in the guard statement is (by definition) the last expression in that branch of the function. Any code with guards:
func foo() -> Foo {
// ...
guard conditionA else { .valueA }
// ...
guard conditionB else { .valueB }
// ...
.valueC
}
Can be trivially converted to nested if/else blocks where implicit return is allowed:
func foo() -> Foo {
// ...
if !conditionA {
.valueA
} else {
// ...
if !conditionB {
.valueB
} else {
// ...
.valueC
}
}
}
Without changing anything semantically (the function behaves exactly the same).
The latter is significantly harder to read, yet if last-expression implicit return goes forward, only the latter would be allowed to use implicit returns or be part of a multi-line expression. So this seems to have the unfortunate effect of encouraging writing code in the second way, which is commonly accepted to be worse.
(To be clear: I'm against implicit returns. But if they come to be, I think it's worse if guards are not supported).
The problem with supporting guard that I see is behavior of guard â it always should alter branch, but itâs not limited to returns â for instance, it can contain continue in the loop. With implicit returns supported for guard it either has to be a specific case inside loops, or it would produce unexpected returns, plus there is still case for Void leaving with odd requirement to keep return for that case I guess⊠There are too many implications of that.
For me, this is one more reason why elided returns only bring more confusion after all.
This thread is hard to follow, and I may have miss this point, but how do this feature works with return type inference ?
@discardableResult
func doSomeWork() -> Int {
return 42
}
let fct = {
guard someTest() else {
return
}
doSomeWork()
}
Is the implicit return ignored here, which would be very confusing and make it even harder to understand when a last line is a return or not.
Is the compiler emitting an error because the closure return types are inconsistent, which would be a source breaking change.
Personally Iâd much rather remove implicit returns entirely than add more of them. I find the âconvenienceâ of omitting a return statement doesnât help (me) in any useful way, and puts us into the edge-casey state this pitch is trying to solve.
While Iâm sympathetic to the idea that this pitch may reduce those existing edge cases (indeed, I argued for exactly this solution in the then keyword pitch), I still donât think itâs a good direction for the language.
Swift was built on a foundation of spelling things out to prevent possible bugs (which is why we have to write braces to enclose if branches, for example). While swiftâs strong typing does help in this instance, I still donât believe that making things more implicit helps readability or maintainability for non-trivial codebases. Quite the opposite.
So from me itâs a -1 on this
I wonder how many of the concerns around implicit returns are founded in different programming styles.
As someone who likes to program in a mostly functional style, structuring functions around a single return value is very natural to me and reducing boilerplate would further encourage function decomposition, which IMO is one of the best tools for abstraction in the language (as evidenced e.g. by SwiftUI's heavy use of modifiers).
On the other hand, I can understand that this might not seem as natural to people that come from a more procedural background where implicit returns are seen as "magic" sugar that obscures the control flow.
Personally, I believe Swift as a multi-paradigm language does just fine supporting both and, given that explicit returns are still allowed, would do fine being unopinionated here (delegating these style questions to a linter, as suggested by others). While I can see the merit to the discussion, implicit returns are unlikely to go away and removing the special cases would actually simplify the language here.