That's only true if people choose to adopt if expressions in every single situation where they are able to, but I don't think we can or should assume that.
I agree it privileges those "symmetric" cases where the else is explicitly included. But I think I'm missing how it prevent other cases. Or are you saying that while "no else" code that was valid before remains valid, and doesn't itself get any kind of sugar, and so is lessened?
Personally, I'd write the else form anyway. I'd also say it's fine keeping the return if you're really keen on the "no else" style and think the elided else with it looks a bit icky.
(I have a feeling LLVM developers in particular are keen on this idiom)
I do agree that "come on, is that return/semicolon/paren/longer name really a problem?" is not ever useful feedback. But I don't think that's what we have here. Most people in this thread agree that the problem described in the motivation section is a real problem worth tackling. I surely run into that limitation frequently and would love to see a way around it. But, at the same time, if the solution proposed has broad impact across the language, it's reasonable to question whether those impacts may become a bigger issue than the original problem being fixed.
Otherwise, the same argument can be reused to virtually every idea about cutting down ceremony. We could elide the else { return } clause in a guard part of a function returning Void. Skip the [weak self] in at the beginning of a closure if self is used as an optional inside. Allow implicit throws if the last expression has a type conforming to Error. Or any other idea where it's probably best not to cut down in the ceremony.
I'm not arguing that a solution should be flawless and with no downsides. That's rarely possible. But it should be possible to conclude that for a specific proposal the downsides are greater than the benefits, even if that proposal allows cutting down some ceremony (which granted, is a huge bonus because it composes into clearer code). In fact, there must be a line somewhere, because guard statements in particular have been carved out from this proposal and forbidden from using implicit returns!
I disagree with this statement. I absolutely read this version better than the one where bothreturns are elided. I certainly don't think they interfere with readability. This case is simple enough that one may call those returns redundant, but I don't see how they interfere with readability in the same way as your other example with $0 vs a named i. In that example, the problem with the i is that suddenly one needs to pause for a second and ask "Wait. What is i here?". There's no such problem with return. I don't need ot pause to think what return means. It may be slightly redundant in some cases, but hardly interfere with readability.
And, in the case where you haven't immediately internalized the function signature before reading a snippet (there's many reasons to start reading code in the middle of a function), the returns absolutely help readability.
Ah, but implicit returns at the end (as in last line) of a function are not as controversial. Not to me, at least. The main problem is that when combining implicit returns with large expressions (which can even be nested!), a given "end of a function" may be dozens of lines above the closing brace of the function. It may not be obvious at all that the function ends at that point without reading the entire control flow of the function.
One way to limit that is by mandating one-line expressions. Maybe there are other heuristics (disallow nesting implicit returns?). But I think some guardrail should be in place to avoid writing code where an implicit return happens several lines before the end of the function.
Exactly. Let's imagine that the user had this today, using the elided-else style:
func f() {
if someConditionWhereEitherOutcomeIsLikely {
let x = doSomething(a)
return doSomethingElse(x)
}
let x = doSomething(b)
return doSomeOtherThing(x)
}
That code is already a small bit asymmetric by virtue of eliding the else, but aside from that one concession, it's otherwise symmetric—each branch is a block of statements that ends with a returned value.
If that author wanted to adopt last-expression results—let's say for the sake of argument via a formatter that could automatically remove "unnecessary" returns—then such a rule would introduce more asymmetry into the code by removing the second return but not the first. If the user wanted to improve this (where "improve" means "put back more symmetry"), they would have to introduce an else to remove the other return. This isn't an easily automatable change. If the user wants to benefit from the proposed idea that it draws attention to early returns, then this feature requires more manual intervention to differentiate between what's a real early return and what's just the false branch of an otherwise innocuous function-final conditional, a distinction that doesn't matter/exist when return is required everywhere.
Of course, it's entirely possible that the two style choices—elided else and last-expression return—are fundamentally incompatible with each other and no code base should attempt to do both. If that's the conclusion we reach, it's fine! I'm not necessarily advocating for that style, but I've seen it in plenty of projects I work on day-to-day.
Since the topic of linters does keep coming up, since this proposal does touch on style as much as it does introducing new functionality, it's worth noting that a linter/formatter can diagnose and remove an unnecessary return in a world where this proposal is accepted and the user wanted to enforce a no-return rule, but it cannot insert a necessary return if the user wants to enforce a required-return rule. At least, not without semantic information (it needs to know whether expressions have type Void or not to avoid adding unnecessary returns of those), which is a much less-supported side of Swift's tooling story. (When single-expression implicit returns were added, swift-format was able to add a rule that diagnosed and removed unnecessary returns but it cannot do the opposite.)
So at least in terms of the tooling side of the equation, this would be another situation where a particular style is privileged by a language feature. Given that, I wonder if we're going to be heading in a direction where it makes sense to have the discussion about a more opinionated style and format being enforced by standard tooling, to avoid the "style" part of these discussions from being such a heavy factor. (My biases are certainly showing here.)
ceremony
I don't think that critics of this feature are arguing that less ceremony is necessarily bad. We love Swift's conciseness! We may even use this feature on some places.
However, I repeat an earlier comment that we are somewhat free to choose what reduction in ceremony we may enjoy:
But where will we draw the line for the minimum amount of ceremony necessary? The reduction of ceremony has come through fine iterations like chipping away at marble, removing ever-so-small parts to reveal a fuller form of the language. However, it only takes one bad chip to cause the largest cracks.
There are many other places throughout the language that we can apply this broad "ceremony reduction":
- remove
await - remove
tryif last indoblock, or at all - remove
()for no function arguments - remove trailing closure labels
- remove
func, you can tell it's a function from thename [-> type] { - remove
catchfromdo/catch. If you have ado, you should know the next block is thecatch - <insert your removal here that may not be difficult for the compiler to parse, but may be for people>
No, I don't feel like I'm being rhetorical or outlandish with these examples.
teaching beginners
For those unconvinced by the teaching aspect, Swift is marketed as an Accessible language. This is the first line in the description at developer.apple.com/swift:
Swift is a powerful and intuitive programming language for all Apple platforms.
For teaching absolute beginners to programming, absolutely nothing is intuitive. You have to explain what syntax is and why it is the way it is. You have to teach about types and why they matter ("Why can't we mix Int and Double? They're both numbers?"). You have to teach so much from absolutely nothing where absolutely nothing is "intuitive", because now they're undertaking a heavily technical learning path that they may have never done before.
Eliding ceremony is not the best for beginners. The only explanation I can really offer at that level for why it still works when ceremony has been reduced is: "the computer does it for you". You'd be surprised then that it makes them more upset and confused! They keep wondering "Why? Why? Why?". Many beginners don't know what a compiler "is" or is a master in linguistics. Of course we can just "write the ceremony" but these reductions are at the forefront of all syntax, they will run into them "in the real world". No, these are not just "taught once". Having to remember all the time what reductions are allowed is a constant familiarity process that beginners have to fight through.
It has been commented that maybe the frame of beginners isn't the most appropriate to assess this feature. However, I've only seen proponents comment that they enjoy this feature "from other languages".
For those that don't have the expertise of "other languages", the rest of the Swift world are the "beginners". When we borrow features, they should be able to be a natural fit, but we seem to be at an impasse on that front.
The example code I provided is a snapshot into the thought process of someone who is evolving code that either they wrote a long time ago OR is a code repository they are now in charge of evolving. It highlights how difficult it is to reason with expectations.
Which is why I said the following:
So for example when I wrote the comment (on that return in question) as follows:
//return here? Okay. I will add return because this is the value, I think they want.
It is exemplifying how hard it can be to tell how I am supposed to yield the value. Now that I am revisiting it or taking it over.
That depends on what you mean by 'last line', an if-expression whence the return value is yielded can now be hundreds of lines long. It's the last 'logical' line, I suppose, but it definitely obscures where the return is happening all the way at the top.
This is precisely right, the critique here is not if we could theoretically have this feature work (it will work!) but it is how does it affect the code reader and the cases where a function can assume too much about what your intent is.
I have been programming for ~26 years now. I appreciate what you did here @shantini so much because what you point out is how something as simple as eliding return and having the compiler do more work for us promotes dangerous code style options that can confuse intent. I would have the same problem and have a similar thought process when trying to discover intent.
I think @xwu said it well:
I believe this is hardly a problem in single line closures and functions -- we can come up with examples similar to the ones provided where it obfuscates intent to the reader even in logically single line, but it is less likely. In multi-line functions, it feels more likely and should simply not be allowed.
Often it is okay to sacrifice 'consistency' for 'clarity'.
In theory, eliding return can seem clean, and easy to work with, but in practice it just is not. Eliding return hides what is happening in larger functions and that is feels very dangerous and should not be expanded into multi-lined bodies.
It is equally easy to identify the end of the else block as it is to identify the end of the enclosing function block, and in both cases you have to look at what precedes the paired opening brace to know that it’s a function or a guard.
To add a data point, as a recent beginner to Swift, I ran into this same confusion.
It seems this pitch would help with consistency, seeing as omitting return is already in the language.
After all, when omitting return, all one needs to search for is the final block of a function. Or, in the case of Swift, an early return which would be highlighted/boosted by the fact the default end-body return would no longer be needed.
Upon seeing @ben-cohen’s example, my mind immediately translated it into something more declarative:
func hasSupplementaryViewAfter() -> Bool {
elements.firstIndex { $0.elementCategory == .cell }.map { cellIndex in
elements[cellIndex...].contains { $0.elementCategory == .supplementaryView }
} ?? false
}
This of course has the added benefit of being a single statement, avoiding the issue, but I wouldn’t say that avoiding the return is motivation itself using this style.
You're not, but each one has an answer. The goal here is to remove ceremony where it delivers insufficient value, or is otherwise causing problems, inconsistencies etc.
So for example, "remove () for no function arguments" is, I think a very reasonable proposition (if you're talking about the call site). Ruby, for example, doesn't require parens for function calls. The challenges though would be considerable. Mainly, the syntax for unapplied methods is to leave off the parens. If we went back in time to Swift 1.0, added the \ syntax for keypaths, and then extended that to also be the sigil for unapplied methods, then I could totally see an alternate reality where () aren't necessary for function calls. These days, however, that's almost certainly an unrealistic change to make to Swift. Unlike the change pitched here, it could not be adopted as an incremental change, and so probably not worth discussing further. But that doesn't mean it isn't interesting to consider.
As for removing await or try. There is every reason to look for ways to minimize these keywords further. They exist for a specific purpose, which is to mark a point where reentrancy could happen, or a scope can exit. Those markers are vital – but also sometimes redundant, like return can be.
Swift already allows you to skip stating the try twice on one line. Sometimes. But not in this case:
try values.map { try $0.failable() }
The second try is entirely redundant. It would be nice to have a rule to avoid having to restate it.
This kind of thing is important to look at in future. If you look at some well-written modern structured concurrency code in Swift, it is a cascade of disheartening return try await ... return try await ... return try await. We can do better there, so long as it's done with care.
Great! As presented by the naysayers, it adds sufficient value to keep it in multi-lined functions and removing it causes problems.
I was afraid that you'd try to make the case.
Requiring it does it with care, it is also elegant, clean, safe and gives us the right information that we need.
A tiny bit of redundancy for the sake of safety and clarity is the Swifty way to go!
I think this is a great example of the differences how people parse code. While Ben’s examples (all, even the one with the omitted return) where completely easy to read for me, my brain cannot easily comprehend your code. I have to go to the innermost part first and then switch between the different closures, check what you're mapping, go back to the contains, etc.
So while one person may find it easy to 'determine' what a method will return, others have to think more and go through the function line by line as some people can 'unwrap' closures easily, I cannot.
Whenever I use an “ugly” combination as return if (which happens to be a lot in my code at the end of functions) I have the suspicion that there should be something better. And having to type return twice instead does not feel much better.
And after seeing some examples without any return at the end this does not feel so “bold” any more.
Yes, so I think if you have to use return or not at those places will not decide if you write code that is easy to read.
Arguably easier, as it's unusual to find large guard bodies in practice. So the end of a guard's else block is usually much closer to the guard keyword, and has simpler control flow compared to the end of a function.
I can't think of a reason to avoid extending implicit returns to guards, other than enforcing the idea of using return only for "unusual control flow". But I'm not sure that's even achievable. Sometimes the "unusual" flow happens at the end, this is a relatively common pattern:
func handleURL() -> Bool {
if handleWithFeatureA() {
return true
} else if handleWithFeatureB {
return true
}
// No feature can handle this URL!
false
}
Anyhow, I feel like the pitch as-is would have the effect of discouraging early returns using guard:
guardstatements can't use this new syntax to return values.- In a
if/switch/doexpression, one must avoid usingguard, as there's no keyword to assign the variable in theelseblock and there are no implicit returns there. This may build an 'inertia' of not usingguardeven in contexts where it's supported.
To this point, the version of @ben-cohen's final example using guard instead of an if/else block reads best to me, even with an elided return:
(Of course, this opens the door of what to do with guards in functions that return Void. The logical conclusion would be to allow eliding the else there entirely).
Does not Swift need a keyword to differentiate class vs instance methods too?
Could you expand more on how in this particular example the returns actively “hurts” readability please? It is not clear to me it does.
As @xwu was mentioning before, reading your reply made me think about it even more, it is a concern where having lots of smaller ceremonies to pick from and the subtle effects each would have or they would have when combined that creates a ceremony of its own.
For all its faults, block syntax was a late addition and not always great (although it could be side stepped), Obj-C was a relatively simple language easy to compose. Actually quite readable and beyond the scare of [ brackets and message passing, the verbose syntax was a bit of a difficulty bump but after that bump the language gotchas mostly disappeared (it was a great language to make apps with…).
The reduction in ceremony in Swift and the additional features it gained vs its predecessor (generics, result builders, structured concurrency, etc…) and how it gained them have a cost in complexity (early Swift did not need famous C++ language lawyers, jury is a bit out on modern Swift…).
@ben-cohen the paragraph where you mentioned removing parentheses from methods that had no arguments gave me flashbacks of Obj-C 2.0 when properties were introduced. Few changes I was so excited about that in practice confused many people blurring the line between properties and methods
.
For the pitched multiline expressions this is not holding true. Even as of right now it is possible to write the code where last line isn’t clearly denoting returned value.
Considering the initial example of this function,
- If we treat this from standpoint that both branches return a value, then this function has two ends that here presented as “equal”.
- If we treat this as an expression, then whole
ifdenotes the end of a function.
Both cases can be (subjectively easy) deduced in the example. Neither of them, however, follow that function has either one end or it is clearly denoted by the closing brace.
It also can be rewritten with only one return given currently available expressions, and that reads nicely:
func hasSupplementaryViewAfter() -> Bool {
let cellIndex = elements.firstIndex { $0.elementCategory == .cell }
return if let cellIndex {
elements[cellIndex...].contains { $0.elementCategory == .supplementaryView }
} else {
false
}
}
I would still prefer version with guard in the first place, but that’s question of preference.
I also agree with @austintatious that problematic cases tend to be more complex and less expressive in the first place, they quite often present much more critical parts of the program as well. Large if-else-if or switch is reality for many apps, more than I wish I end up working with code that looks like this one, with me not being an author of this code, so I have to guess what was intent of the author.
There is also another issue brought by implicit returns. Modifying example with rays to match proposed behaviour, plus one more replacement to get this code:
func hit(_ ray: Ray, _ bounds: Range<Double>) -> Hit? {
// snip...
// Find the nearest root that lies in the acceptable range.
var root = (h - sqrtd) / a
if !bounds.surrounds(root) {
root = (h + sqrtd) / a
if !bounds.surrounds(root) {
return nil
}
}
let point = ray.at(root)
hit(with: point) // <- changed
}
It is now harder to reason just by the bare last line what’s happening: does this function returns a value or not? I might be brought there via debugger, so I am not yet aware of the signature, and explicit return can help understand more from this narrow context.
Comparing to Rust, for instance, it is a bit better by at least signalling via missing semicolon:
fn hit(_ ray: Ray, _ bounds: Range<Double>) -> Option<Hit> {
// snip...
// Find the nearest root that lies in the acceptable range.
let mut root = (h - sqrtd) / a;
if !bounds.surrounds(root) {
root = (h + sqrtd) / a;
if !bounds.surrounds(root) {
return None;
}
}
let point = ray.at(root);
hit(point)
}
IMHO it is still harder to get compared to explicit return, but there is some hint for better understanding.
This all can be justified that it is not the reason to block the change, because someone always be able to abuse feature to the point of non-readable code. I don’t find this argument relative to this change: the question for me not that it can be abused, but that it promotes certain way to write code to the point where long if/switch expressions with side effects are extremely easy to create and language has explicitly been adjusted for that.
Another big thank you for that post, it's definitely shifted my opinion a bit, although I'm still very much -1.
I've been thinking hard about just why I dislike this so much, because I want to make sure it's not a rationalization for laziness or stubbornness or something else. I think my main reason is that returning a value from a function is kind of a side effect, and when you don't have the return keyword, the fact you are causing this side effect becomes invisible. Consider this PR for the tail end of a long public function inside a class...
// complicated cool stuff
// other things happen
// generate profits etc
--- calculateCatAgeInYears(...)
+++ calculateCatToyCount(...)
}
With the code seen as is, without implicit returns, we know that this function cannot directly affect the behavior of a caller just by returning, otherwise there would be a return statement. But with implicit returns, this could directly affect the functionality of callers, too, maybe even causing breaking changes to contracts if this was a public API...but that affect is invisible at the site of the code change that caused it.
My two cents are not at all original, but as a person who loves Swift, I would like to chime in:
I would absolutely love this!
I love single-statement closures without returns. I love single statement functions without returns.
I love switch-expressions allowing me to leave out returns in my many, many functions that simple map over an enum value.
They read so much better to me, because they are very obviously just mapping values.
I do not like realizing that I can’t eliminate returns from a switch statement because of a single log statement or extra check in one of the branches.
Hitting these situations actually annoy me and I wish that I didn’t have to keep all the returns around.
Similar for single statement functions, it feels hacky to me that I have to add a return because of an extra log statement.
I very much like that this pitch does not introduce new keywords. I really dislike the idea of adding something like ‘then’.
Knowing myself to prefer brevity - and actually believing that brevity most often helps readability, I’m certain that I will also start returns for ‘longer’ functions.
+1 from me.