No. See my post here: [Pitch] Last expression as return value - #149 by Ben_Cohen
I feel like this contradicts all the people who said that removing a constraint would make things easier for people new to the language. Like, they now need to learn that some expressions that work with ternary won't work with if
, but might compile a bit faster [1]. It makes me wonder how long before someone pitches removing that "limitation" too, and allow bidirectional typing for if
and switch
expressions.
I can't help but feel this proposal is a slippery slope that largely misses the point of the original. It would create a construct that does some things more than a ternary, and some things less, requiring that much extra brain space just to figure out whether or not it should even be used in any given situation.
If that's a priority. It feels like a niche thing to keep in mind. â©ïž
I don't believe there's any desire from the core team to make if
expressions work like ternaries. If anything, backwards compatibility is the only thing keeping ternaries from being made to work like if
expressions.
I'm +1 on this proposal. This continues a very swifty trend in the language that has been there since day one and on the whole makes the language more consistent given some of the recent additions to the language.
I am against making the language more complex than it already is (or I am for slowing down the rate of added complexity), however this change is just further accentuating an existing feature of the language and applying it more consistently. Yes, removing the returns can make some code harder to manually parse, but that is the language and part of learning it. There are features in every new language we learn that break our brains a bit until we groove those neural pathways.
I really wish we had thought through the if statements as expressions and had gone the way of requiring an explicit marker (some have suggested something like do
) but I would have been happy if it was get
and return
would be give
let u: Int = get if ... {
...
give 2
} else if ... {
...
give 3
} else {
...
give 5
}
I don't think I like 'leak' but it is sure a lot more clear and having nothing at all (which I oppose).
If we bringing learning topic IMHO last expression as return value
is more valuable for beginners as people can face it in lots of other languages, than number of keywords only in one language.
Unfortunately that just isnât true. I wish it was true that teaching Unexplicit things was easier than teaching explicit things. It would save me some time. I do not teach that you can elide types from variable declarations anymore. I do not teach about trailing closures at first (or maybe at all, I avoid them). Iâm even careful around autoclosures when creating sample code for my students. I need them to see what is actually going on. Only after they solidly understand what the compiler is guessing, do I start to demonstrate that certain things can be removed.
Explicit code is always better, safer and easier to teach. it allows me to get much more quickly to the concepts.
Builder functions are still exceedingly difficult to explain. Especially when control flow statements donât work the same. There is too much incredible magic happening behind the scene with builder functions.
I hear what youâre saying, but youâre also showing some of the limits of âwhatâs best for the first month of intro programmingâ as a rubric for language design. I completely agree with teaching your students to write out variable types, but that doesnât mean that type inference shouldnât be in the language at all. Teaching intro students a curated subset and then gradually expanding it as they begin to master concepts is time-honored practice.
Explicit is better than implicit. If even Python, the language that decided to get rid of curly braces in favor of letting the interpreter figure things out solely from indentation, agrees on that, we should probably take note.
The idea behind Swift has always been for me safety and clarity. Being Explicit always wins that one. When those two things are violated, it is not the "Swifty" way of doing things.
I am trying my best to refrain from having a discussion of past 'features' that were added to the language that in retrospect should have never been (either because they degrade compile times or they reduced clarity and clarity and make the language more akin to messy C than to the philosophy of Swift). I am really just trying to focus on not introducing more harm to the language. I know it's difficult to advocate for such harm reduction without bringing up these cases, but when they are discussed I think it mentally taints our bias towards "well we let these types of features in before, and I like them, so let's do this as well."
So I will say it again, eliding return
causes measurable harm to the language, debug cycles, and if those are not accepted reasons (because they can be viewed as just opinion), then at least considered that it is not needed. Allowing it to be elided does not solve a problem, it encourages a problem, creates ambiguity, and may have special circumstances that create valid code when there should not be. There is no functionality or feature to be gained by allowing it to be elided.
The compiler can figure out when you need await
because the function will be decorated with async
. We choose to require it because disclosure of that yield point is necessary to quickly seeing control flow. This is truly no different than return
. Return has consequences to what value is being thrown around. Not just reading it but also searching for it! The language should refrain for guessing what I want to do with my code.
Furthermore, I do more than just not teach them at first, I briefly cover the less Swifty way of doing it (i.e eliding things), and then move on to encouraging to never do it. Avoiding eliding code just makes for stronger code that is better read by a team and also allows you to -- for example -- debug easier in a TextEditor (which is what I do when I compile on Ubuntu).
Sometimes I wonder how many of us are working on large projects where little details like this an have huge impacts on debug cycles. I can guarantee you that working on projects with hundreds of files with with 10s of thousands of lines makes bug prone features like eliding important control flow keywords much more difficult (worse types!)-- even for code that I wrote myself, looking up return types for functions that are getting assigned to variables can be a headache (and I am guilty of it!)
Let's not continue this trend.
I do think I need to yield on this topic, however, I am just repeating myself. And even I understand that adding this features will only make it harder to find return types, make code less safe, create silly debug situations, but we are all smart and can figure it out. The language will carry on.
Thank you for all who took the time to read my position. You have been very kind to humor me.
Seems that it is time for summaries, so here is mine.
The new syntax allows me to inject log messages inside expressions, which is useful for debugging. Am I going to use this feature? Probably yes, with some caution, since it works in some context but not in other seemingly similar contexts. Certainly yes, when I am desperate during debugging and any additional help is welcome.
Am I going to use it as a new debugging paradigm? Definitely not. I prefer to have a sound debugging strategy and to implement it in a systematic way. The opportunity to inject some log messages inside an existing expression is not sufficient.
The new syntax also allows me to introduce side-effects inside expressions. This in turn could make the code more compact. Do I need this feature? Quite the opposite. I try my best to separate update logic from pure expressions. I had many times this great feeling of eliminating code lines without losing functionality. It was many years ago, even before Swift, and I don't miss it. Minimizing the number of lines or bytes is not my target. For this purpose I could remove comments and I could refactor variable names to a single letter. It is mechanical, easy, and very effective. It is also very counter-productive. Separation of ideas is much better than local code reduction. It allows for better refactoring at a higher level, and eventually also reduces the code size by much more than a few lines here and there.
Sooner or later I'll find Swift code with if/switch expressions mixed with side-effects. Can I work with it? Definitely yes. Even if this style becomes popular, it will be quite limited. Anyway the code cannot be too complex and correct at the same time. So I am not worried about the widespread of a feature that I don't like.
The new syntax also allows me to omit the return
keyword in some places, if I want. I definitely don't want to do so, actually I'm going to fill-it in where it is missing. But can I work with code copied from others? For my personal projects definitely yes. Either I use some code as it is (and I learned to not touch something that works ok), or if I want to refactor it, I'll make many more changes than adding return
here and there. In addition, the rules for where return
can be omitted are pretty simple, so it is mostly a mechanical process. So not a big deal.
For team projects it is different though. What starts as a personal taste can quickly escalate and become an existential problem. If my team likes to omit return
, I don't have many options. Re-formatting the code every time I want to look at it is not a solution. Going after the others and filling in return
s is not a solution either. Even worse, if I cannot read code without return
and my team cannot read code with return
, there are only two options left. The first is to adapt to the others, the second is to find another team and project.
Overall, the last expression with optional return
has the potential to create big annoyances in team projects. The if/switch/do expressions introduce features that I hardly need, but nevertheless they are manageable.
+1000
longer comment
Sorry I am repeating myself, that would eliminate a point that @austintatious brought up (âWhat if it were allowed in multi-lined closures/functions and in the course of development I comment out my return statement and now a unsuspecting discardableResult becomes the return value?â), would remove some ambiguity that exists even without this pitch, and a warning should not hurt old code bases too much. I would like to know what people think about it, if it should be added to the pitch (regardless of whether you are in favor of it), or if it would even be a good thing independently of if. To show some code:
@discardableResult
func f() -> Int { 1 }
let g = { () in f() }
To my opinion, the type of g
should not have to be guessed without a warning.
Update: I added an according Swift issue as this question might be seen independently from this topic and it also might be a change too small to formulate a pitch for it. Maybe not to be discussed further here, although it might be a point to add to the pitch text.
One of the arguments for the optional return
at the last expression is consistency with one-liner closures.
The following is a non-sense feature, with an equally strong consistency argument. We make optional the return
at the first expression of a function/closure. Of course this is not so useful, since there is no code after a return anyway, but the compiler allows it with a warning, and sometimes an immediate return is temporarily useful for debugging. Anyway usability is a different story and a different argument. So let us focus on consistency. This feature has the nice property of extending an already existing property from one-liner closures to all functions. It fills a gap in the language. One-liner closures are not an exception anymore, since all functions/closures get the same property. (Of course all this paragraph is non-sense.)
I buy the argument of usability, I don't buy the argument of consistency. Both the first and the last expression are extensions of the single expression. Both extensions are equally arbitrary, I wouldn't call them consistent. And there are many more arbitrary extensions (any one, but only one, of the return
can be optional; the return
closer to the center can be optional; if one return
is omitted all must be omitted; etc., etc.)
Please do not post summaries.
Almost by definition they make an already-long thread longer without injecting new information. In theory, they can provide a useful recap for people new to the thread, but in practice they of course emphasize the preferences of the summarizer, hence you get multiple summaries, negating the benefit and leading to dueling summaries.
You are right, sorry for starting that.
Attention conservation notice: long post, sorry.
I want to take a step back for a moment and talk about a more general principle that's behind this and many evolution proposals, which is the pursuit of an overarching goal of Swift as a low-ceremony language. As a way to illustrate this I want to use a very short snippet of Objective-C (which was recently posted by @Dimillian on Blue Sky).
- (BOOL)hasSupplementaryViewAfter {
NSInteger cellIndex = [self.elements indexOfObjectPassingTest:^BOOL(MSCListItemElement * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
return obj.elementCategory == UICollectionElementCategoryCell;
}];
return [self.elements indexOfObjectPassingTest:^BOOL(MSCListItemElement * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
return idx âș cellIndex && obj .elementCategory == UICollectionElementCategorySupplementaryView;
}] != NSNotFound;
}
In just this small snippet of code there are maybe a dozen different kinds of "ceremony" that Swift avoids. In most of these cases, it does so by trading off what you might consider clarity or safety. To run through them:
- Swift defaults members to be methods rather than statics, so the leading
-
sigil is unnecessary. This is the natural default, without having much of a clarity downside. - Type inference means you do not need to declare the type assigned from
indexOfObjectPassingTest
. This is less clear â you cannot just by reading the code tell that this method returnsNSInteger
(in fact in Swift, it would be anOptional
â let's come back to that). - Explicit
self
. The concerns about Swift not requiring that have been thoroughly litigated. - The verbose name of
indexOfObjectPassingTest
. In Swift the API guidelines discourage needless words. In this case Swift's equivalent isfirstIndex(where:)
, since the call site's use of a closure makes it clear there is a test being applied. Indeed, with trailing closure syntax, even thewhere
can be omitted. - Type inference appears again, in the most extreme case of the types of the block. Swift allows you to name the arguments in a closure without typing them, or to use
$0
as a shorthand isntead of naming them. - Within that type signature, we see pointers and nullability. At first this might not be seen as a clarity trade-off, except that a C++ programmer might not agree. Swift values are passed into functions by const ref by default, but this is not explicit in the code unlike in C/C++/Objective-C.
- And Swift's closure syntax is simpler in other ways, requiring only an opening brace, no
^
or parens. Another possible source of confusion since it makes closures (by design) look like control flow syntax. - Within the first block we get to the subject of this pitch: the first
return
In Swift this first block is a single-expression closure, so thereturn
can be omitted. This thread includes many reasons why this can be considered less clear, confusing for beginners etc. - Swift does not require semicolons, of course. Even this seemingly simple thing introduces complexity and ambiguity into the language. Without semicolons, new lines are syntactically significant (i.e. you can't write on one line
let a = 1 let b = 2
). However, the fact that they don't at first thought seem to be significant â that most of the time you think of Swift dropping the semicolons as just casting off a redundant vestigial tail â is a testament to how well the grammar and parser handles this. But there are surprising edge cases you can hit on rare occasions. - Finally we get to the actual subject of this pitch â the second return. This code is written to first get a value, then use it for the return value. With this proposal, that
return
could be dropped. - At the end, there is a
!= NSNotFound
. In Swift, this would be represented by thenil
case of anOptional
return value. But even here, you could make the case this is less clear. The words "not found" clarify the meaning of the return value. PerhapsfirstIndex(where:)
should define a custom enum, with.found(Index)
and.notFound
cases, instead of re-usingOptional
.
So let's look at the Swift equivalent and talk about that. First, let's take a fairly literal approach, which came via @Paul_Cantrell:
func hasSupplementaryViewAfter() -> Bool {
let cellIndex = elements.firstIndex {
$0.elementCategory == .cell
} ?? .max
return elements.enumerated().contains { idx, obj in
idx > cellIndex && obj.elementCategory == .supplementaryView
}
}
Most people[1] will look at that code and agree that it is vastly better â clearer, more readable, more enjoyable â than the Objective-C code. Now, I am putting a finger on the scales by comparing this code to Objective-C, which is notoriously verbose. But hopefully this helps illustrate how important Swift's low ceremony approach is.
Yet there's no doubt that nearly all of the reduction in verbosity has, item by item, a "this is less clear, this could cause confusion" argument to be made against it. What's more, any one of these small improvements could be dropped, and the code wouldn't be much the worse for it. But if you dropped them all, the code ceases to be better. The changes all combine together to give Swift its low-ceremony feel. Their effects compound.
The challenge with this situationâthat to achieve the low-ceremony feel we desire, we must bring together many low-impact thingsâis that each one has downsides. Individually they have a small impact, so for each one, one might say âwe shouldn't do this because â due to those downsides. For each one, you can say âcome on, is that return/semicolon/paren/logner name really a problem? it's NBD, and consider downsidesâ. Each one is small, and so easy to argue against in isolation, but they add up to be huge.
Another fair counter to this is "OK the benefits multiply, but don't the risks too". But they do not. Firstly, because they are oughtweighed by the fact that ceremony free code is clearer. We'll come back to that point. But also, the cited risks often don't multiply. For example the beginner issue of needing to learn about single-expression closures, or of if let x {
: these are learned once. Whereas you then benefit from them forever. You learn a language once, then use it for years. Over-optimizing for beginners is the wrong trade-off.[2]
Another position to take is "OK fine, where we are now is good, but let's not go further". And to that I also disagree. Swift today is great, but I reject the idea that it doesn't need to be better. There are still places where devs coming from other languages say "huh, ok, but this other language I used to use had ". We should not deny Swift users the benefits other languages have, out of an excess of unjustified caution that Swift needs to be settled, or because downsides can be cited about this latest change that could also be cited about all the other features Swift already has that come together to make it the language it is today.
Ceremony reduction isn't the only direction Swift can go in future. There have been evolution proposals to increase ceremony where it is important. For example, the addition of any
to indicate use of existentials, based on the experience that lack of that ceremony was leading people to do the wrong thing and default to existentials when they probably wanted generics. But overall, the general direction of Swift is to reduce ceremony for generics too â from protocol extensions in Swift 2, to the recent addition of primary associated types.
Let's talk about the word clarity. I think this term tends to get misused, and conflated with verbosity. It's sometimes taken as a given that being more explicit always means being more clear. Let me try and argue it's often the opposite.
I called that first translation "literal" because this code does something to remain similar to the original: it replaces the nil
returned from firstIndex
with Int.max
.
Interestingly, this is one place where the Swift code is more verbose than the Objective-C code. indexOfObjectPassingTest
returns a sentinel value. And it so happens that this sentinel value is defined to be Int.max
, and the code later takes advantage of this: idx > cellIndex
is going to be false for all values when the value returned is NSNotFound
. But this is partly luck â the sentinel value could have been -1
, as it is in a lot of languages and APIs. Swift, by using optionals instead of sentinel values, forces this information out into the code, explicitly.
This is an example where Swift made a choice that is certainly harder for beginners. Sentinal values, for all their faults, are easily explained in a sentence or two. Whereas if you were showing this to a beginner, you may have to take a lengthy pause to explain the entire concept of optionals. But it would be a mistake to think that we should therefore use sentinal values, because they're easier to teach.
Once the optional forces the .max
explicitly into the code, you can then make another leap. This code could be clearer if either it avoided doing that redundant iteration over the whole array in the case of not found or just !
-ed the return value because that test can never fail (e.g. there is always one .cell
). Let's say it's the former. It can be rewritten like this:[3]
func hasSupplementaryViewAfter() -> Bool {
if let cellIndex = elements.firstIndex(where: { $0.elementCategory == .cell }) {
elements[cellIndex...].contains { $0.elementCategory == .supplementaryView }
} else {
false
}
}
This code is an improvement, both in performance (objectively) and clarity (subjectively). And it came about in part because of Swift's use of Optional
forcing the surfacing of the detail about .max
. But also this refactoring was obvious because the rest of the code was so short and clear. You can see all of it at once, because you are not distracted by the huge amounts of ceremony the Objective-C code did. So you spot improvements or bugs more easily.
Clarity is correlated to, but not synonymous with, code being explicit. The Swift code was less explicit in various ways, which made it shorter and easier to read, and therefore clearer.
This isn't always the case. Sometimes brevity is clearer, sometimes verbosity is clearer. let doubled = numbers.map { $0*2 }
is clearer than let doubled = numbers.map { i in i*2 }
. The i in
does nothing but distract. But sometimes the terse $0
syntax is worse. For example, I always name the inputs of reduce
explicitly even in one liners i.e. let sum = numbers.reduce(into: 0) { total, i in total += i }
because I can never keep straight which is $0
and which is $1
.
This shows another important point: you are still responsible for writing clear code. Nearly every technique Swift has for reducing ceremony can be abused, just like $0
above. This pitch will be another example. Just because a feature can be used to write obfuscated code does not been it is a bad feature, just that it can appear in bad programs.
Finally, I'll do a bit more boosting for this pitch. The code above has
if let cellIndex = elements.firstIndex(where: { $0.elementCategory == .cell })
Why did I write it like that? I'll be honest, it's so I could make it a single-expression if
statement and avoid the return. Really, I think it's better to break it onto a separate line:
func hasSupplementaryViewAfter() -> Bool {
let cellIndex = elements.firstIndex { $0.elementCategory == .cell }
if let cellIndex {
return elements[cellIndex...].contains { $0.elementCategory == .supplementaryView }
} else {
return false
}
}
That's clearer. But now we have our ceremony back. The return
s here don't add value, they interfere with readability. This pitch would remove them entirely and would be in keeping with Swift's long term vision of being a low ceremony language. I think this is better:
func hasSupplementaryViewAfter() -> Bool {
let cellIndex = elements.firstIndex { $0.elementCategory == .cell }
if let cellIndex {
elements[cellIndex...].contains { $0.elementCategory == .supplementaryView }
} else {
false
}
}
I get that many disagree, that to them the returns are load bearing. But in Swift, returns can be omitted. This isn't something newly proposed here â that is how the language is, today. We just have an exception they can't be omitted when that first declaration is present. And that isn't very well justified.
One more example. Here is some code from the end of a function:
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)
return Hit(
at: root,
by: ray,
normal: (point - center) / radius,
)
}
There are two returns
here. One adds immense value. Not just semantically: its presence and highlighting as a keyword draws attention, because it is important. It is an early exit, a place to care about when reading and writing. This is the point in the code where we determine the ray did not hit the object. This is why people like guard
, though it's not a good fit here.[4]
The ending return
has no such role. Functions return when they reach the end. That doesnât need re-stating, and doing so increases ceremony without increasing clarity.
By no means all. There are still some enthusiasts out there. â©ïž
Note, over-optimizing. We should still pay attention to the learning curve and progressive disclosure and make sure Swift remains an approachable language. It's a trade-off, not an absolute. But (and this is subjective) I just don't believe claims that newcomers to Swift are confused by single-expression returns, once they are properly explained. â©ïž
I would not be surprised to find there's a bug in my refactoring here. That shouldn't detract from the point: logic bugs can happen, of course. By removing the ceremony, we give ourselves more of a chance at spotting them. â©ïž
Lots of Swift devs like to use guard for every early exit, and I get why (it's part of the motivation for this pitch). But I think forcing the inversion of the predicate just to use guard is unnecesarily obfuscatory in some cases including this one. â©ïž
That's not what the leading -
means at all. It means it's an instance-level method, as opposed to a type-level method (which would use +
).
D'oh, you're right! Thank you. Though it's still a default-vs-not thing in Swift's case. Will edit.
So, here's the rub. Let's go back to the earlier example:
func hasSupplementaryViewAfter() -> Bool {
let cellIndex = elements.firstIndex { $0.elementCategory == .cell }
if let cellIndex {
return elements[cellIndex...].contains { $0.elementCategory == .supplementaryView }
} else {
return false
}
}
Though, in your view, both returns here are ceremony, it is also the case that the first return
is an early exit.
Before SE-0380, there would have been a singular least-ceremony way of writing this code (two return
s, no else
); today, there is a second (one return
, one else
); and with this feature, there would be a third, leastest way (no return
, one else
).
There is low ceremony, but I think we should be attentive that the pursuit of least ceremony doesn't lead to so many stylistic choices that choosing how to writing anything becomes a ceremony in itself.