Hopefully this pitch makes it pretty rare. A huge class of closures are a single expression. This will allow implicit returns within closures in many more cases.
Not just closures, local functions as well... and functions are closures by and large.
One of the older approaches is labels
:
func foo() {
func bar() -> Int {
DispatchQueue.main.async {
...
return from closure // alt: `closure return` or `closure.return`
...
}
...
return 42 from bar // alt: `bar return 42` or `bar.return 42`
...
}
...
return from foo // alt: `foo return` our `foo.return`
...
}
Having said that, if the function is long (and thus hard to follow) typically it is already a problem on its own.
FWIW, I really love the direction this is going.
However, if some branches can return/throw, is try
required here?
let x = if flag { 42 } else { throw someError() }
// ^ try?
Similarly, would a decoration be needed for an expression that might return
on some branch? Such an expression has an "unexpected" control flow and that seems to contradict the "no surprise" reasoning behind the ubiquity for try
.
For example, Ben's typo above is a great example, as that snuck through and is legal under the proposal but had a surprising side effect that he didn't intend -- and one that the compiler wouldn't diagnose.
Perhaps it would be sufficient to allow non-returning do
(without catch
) and defer
blocks within branches (as well as implicit returns in general), to group code that is not the returned expression.
I suppose unless do
was allowed after the implicitly returned expression this basically becomes returning the last expression. However, it makes the control flow clear (IMO) while not feeling like a language workaround like { immediately called closures }()
for the sake of a print()
statement or other side effect.
let value = if someCondition {
getSomeValue()
} else {
do { print("Condition was false") }
getSomeOtherValue()
}
Edit: I suppose this would not allow declaring variables that are used as/in the returned expression:
let val = getSomeValue()
print(val)
return val
This pitch seems alright. The ternary operator sets precedent.
However, the suggestion to always implicitly return
the last expression in a scope is awful and would severely harm the readability of Swift code. Explicitness, both for types and keywords, should never be underrated. Conveying intention multiple times to the compiler lets the compiler check programmer logic and consistency. The call/return pattern is foundational to structured programming; we should be extremely cautious when debating adding another implicit return
scenario to the language.
In some cases it would be extremely confusing indeed:
let x = if foo() { bar() } else { baz() } // ok
guard let x = foo() else { bar() } // not ok
// means the same as:
guard let x = foo() else { return bar() }
// or even
guard let x = foo() else { } // not ok
// means the same as:
guard let x = foo() else { return }
on guard statement
guard statement always felt "out of this planet" to me as it's "else" block is not just a block (as it needs to be a special case kind of block that has a return statement) neither it's a closure (that has an implicit return anyway which returns from the closure itself, not from the outer function).
Thanks Ben — I’ve been wanting to write a pitch for this functionality, particularly with switch, for some time.
Many times I have thought, “why can’t I just say let foo = switch…
?”
So I am obviously in support.
The one thought/question — is there any issue with existing switch statements that return values (as return values from functions, for example), to not have them interpreted as an expressions whose result value is unused? perhaps this is a no -issue
+1 Edit: +0
While the basic pitched idea would make Swift more consistent and thereby simpler, this exception would be unexpected and the cause of lots of frustration and would re-introduce language complexity.
I'd want each branch to be like any closure or function returning something.
Are you taking into account the fact that, currently, returning from an if block returns from the enclosing function? I also feel uncomfortable about the single-expression restriction, which is why I voiced support for the idea of a new keyword for this purpose, but as far as I understand using return
as you suggest is simply not possible, because return
already means something completely different in that context
Ah, I did not I was too captured by the benefits.
But then, mixing control flow syntax with expression logic might actually not make Swift simpler but rather foster the confusion that I just had.
There are countless ways to syntactically marry the two concepts but they would necessarily feel forced, contrived, inconsistent, compromised, arbitrary ...
This reminds me of the discussion around result builders, which also involve familiar looking scopes that don't allow much familiar code. Only this pitch here repurposes syntax whose look, meaning and behaviour are already established. I'm not saying repurposing is bad per se, I'm just seeing all these little (or not so little) evolution steps that gradually make the language more complex, and I'm wary of that.
Entertain this possibility for a moment: Five years from now, new hip languages will pop up to replace the old generation, and their value proposition will be: It's simple, consistent and quickly learned but just as powerful. Swift got too complex and inconsistent because its open-source evolution process lacked the strategic oversight and wisdom to see the language as a whole and the direction it went.
This argument is not at all countered by the intent to disclose complexity on demand and make new features optional and non-breaking. People will use all those features combined to write code. And then other people (whom I pity dearly) will have to read it.
We tend to underestimate how making a system a little more complex increases the cognitive load in understanding and using that system: The user would sometimes see a switch block that really isn't one and would see an if-else block that really isn't one. And then, whenever she does see a real switch- or if-else block, she would have to look twice. And even though that would just take moments, it would gobble up brain power overall and feel yet another bit more draining.
How does this rule apply to leading-dot syntax (e.g. for enums) when the type of the expression is already known (e.g. when it's the return value)? Consider the following use case:
func statusColor(forCode code: Int) -> Color {
switch code {
case 200..<300: return .green
case 300..<400: return .yellow
case 400..<600: return .red
default: return .primary
}
}
With this limitation, I'm not sure the expressions would be inferred to be of type Color
, allowing me to leave away the type. Instead I'd be left with this:
func statusColor(forCode code: Int) -> Color {
switch code {
case 200..<300: Color.green
case 300..<400: Color.yellow
case 400..<600: Color.red
default: Color.primary
}
}
…which I'm not sure is an improvement. Ideally, I'd be able to write e.g. case 200..<300: .green
This use case covers around a third of case x: return y
occurrences in my code, so it's pretty important to me.
The type context of Color
would still be provided, so you could use case 200..<300: return .green
Oh, that's perfect! I'm all for this pitch then: I think the limitations make sense, at least for now.
Hi everyone – thanks for the feedback so far. There's now a proposal PR and implementation.
The PR incorporates feedback from this thread:
-
do
,break
/continue
, multi-statement branches as future directions - explicit that binding in
if
andswitch
is supported - clarifies just
if
builder expressions, not assignments, are unchanged in result builders - alternatives considered covers a
->
variant
I don't really see why it would be, any more than a throw
in an if
statement today might. You need try
on function calls that throw to make sure it's clear your function might exit. But open-coded throws inside a function are clear enough.
as that snuck through and is legal under the proposal but had a surprising side effect that he didn't intend
I don't think a compiler can stop you writing incorrect code. There is a big difference between things a compiler can catch – type errors, unreachable code, flagging calls that would exit the function abruptly because they throw – and then there are logic errors (like getting your if
and else
branches the wrong way around) that are just unavoidably on the programmer. The extra return typo falls into the latter bucket IMO.
That's a fair counterpoint. I don't love that "let x = try ...
" isn't a reliable scan-marker for my eye but you're right.
I half-heartedly ponder that the syntax could allow a try
here, but I dislike even more the idea that there isn't exactly one "correct" way to write this.
I dislike the invisible return
s of the current proposal quite a bit. Seems like a clear bug generator when someone comes to work on the code later and writes code below the switch statement thinking it'll execute.
So this, or some other way that makes it clear these are not simple switch statements but returns/exits from the enclosing function seem very important to me.
Nice work, Ben and Hamish.
Is there any chance of getting this much-upvoted suggestion into the proposal — if not as part of what’s actually proposed for this round, then at least as a future direction?
Lack of full generality was one of the most common objections above, and doubtless will be so in formal review as well. People seem to like the solution above. It addresses all of the concerns in the Future Directions | Full Expressions section of the proposal:
// ❌ wat
for b in [true] where switch b { case true: true case false: false } {}
// ✅ meaningless, but easy enough for humans to parse visually
for b in [true] where (switch b { case true: true case false: false }) {}
var body: some View {
VStack {
if showButton {
Button("Click me!", action: clicked)
} else {
Text("No button")
}
.someProperty // ❌ parsing ambiguity: static property, or property of View?
}
}
var body: some View {
VStack {
(
if showButton {
Button("Click me!", action: clicked)
} else {
Text("No button")
}
).someProperty // ✅ no parsing ambiguity
}
}
…and I don’t think anybody identified a downside.
I don’t want the idea to get lost. It may well be too much to consider in this proposal, but at a minimum, it’s a useful answer to those future direction concerns.
Thanks everyone for your feedback so far! I've kicked this off for review over here:
Please provide further feedback over on the review thread. Thank you!
Holly Borla
Review Manager
Sorry, I didn't get to incorporate it yet but I will put this approach to full expressions into the future directions section. It is an interesting question whether, if/when we go that direction, parentheses should be mandatory vs just ensure sure compatibility.