This seems to make code blocks more imperative, while IIUC the original motivation is to make code blocks valid as expressions in the functional programming style.
FWIW it's a de-facto standard for last expressions in code blocks to be always returned without keyword noise in other functional languages (ML/OCaml, F#, Haskell, Scala). Swift as a functional language (1st-class functions/closures, preference for immutability and local scoped mutability, result builders, presence of all the FP staples like map/reduce, in general it's not that more imperative if at all than say OCaml) seems to be the odd one out so far.
I've personally been confused by "unusual control flow" from the pitch or other comments about the "abnormality" of returning early. Functions are (usually) written with intent about conditioning on arguments or logic - this is not abnormal. Every return from a function is somewhat considered valid control flow and the end may not always be the intended "happy path".
In the same vein that return isn't necessary for single expression contexts and it would be considered to be in the same class as try and await, for syntactical "consistency" the following should[n't] also be valid:
Task {
myAsyncFunction()
}
do {
myThrowingFunction()
}
Or, even further, why not omit these too if they're the last expression:
Task {
notAnAsyncFunction()
myAsyncFunction()
}
do {
notAThrowingFunction()
myThrowingFunction()
}
While the single-expression rule for if and switch statements is bothersome, IMO it is so not primarily because you have to fiddle with the branch where the edit is being made. Rather, it's because the number of branches can be unboundedly large, and each of will require updates in order to turn, e.g., a switch-expression assignment into a switch-with-definite-initialization statement.
Which is to say, I consider solving 'you need to add return when introducing a second statement to a function/closure' to be pretty much a non-goal.
While I'm still partial to a keyword and somewhat allergic to the fully general last-expression rule I think I could be satisfied with a version that was scoped just to if/switch/do expressions, or even just do expressions. I've never really desired a language-level solution to the 'adding a statement to a single-expression function' problem, but I frequently desire such a solution for if/switch expressions.
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.
I don’t know about other languages with this feature, my major experience is with Rust only on that topic. There, this is the matter of style you choose — to omit returns you have to alter way to write the code, not significantly, but still — you need to reach for this feature additionally. And in either way extremely meticulous Rust compiler will check that you follow its strict syntax.
In Swift with this change we would have a different situation: you have to explicitly remember or rely on linter (which is usually kicks in later and with Xcode have not the perfect stability tbh) to follow one style over the other. It makes no need to do extra step to use this feature, but a lot to avoid.
And language in that way seems to promote this way of writing code — I have no objections to language pushing something forward in general, but imho this pairs badly with progressive disclosure concept. Returns are quite common and easy to understand, they exist in many popular languages, but last expression as return requires whole another topic to be explained.
I think the feature would’ve been better as opt-in, where you need to write a bit differently to get the behavior, or at the very least has offered explicit alternative for the expressions (like then or similar).
I’ve been following this thread, but haven’t commented yet as I’m still trying to weigh all the viewpoints that have been shared.
I have, however, been thinking about suggesting something similar to what you wrote, but instead of using “$0” I was going to offer the “newValue” of setters and property observers.
In a setter, the new value is passed in and immutable. Whereas in an if or switch expression, the new value is being created so it is assignable. The single-expression rule would still apply to simple cases, so the example from the original pitch could become:
let width = switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default:
log("this is unexpected, investigate this")
newValue = 4
}
I’m not entirely sure if I actually want this. It might cause ambiguity when “newValue” already exists in an outer scope, so perhaps a mechanism to provide a custom name would be needed. Or it might simply be unintuitive to readers. But then again, it might be perfectly fine just as it is for property setters.
• • •
Conversely, I also kind of lean toward a “redundant assignment” spelling, where the variable being assigned to is recognized within the block:
let width = switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default:
log("this is unexpected, investigate this")
width = 4
}
Or at the end of a function, repeating the return keyword within the block:
func foo() -> Int {
...
return switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default:
log("this is unexpected, investigate this")
return 4
}
}
This might break down when assigning to something complex like players[i].items[j].count instead of a simple variable like width. But maybe it would still be fine.
• • •
Regardless, after reading through this whole thread, I have landed in the position that we definitely should not allow return to be omitted from multi-line functions and closures. That level of brevity should be reserved for single-line situations.
I am sympathetic to the use-case of adding logging to an if or switch expression. However there too we should not introduce a “bare last expression” rule. If we can find a suitable and “Swifty” solution, that’s great. If not, I’d be okay with that too.
After all, we don’t want to encourage people to put large chunks of logic within a single expression, and an overly-sugared syntax might have that effect.
Would you be able to provide example code that demonstrates this? Note that I wouldn't consider type checking errors as "issues in the code you might not be aware of", since those are extremely explicit and just won't allow one to proceed without any awareness of those.
This is not different at all to the existing situation with explicit/implicit self where one already has to use a linter/formatter to enforce consistency.
If I'd like my code to be free from the noise of redundant return keywords, I'll have to take an extra step to set up a linter/formatter to remove those from the codebase. Just as engineers preferring explicit keywords would have to do to enforce their preference in their project. So either side would have to take an extra step, just as people take an extra step to enforce use of self.
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 implicit self altogether for that argument to make sense in the first place.
Doesn't seem relevant, control flow and self shadowing are very different. May as well ask them about their stance on any of Swift's other implicit features. If you want relevant comparisons, proponents should have to argue why return is superfluous and not the rest of Swift's control flow syntax. But I don't think arguments in either direction are relevant to this specific change.
Even for folks who are against implicit self, I think it's perfectly consistent to argue that, regardless of ones position, implicit self is far too entrenched in Swift today to have any reasonable path towards deprecation. Such an argument has no real bearing on whether we ought to introduce new implicitness, though.
Take this with salt but I believe the implicit newValue for getters is old-fashioned and a legacy implementation. I also believe that there were some comments here and there on the threads about that since it doesn't require explicitly defining closure arguments or exhausting all of them with $_. Although, I think continuing this exact point could be taken elsewhere.
It is absolutely relevant. Implicit/explicit self (or explicit vs inferred types etc) being a core part of the language already establishes a precedent for users to choose their level of explicitness based on their preference. It seems natural to extend this to allow implicit return to resolve the inconsistency with single-line closures, functions, and accessors.
This is what has been argued countless times here: a closing brace following an expression is already an explicit indicator of such expression being a return value. There's no need to repeat that with an additional keyword that will just restate the same thing the second time. return x } reads just as weird and noisy as let x: Int = 42, while x } and let x = 42 read clearly and unambiguously.
If I was to manually refactor the Cooker protocol to make cook(...) return a Vegetable, there would be a compiler error that Microwave didn't conform, but no error making sure you updated your code to return the right value.
I'm also not sure how automated refactoring tools that updated signatures would be able to handle this either, as the intent would be ambiguous.
Explicit self would add far more noise than explicit return, that's enough to justify it, imho.
Sure, but what about code from a team that doesn't use the same standard, or doesn't use a linter? What about code I read in an article, or tutorial, or Github?
let value = {
let `break`: Int
switch true {
case true:
`break` = 1
`break` = 2 // Error: Immutable value 'break' may only be initialized once
case false: `break` = 3
}
return `break`
} ()
That wasn't the point of the feature, even if it has that effect. It was designed to make self meaningful vs. Obj-C, especially in captures, and Swift has continued to build features to increase the value that "implicit unless required" provides. self is also used a magnitude (or more) more often than return, so the value implicitness brings to it is completely different. And finally, the relative importance of visible self references is completely different than control flow, which Swift has worked to make more explicit than other recent languages.
On a related note, if this proposal is supposed to give users control over whether their returns are explicit, it needs to solve the multiline expression problem that motived the first pitch. This proposal lets me remove explicit returns but doesn't give me anything explicit for the expression case.
Personally, I'd like to see the proposal split the difference. Add do expressions and the bind keyword, as well as implicit returns for all expressions. Allowing for implicit return in all cases can then follow at any point, once we see what impact it has in the narrower case.
This conveniently for you omits the second step where a user updates the function to conform and then gets an error making sure that return value is corrected. Thus there's still no way to write wrong code here and make it compile.
Again, it's either very subjective, or you're very conveniently for your argument omitting the fact that implicit self can lead to many more pitfalls due to shadowing.
IMO using a linter is a good practice, thus it's good if users are encouraged to do so.
$0 would be source breaking, because a switch can be inside an actual closure. Adding a closure-like parameter to a switch block looks attractive. But for an if statement you hit a problem:
let unexpected: (inout Int) -> Void = if Bool.random() { val in
val = 1
}
else { val in
val = 2
}
var myint = 0
unexpected(&myint)
print(myint) // Will print either 1 or 2
The above is currently valid code, but would be a compilation error if in had a new interpretation inside if.
func foo() {
// code before
let x = if a {
"hi"
} else {
if b {
"bye"
}
"welcome"
}
// code after
}
Note that warning emitted for "bye" is a little help, you might miss it.
If that's a larger function (and this is quite often is), then this expression can simply be buried there, and in the end you can have an issue with that. I've made such mistakes in Rust when the same type property happened to be the last line, and spend quite of a time on debugging. That makes it easy to make a mistake just by accident, that's why I find the defensive path to be preferable.
We can argue that this doesn't illustrate any particular issue in the code, and that's the problem of an example outside of a context: quite often the case is working with large functions, where even with explicit returns you can make logical error. All of the code examples as above can hide in this pile of code without hint of return statements, making a mistake much easier to make. I don't have such a project to share anywhere, unfortunately.
The implications of implicit self is very different than of a return. Only if you "play" with shadowing of variables, which is not what you'll reach (or know) by default. I mean, you can use shadowing, but that's not what will be implicitly in the way akin to return made for you by the language when you don't expect this. And yes, the implicit selfcan be dangerous, I find it hard to justify in codebases to be explicit by default, but sometimes you have to be very careful, if you use some niceties of the Swift.
My point is not about linters, of course if you'd like to enforce certain style you'll need linter/formatter either way. The point that you cannot choose as in Rust if you want to follow one path or the other by writing code in that manner with compiler keeping an eye on you – that's a very different experience than "everything you write as last expression will be treated as return value automatically".
I don't think we should mix-in this. This is a different aspect of the language. It can cause issues, and I always alerted about them, trying to avoid shadowing if I need to interact with instance property, because mistakes happen. But the biggest difference is that they kick-in only when you shadow variables – not by default.
It's hard to be objective after 10 years of this being part of the language from the very beginning, but in reverse scenario – if Swift would had explicit self all the time, and today we thought of making it implicit – I more likely would've been against this as well. The same way I more likely wouldn't mind if it would've been made explicit now.
As well as implicit return at the last line, but nevertheless this change makes the behaviour almost default and preferred for the users. If that's subjective, either option have to be at some "distance" – you either have to write return or, say, put a semicolon in the end of a line.
P.S. Might not be the best quote to use, but you have mentioned subjectivity quite a lot across several posts and this comment to the subjectivity as it, not this particular take, sorry if confusing.
Apologies, I'm not sure what you mean. With explicit returns, I'd get a compiler error that Cooker didn't compile - so I'd add the missing -> Vegetable to make it conform, but then I'd get an error that it was missing a return, which would force me to think about what I should do. With implicit returns, the code would compile after fixing the first error, but without prompting me to check the resulting behaviour was correct.
FWIW I've updated my code example to add a print, if that's caused confusion