If it was for “traditional” commercial environments, Swift would not have type inference. This was the reason why you always had to write all those types also in later versions of Java, because corporate environments wanted them (and distrusted their own developers).
I'm not talking about traditional environments, at least not in the old-school Enterprise Java sense (I also don't really advocate for a poll!) I just wanted to illustrate how 'raising the stakes' for me increased my dislike of the proposal, and wondered if it was a productive thought experiment for other people who are on the fence.
A feature that I'd fall into using in personal projects, but disallow in professional work, feels like a feature that belongs more in loosey-goosey languages like Typescript, not a language like Swift.
If this proposal goes through you WILL be using implicit returns whether you like it or not. Your code base will be filled with implicit returns. In most cases it won’t matter much at all because you’ll just be returning Void like you already were.
But in other cases, your code will change meaning. Your if and switch statements will suddenly become expressions. Many of them will be invalid because not all branches are the same type. So you will have manually fix that. Some rare cases will be valid but produce a different type.
You can opt IN to explicitly returning but that’s not helpful because that’s not what your code was doing before the proposal.
So now you just need to refactor.
This problem is pretty small. But multiplied across every single function and closure it is definitely not small.
I don't have this feeling. The previous Pitch was the exact opposite, it had no implicit return and it had the keyword then in multi-line expressions which was not optional.
Remember that if and switch are only allowed to be expressions when assigned to a value or used as the value of a return statement. Any if or switch that is not an expression, could only become an expression under this proposal if the ifor switch is both the last statement in a function and contains no return statements. This is very unlikely to be true in a function that doesn't return void.
The only reasonable expectation of breakage lies in the use of @discardableResult functions, which already cause problems in single expression closures. Those cases often already result in an unused variable warning, as demonstrated on godbolt.
As a suggestion, I don't think it's unreasonable that the compiler should infer that a final if or switch is the return expression instead of a statement only if the inferred type of said expression is compatible with the function result.
im about as polar an opposite as could exist to a “corporate” creature and i still use type annotations everywhere. they speed up builds and make it a lot easier to read code i (or someone else) wrote a long time ago.
I haven't been able to think of a legitimate use case for any keyword-fronted expression returning Void. Do they exist? (It's supported for if/else/switch but I bet nobody would notice if it wasn't.)
If they don't exist, there's no problem reusing an old keyword. I don't like continue but it's available.
let x = do { // x is `Int` if `Void` is disallowed.
let xs = [1, 2, 3, 4]
for x in xs {
doSomething(with: x)
guard x > 2 else { continue }
print(x)
doSomethingElse(with: x)
if x == 5 {
continue x // This is confusing if the for loop or do statement is labeled as `x` but nobody cares because nobody would do that.
}
}
continue 55
}
To clarify, are you saying that if the "inferred type of said expression" is incompatible with the function result then it should be interpreted as a statement instead of an expression? Does that mean that the function would now return Void? What if the function signature doesn't return Void either?
Please don't second-guess people's motivations this way. Ben is pitching this and strongly arguing for it because he thinks it's a good idea, and you are arguing against it because you don't think it's a good idea. We do not need to imagine additional hidden interests.
You are right that this is not a democratic process. There is no way to make it a democratic process, even if we wanted to. The membership of these forums is far from representative, and that only gets worse as you drill down to the ever-narrower sets of people who post at all, participate in evolution, and actively engage in arguments in pitch threads. Even if we could magically solve all that, it would still be an internet poll, prone to brigading and all sorts of other manipulation. That is why we always talk about the evolution process as an opportunity to evaluate arguments and gather feedback rather than as anything like a vote.
No, it means that the function returns whatever you declare it to return, and untyped closures will default to assuming that you are returning the last expression just like they currently do with single expression closures. The assumed to be a statement idea was only meant for functions and closures that have a declared return type to check against.
Although I just realized that if the inferred type of the last expression expression doesn't match the return type, then it wouldn't have compiles in the current version of Swift anyway, because it runs off the end without a return.
Things brings up a diagnostics quality-of-implementation question—
Currently, if you leave off an appropriate return statement in an early-exit branch, the diagnostic will highlight your error at the correct spot, while the rest of the code on the happy path will return as intended.
But recall as discussed above that, to avoid backtracking, if expressions use only the first branch for type inference (unlike ternary a ? b : c).
With this proposal, will there be scenarios (maybe only in closures?) where the first early-exit branch that "runs off the end" will be taken as correctly written, and the rest of the code will have unexpected errors or (worse) compile with unintended return types?
I'm thinking in particular of what would happen if later branches end returning the result of calling a function f<T>(..., _: T.Type = T.self) -> T, and T ends up inferred to be whatever arbitrary thing was left as the unintended last-statement-as-return-expression in the first branch.
That’s assuming the reader is correctly counting nested braces, and has already determined the scope of the block. I don’t think this is how everyone scans code.
Another criteria is the likelihood of introducing errors that are syntactically correct, which I believe is higher and significant since it’s only a one-character shift across braces. By hypothesis, these errors require testing to be discovered, so they are also expensive. That risk is not presented in one-liners.
I say this reluctantly because the proposal would be my personal preference, but I find it concerning for disparate teams and application developers.
This is where I lost the Swift code. i.e. where I could no longer figure out what it's doing and how. Part of the problem is that I don't have a clue what the type of cellIndex is supposed to be, so I need to do like 3 or 4 documentation dives before figuring out which type is either an enum with a .max case or a type with a .max static constant of the same type.
By contrast, the only thing confusing about the Obj-C code was that the extremely long closure type made the line too long to parse. A typedef would likely take care of that. Inferred closure type is the only thing you need in order to render the Obj-C code clearer than the Swift one.
So I don't think you've shown a case of multiple steps of ceremony removal making the code more readable. What you've shown was one big and necessary step forward, and then several small steps backwards, and then showed that cumulatively they're still sum up to a step forward. Then coming to the incorrect conclusion that each step adds, even though only one of the steps was actually forward.
I would honestly make this a guard, thereby removing the weird dangling false:
The "ceremony" returned, except now it looks more meaningful. More importantly: Happy path at the bottom. This better delivers the intent of the code, rather than simply how it works. It also makes it easier to expand the function to verify a sequence of 3 or more items, instead of just 2. Done with happy path at the top, that would require nested ifs.
It's a perfectly good fit here. The condition starts with !. That would be reason enough. The issue comes from having a nested condition, which, in turn, is only necessary because we need the root which "matched". If we didn't need the root, this could have been written much simpler:
guard bounds.surrounds((h - sqrtd) / a) || bounds.surrounds((h + sqrtd) / a) else {
return nil
}
Mind you, the nested form is unclear anyway. The two roots are equivalent, but they look like they aren't. What if there were 3 or 4 possible roots? It's clearer if flattened:
let root
let candidate1 = (h - sqrtd) / a
let candidate2 = (h + sqrtd) / a
if bounds.surrounds(candidate1) {
root = candidate1
}
else if bounds.surrounds(candidate2) {
root = candidate 2
}
else {
return nil
}
The issue here is that candidate2 went from being calculated only in the else to being calculated unconditionally. The problem is that you can't bind together the definition and use of candidate1 with the if in the way that lets you else both at the same time.
For comparison, here's a version with IIFE that better expresses the intent
guard let root = { () -> Double? in
do {
let root = (h - sqrtd) / a
if bounds.surrounds(root) {
return root
}
}
do {
let root = (h + sqrtd) / a
if bounds.surrounds(root) {
return root
}
}
return nil // Unfortunate case of happy path at the top
}() else {
return nil
}
Which could be farther made clearer using iterator operations
However, this goes back to unconditionally calculating the second root. Additionally, the literal array may cause an allocation that should be optimized away. If Swift had proper generators, both issues could be resolved [1]:
guard let root = (generator {
yield (h - sqrtd) / a
yield (h + sqrtd) / a
}).first(where: { bounds.surrounds($0) }) else {
return nil
}
Another form possible today is if bounds.surrounds was replaced with a function that, instead of returning true or false, returned root or nil:
guard let root = bounds.nilUnless(surrounds: (h - sqrtd) / a) ?? bounds.nilUnless(surrounds: (h + sqrtd) / a) else {
return nil
}
More generic option: Create an extension T.nilUnless((T) -> Bool) -> T? and use as ((h - sqrtd) / a).nilUnless({ bounds.surrounds($0) })
This is less pretty than a generator if the root count increases. But as a bonus point, it doesn't need a new language feature.
The return here tells me that this line is creating an object of type Hit and returning it. Swift's lack of new would, without return make this line indistinguishable from a call to a function called Hit that has Void return.
Notable, this would be the first line in the entire example that doesn't start with a keyword. Look at all the lines above it: let, if, var. With the function leaning in such a way, it would be hard to tell there's even a statement starting there, as it, at a glance, might be confused as a continuation of the previous statement.
Like a non-escaping closure, a non-escaping generator should be optimized away to 0 allocations. ↩︎
More simply: The difference is between code that describes how something is done, and code that describes what is done.
The "positive" examples for removing return seem to start with code that already shows how something is done. So removing the return raises no issues. It's unclear before, and unclear [1] after.
By contrast, if you refactor that same code to show what is being done, suddenly the return becomes meaningful. It expresses intent. And removing it reduces that expression, and obfuscates the intent.
So the question that should be asked is: Does removing return make it easier or harder to express the intent of code? And is it worth introducing ambiguity in closure type inference for it?
True, I have never seen code with block syntax not typedeffed away to simplify it (http://goshdarnblocksyntax.com/), so it is a bit cheating showing it without typeset being used. Still it was a bit of an ugly corner of the language, but it barks far louder than it actually bites ;).
I would welcome this change if only because using single statement returns is already very practical in contexts of properties and using a switch or chain of if statements to determine a value instead of using an inline closure, and breaking down complex lines to achieve this would be a good thing. Most importantly though, as the pitch outlines, adding any single line to these constructions after the fact is tremendously disruptive, and holds the potential to introduce bugs in the re-write rather than augment or fix an issue. I'd imagine those that are against this style could use a linter to avoid it (and the existing instances of it) in their own code bases, as I don't see how lacking a return at the end of such statements could cause actual problems.
The only argument against that I could think of is forgetting to mark the return type of a function, and not getting the diagnostic that the return statement disagrees with the value actually being returned, but since Swift enforces the return type, and would warn if you tried to use a function and assign it to something, I can see disaster being avoided (well, at least in most situations… async let returnValue = functionMissingReturnValueButMeantToReturnSomething()may lead to a few surprises, at least until await returnValue is used?)
To those arguing that this will end up just like goto fail, do note that there are different ways of solving that problem as well, than just saying {} braces are always needed. Consider a same-line requirement, for instance:
if (someCondition) return; // Would succeed
if (someCondition)
return; // Would fail to compile
I've similarly argued simplifying the language not for if statements, but for guard else statements, that are almost always two word return statements that read better without the {} getting in the way. I similarly don't think leaving the return statements out gets in the way of reading familiar, or even unfamiliar code — we are poor readers of code to begin with, and removing extra keywords will likely help parse code more quickly as our brains were skipping the ceremony already without most of us realizing it.
It's been almost a decade and a half since I learned of this, so forgive me for being unable to quote the study, but from a human language point of view, redundancy in speech is a good thing because it helps us catch phonemes that are dropped or hard to hear — this is why we conjugate verbs despite other words surrounding them giving us the same information. In written language though, redundancy tends to complicate things quite a bit — via eye tracking, it is largely skipped over (though different groups from different languages focus on the different redundant part), and many learners of a language, typically children, second language learners, and people with reading difficulties like dyslexia often misapply the redundant rules. The argument that written language should have settled by now to be most optimal falls through when we look at the largest literate group we have ever seen: people on the internet — I'm sure everyone here knows someone who speaks properly, but leaves almost everything out when texting, no matter their age
Point being, I see dropping the return statement when it can be safely inferred to be more welcoming to all involved that will be coming to Swift. Those that wish to include redundancy when writing the code still can, and will get the additional benefits of the compiler checking their work, but when the redundancy isn't necessary, I will surely welcome the lowered mental overhead.