The problem of “having to scan back up” seems to be exactly the same for the case of closures and local functions, but there we make do with return.
Furthermore, I would argue that it is pointless to understand any piece of code at all without knowing the context, so that is something you have to do anyway. You could never just randomly open a line of code and not first figure out the context.
Yes, I see the difference between “normal” control flow and a closure content as something that should be held clearly apart, using different keywords helps.
Well, besides maybe a not so perfect (?) choice of formulation I think there is some truth in what @wadetregaskis expressed, at least I do not see an intensional strawman trick here…
Swift is quite good in some cases to allow a “local” understanding of code, at least one should be careful not to make this worse. —
We should ask some less-experienced programmers what they think about the proposed variants before this discussion gets too philosophical
A strawman is simply a type of logical fallacy, it doesn't have to be intentional. But if we assume "new reader" to be someone familiar with Swift, the point simply doesn't follow. Swift users are already familiar with multiple contexts for return and other keywords, so another use case where return has its common meaning doesn't seem surprising.
Swift's "local" reasoning is actually pretty bad, as are all languages with type inference. Given no other context, how can you reason about a line of code that has no types at all? Hopefully naming can help you out but it's not guaranteed, especially if your "local" snippet doesn't include the relevant function name. This is also true of other implicit features like returns. Given the "local" snippet 5, what is it doing? You can't tell. Now, of course Swift strives to be understandable given limited context, and it largely is, but I don't see using return from expression changing that at all. After all, one of Swift's foundational assumptions is that getting rid of all of the repeated types and words from Obj-C is a good thing, that it helps to actually clarify the meaning of code by removing extraneous or redundant words, much like human writing. In recent years, Swift has doubled down on that principle with implicit returns in multiple contexts, and now value expressions. Suddenly pointing out that we lose context doesn't really make much sense to me.
As for less experienced users, that perspective is why I keep bringing up the "natural" (in the sense of common) reaction users will have to failures in multiline expressions. This is especially true for any user who adopted implicit return from closures before multiline inference was supported. In that case, the thought process was very similar to what we're talking about here. A user with an implicit return closure:
.map { x in
x * x
}
suddenly wants to add a print
.map { x in
print(x)
x * x
}
and compilation fails. They try the obvious solution (or what the compiler tells them to do) and everything works.
.map { x in
print(x)
return x * x
}
Why do we suddenly think users will act differently here? Why make their previous intuition incorrect?
The “difficulty” here is that “map” uses a closure and you need to understand this. “return” leaves the closure. In my opinion using “return” for if-expressions would be confusing. The keyword “use” would be a nice indication of what is happening.
As already said, we are repeating arguments here and too many “experts” discussing, better ask some “beginners” at this point, what do you think? (I know, a lot of strawmen here,,,)
My position is based on years of working with junior developers already, so, personally, I'm not sure what direct user surveys would bring. Juniors often lack the ability to describe their issues with the language (doing so requires knowledge you get from learning the language, at which point the hard parts change). But you do what you want.
i just wanted to say as someone who has read and written a lot of swift, i would find even this simple example hard to read, especially if formatted with the type annotations.
func example(arg:Bool) -> Int
{
let tmp:Int =
if arg
{
print("True")
return 1
}
else
{
return 0
}
return tmp * 2
}
the forums syntax highlighting is broken at the moment, but with the IDE theme i’m currently using, the = sign would be styled faintly, and it would be too easy to miss it and read:
func example(arg:Bool) -> Int
{
let tmp:Int
if arg
{
print("True")
return 1
}
else
{
return 0
}
return tmp * 2
}
either return from an outer function / closure or not depending upon whether /* let x = */ is commented or not sounds totally weird proposition to me.
I suggest these two bullet points being considered:
forgetting the usage of "return" to mean expression value (which is totally bonkers IMHO), whether to allow or prohibit "return" to return from the outer function / closure scope is not so obvious. We can use "fatalError" or "throw", why not return?
func foo() throws -> Int {
let x = if condition { 42 }
else { fatalError("xxx") } // ✅
let y = if condition { 42 }
else { throw xxx } // ✅
let z = if condition { 42 }
else { return 24 } // 🛑 🤯
}
whether to allow more than one "exit" from a single branch and whether to run the code after an exit:
"pascal" option. Allow more than one "exit" from a single branch. The code after "exit" still runs
let x = if condition {
then 42 // default value
print("42") // prints 42
if subCondition {
then 24
print("24") // prints 24
}
// expression result is 24 when subCondition is true
} else {
0
}
prohibit more than one "exit" from a single branch. No code runs after the first "exit":
let x = if condition {
then 42 // default value
print("42") // the code is unreachable warning
if subCondition { // the code is unreachable warning
then 24 // error
print("24") // the code is unreachable warning
}
// expression result is 42
} else {
0
}
allow more than one "exit" from a single branch. The code after "exit" does not run:
let x = if condition {
if subCondition {
then 42 // expression result is 42
print("42") // warning: this code is unreachable
}
if otherCondition {
then 24 // expression result is 24
print("24") // warning: this code is unreachable
}
then 123 // expression result is 123
print("123") // warning: this code is unreachable
}
whether to allow more than one "exit" point from a single branch and separate to that but equally important whether the code after "exit" is being run or not – is not mentioned in the proposal. Note that only option #2 allows "a bare last expression" rule. Note that a similar thing was allowed in Pascal which chose option #1 from the above option list:
FUNCTION FOO: INTEGER
BEGIN
FOO := 42; // default value
IF subCondition THEN BEGIN
FOO := 24
END
END;
Why? Commenting out code can change the meaning of other code arbitrarily, so why is this particular case so special it needs a whole keyword? You can get similar bugs by commenting out regular if statements, parts of loops, or really any other construct.
That commenting out lines of code changes the meaning of the rest of the code shouldn't be a surprise. You could say the same thing about deleting code. And in at least some cases, an unexpected early return will produce a "will never be executed" warning about the lower code, so there's at least something the compiler can do to help here.
At the very least the question about whether it's a return or then or some other keyword or "no keyword" is secondary to the questions I raised above as different answers to those questions invalidate the answer at hand completely: e.g. "return" could always mean "returning from an outer function / block" (which would be in line with the principle of least surprise and that would require either some other keyword or the use of the bare last expression rule as the expression value) and whether the code after "exit point" runs or not: in case it runs it would be doubly weird to use "return", don't you think?
I'm not sure how those points are relevant to this pitch, as adding rules around continuing execution isn't part of this pitch. But what I would consider weirder than whatever you've come up with is if expressions worked differently than other constructs in the language. Really the only thing you've brought up that might need to be dealt with is that you can throw out of an expression, which does seem weird to me and should really be banned. It may be too late at this point but it seems like it would instead work like rethrows where you'd have to use try.
let x = try if y { 1 } else { throw SomeError.notOne }
But ultimately I don't see a reason for return from nested expressions to mean anything different than return in nested closures or functions. Those can't return through their nesting, neither should expressions.
Which is exactly why I brought it up here as I think it should be part of the pitch ;-) At least considered in the "alternatives" section and / or discussed in this thread.
Here is where we are disagreeing as well: I'd rather see it aligned the other way around (allow both throw / return to mean exiting from the outer function / closure.
That's why we have this discussion here to begin with: the difference of opinions on different bullet points!
Reading through the thread, I'm becoming more fond of using return.
Regarding throw and try, the behavior can differ because the recipient of errors is not always the same as that of return.
For throw, the recipient is the nearest throws closure/function or do { try ... } catch (and /try?/try!).
For return, the recipient is the nearest -> T closure/function or if, switch, and do expression.
I don't find that too confusing. What you need to understand is just one more thing. Values returned by return can now be received by the nearest if, switch, and do expression.
The status quo that Jon_Shier mentions about return in closures within functions not meaning the same as return elsewhere in functions is something I've thought to be an unfortunate thing in Swift. It often confuses me in code of mine that I'm reading, having to catch myself when finding a return and backtrack to reason about which situation it is. I'd propose less of that not more.
I was trying to promote a generalized use keyword a few days ago, but didn't include an example. It could become idiomatic to use in closures instead of return to avoid the confusion I'm talking about:
func example(arg: Bool) -> Int {
let a = something.map {
print("$0")
use $0 + 11
}
let b = if arg {
1
} else {
print("False")
if foo() {
return 7 // this exits the function with 7 as the result
}
use 0
}
return a + b
}
(Even further generalized aspects of use my previous post rambled on about ... nevermind, out of scope for here. Well, it's use in closures is out of scope here too, but I guess I'm promoting the use keyword for the expressions we're talking about here which can be extended to make further improvements to the language over then which I'm thinking cannot)
Caveat: one might make the mistake in thinking if foo() { return 7 } could also be used within the map closure like it was the if expression. Hmm.
[edited: express personal opinions about return statement that confuse me rather than declaring an absolute judgement that its a bad part of the language]
I don't see the "Pascal"-ness of your option #1. The only similarity in the Pascal code sample is the order of IFTHEN and the sub-block following, or am I missing something?
Flip the condition around to the put the complex part in the else part instead and the nice order of ifthen, and ifthenelse, that I think is drawing some people in about this pitch goes out the window. Instead you get ifelsethen.
let x = if !condition {
0
} else {
then 42 // default value
print("42") // prints 42
...
}
I used pascal example above to illustrate that it allows code after assigning the value of the function (and it allows subsequent reassigning value of the function). In our case that would correspond to say:
let x = if condition {
use 42
if subCondition {
use 24
}
print(use)
} else {
...
}
which allows "use" or "then" but won't work with "return" or "bare last expression".
Note: I am not saying that this is necessarily how it should be, merely that it was not discussed (thoroughly if at all) and thus not been considered.
You forgot to add "IMHO" or that "this is one of the possible design options", etc.
To me that would be too weird, as I treat "functions/closures" very different than "if/switch/do" expressions. To me "if/switch/do" expressions are more close and similar to "if/switch/do" statements in regards to "return" – in the "statement version" return returns from the outer function/closure scope, and to me it would be too confusing and surprising if the "expression version" of "if/switch/do" does any different (including being a compilation error or being a value of "if/switch/do" expression itself).
Oh I'm sorry. The nuances of my native language seems to be lost during machine translation Of course that is my humble opinion. I'm just suggesting one of the possible design options.