Totally agree here according to my experience with other developers and code review.
extension Result {
var isSuccess: Bool {
switch self {
case .success: true
case .failure:
print("failure")
(return | then) true
}
}
}
If goal is to solve a cliff â return solves this nicely. In this example if a developer accidentally wants to print a value and remove this print later (e.g. after debug) â return helps with it.
I believe there is no difference in this example between then
and return
for most of developers. There is no difference for them whether this expression or statement. What is needed here is convenient syntax without return return return
most of the time and the ability to rarely add a print for debugging.
Slightly more general example made out of yours, which AFAIK is how it is suggested in the pitch:
extension Result {
var isSuccess: Bool {
let (ok, err) = switch self {
case .success: (true, 0)
case .failure(err):
print("failure")
then if let err = err as? NSError {
(false, err.code)
} else {
print("this is not NSError")
then (false, 123)
}
}
print("err: \(err)")
return ok
}
}
Will the following form be also allowed? cc @Ben_Cohen
extension Result {
var isSuccess: Bool {
let (ok, err) = switch self {
case .success: (true, 0)
case .failure(err):
print("failure")
if let err = err as? NSError {
then (false, err.code)
}
print("this is not NSError")
then (false, 123)
}
print("err: \(err)")
return ok
}
}
I'm personally ok with return here. It is pretty clear that:
var isSuccess: Bool {
let (ok, err) = switch self {
case .success: (true, 0)
case .failure(err):
print("failure")
if let err = err as? NSError {
return (false, err.code) // `return` here is tied to switch expression
}
print("this is not NSError")
return (false, 123) // `return` here is tied to switch expression
}
print("err: \(err)")
return ok // `return` here is tied to computed var
}
In practice it is common situation that inside expression the returned value is tied to enclosing expression.
If someone need to return from body of computed var he can use deferred initialization pattern:
var isSuccess: Bool {
let (ok, err)
switch self {
case .success: (ok, err) = (true, 0)
case .failure(err):
print("failure")
if let err = err as? NSError {
return false // `return` here is tied to computed var
}
print("this is not NSError")
(ok, err) = (false, 123)
}
print("err: \(err)")
return ok // `return` here is tied to computed var
}
and
var isSuccess: Bool {
switch self { // implicit return of `switch expression value` tied to computed var body
case .success: true // implicit return tied to switch expression
case .failure(err):
print("failure")
if let err = err as? NSError {
return false // `return` here is tied to switch expression
}
print("this is not NSError")
return false // `return` here is tied to switch expression
}
}
It is unserstandable according to current swift rules. What problem then
keyword will solve in these cases?
As was brought up several times upthread, quite many people consider return
should only return from closures / functions, but not be the value of if/switch/do expressions â because "it would be confusing otherwise". (Personally I won't see a problem of return returning from the "outer" function closure like in Rust, Ruby, Kotlin and Scala, but, again, many people consider that equally confusing, so we won't have that either.)
var isSuccess: Bool {
let x = switch self {
case .success: true
case .failure(err):
print("failure")
return false // THE CONFUSING PART
}
print("x")
return x
}
Wouldn't it be better to describe this possibly confusing part in documentation? There are also many other language constructs that can be confusing for some people and not confusing for others: multiple defer blocks, double and tripple optional values, subscript call on dictionary with optional values, min / max functions giving incorrect result for FloatingPoint values and structured concurrency features.
We can explain to people that in your example THE CONFUSING PART
won't return the computed var body, it returns value for switch expression / let binding. The rule is simple â as every nonthrowable function, expression must return a value. It can not return the body of outer scope.
The "confusing part" in this case isn't that people won't know how return
works in that context, it's that readers will have to take in a lot more context for every return
they see when reading a function and reasoning about it. That cognitive overhead can't be fixed with documentation.
return returning from the "outer" function closure like in Rust, Ruby, Kotlin and Scala
Hmm, this is a really valuable look at how this works in peer languages. I had previously been supportive of using return
over then
, but seeing how this works in so many other languages makes me reconsider. While we aren't beholden to the design choices of other languages, it seems like important signal -- this probably tells us at least a little on how people will intuitively expect this to work.
So I'm definitely seeing the appeal of using then
instead, to avoid the potential confusion and diverging from how this works elsewhere.
Iâd also add that this is, to me, a much stronger argument against âreturn
should mean something different in this position than it does in other languagesâ than it is against âwe should support the mid-expression return pattern.â
I still think we should be cautious about going down that road and I donât think this feature would in any sense feel incomplete without that ability.
I wish someone could explain roughly why it would be
difficult or undesirable to make something like this possible:
var someInt: Int = random()
x = if condition {
use someInt
someInt = 0
} else {
use 5
}
Agreed â and thatâs from someone whoâs not necessarily opposed to the mid-expression function return. (Aside: Iâve used it in those other langs and itâs really not so bad, but itâs also not particularly essential.) Itâs both reasonable and precedented for people to think that return
means âgo to the end of the function/closureâ in any context. People lament Swiftâs proliferation of keywords, but I lament even more languagesâ habit of giving the same keyword multiple meanings. Good names are the heart of clarity.
Thereâs a related argument here: overloading the meaning of return
is confusing within Swift, even if we ignore precedent from other languages. Consider this code:
func f() -> String {
if .random() {
return "verdigris"
} else {
if .random() {
"viridian"
} else {
return "zaffre"
}
return "eburnean"
}
return "gamboge"
}
Now what happens if we add _ =
at the top? The outer conditional becomes an expression, and suddenly, despite having 4 return
statements, the function can only return "gamboge"
:
func f() -> String {
_ =
if .random() {
return "verdigris"
} else {
if .random() {
"viridian"
} else {
return "zaffre"
}
return "eburnean"
}
return "gamboge"
}
Figuring out whether a return
exists inside a (possibly deeply nested, possibly distantly created) expression context in order to divine its meaning creates an âaction at a distanceâ effect that I donât think is ideal.
Edit: Iâd missed @allevatoâs post making the same point.
Personally, I think the only way return
could ever work within expressions is via (implicit) labels as was suggested previously. I'd be interested to see more discussion around this, exploring any potential problems/concerns/edge cases and whether they can be resolved. E.g. what would happen with a bare (no label) return x
within an expression? Would it be disallowed or would it always return from the function?
My belief is it should only be permitted if there's only one possible interpretation - return from the named function. Otherwise a labelled return (or some other syntax, like then
/ use
/ whatever) should be necessary in order to avoid ambiguity.
Following Swift's sort of general principle that the compiler doesn't like to make assumptions in the face of ambiguity (because that means humans reading the code would also have to, and - unlike compilers - humans are bad at making assumptions consistently).
That said, I'm less sure how well that works for existing functional patterns like using map
/ filter
/ etc. I generally write their closure arguments as pure expressions wherever possible (and proper named functions otherwise), to avoid the return
keyword and any potential confusion, but you certainly can use return
in them today and it does not mean return from the "parent", named function. Conceptually changing that existing use of return
to e.g. then
is simple, but it'd be a big change mechanically and socially.
I don't think that would cause anything but a disservice: there are local functions which are very similar to nested closures.. You are not suggesting that "return" could be used only at the top level function/closure, right?
let globalVar = {
func foo() -> Int {
func baz() -> Int {
return 3 // and not here?!
}
baz()
return 2 // but not here?!
}
foo()
return 1 // allowed here
}
It seems like at least one big reason people want this is to avoid reformatting during print debugging, etc. I use the following custom ~>
operator based on ideas from the "With functions" pitch.:
@inlinable @discardableResult public func ~> <T>(_ value: T, modifications: (inout T) throws -> Void) rethrows -> T {
var value = value
try modifications(&value)
return value
}
// Typical usage
let rect: CGRect = self.parentRect ~> { $0.size.width = 320.0 }
Besides allowing values to be modified in place, it can also be used inside of expressions to ensure they're still a single statement and avoid the parenthesis on an anonymous closure. You can simply add it to the end of the line without adding an extra return.
func doSomething(value: Int) -> Bool {
switch value {
case .min..<0: true
case 0..<1000: false ~> { _ in
print("Why did this happen?? \(value)") // Set breakpoint here
}
default: true
}
}
var goodGetter: Bool {
self.value ~> { _ in print("Using someGetter \(self.value") }
}
var grossGetter: Bool {
print("Using someGetter \(self.value)")
return self.value
}
I like the idea of return
being available anywhere, but I donât have much experience with languages which permit that, to know if it has downsides.
I was thinking it could be considered unambiguous for a plain return
to return from the nearest named function, but thinking about it more and looking at your example, perhaps that would still permit too much ambiguity⌠technically what really matters is whether the context is escaping or not, I suspect. Itâs the non-escaping cases that cause the most ambiguity (and they are perhaps the only cases where you could implement a âfarâ return?).
Itâs the same amount of context they already have to take into account though. No different from a closure. It seems to me that it behaves like a closure in some other respects, so I personally wouldnât be confused at all that returns also behave as in closures.
Perhaps the issue is that these are technically expressions, which is different from closures, but I would guess that most users arenât philosophers, they will just see something that behaves like a closure and expect returns to work the same.
Also, I donât see how return could possibly mean âreturn from the outer contextâ here, since there is an assignment, and since the return type typically wonât match. So how could anyone really be confused?
I am baffled by how many people seem to be fine with "return
everywhere".
The biggest issue I have with this part of the discussion is how it disregards the fundamental differences between expressions and closures (or "anonymous functions" if you will). Those are very different things that just happen to both come wrapped in curly braces.
I believe we are ill-advised if we muddy the waters in this regard: closures are functions (+ captures), expressions are not.
You can't pass around expressions and call them later, you can't have "input arguments", and they sure do not "return" anything (in the OG meaning of returning).
As for the "labels to the rescue" idea:
To me the point of this pitch is to be able to spell expressions that evaluate to some value more concisely and with less drama. Requiring labels feels like going in the opposite direction.
The whole idea is that a chunk of code "evaluates" to some value, throwing in control flow-y things like labels (or then
in the middle of the block? what are y'all doing?) does not make sense to me.
The fact that return
in Swift means unambiguously "return from the enclosing function, whether the function is named or not (that is, a closure), wherever you're calling it from" is one of the key features and principles that I appreciate about the language. Using return
for anything other than that will be very, very confusing to me, and will basically destroy one key principle that allows to reduce complexity in Swift code, and increase clarity.
I'm still not seeing any strong argument against a new keyword here, other than "too many keywords", which seems arbitrary ("too many" compared to what?).
Fundamentally, why does that fact that expressions are not closures have to imply that the keyword for returning a result to the outer context has to be different?
Yes, they are different things, but return would mean the same thing in both places, so why insist on having different keywords?
By that logic, shouldnât we have different return keywords in closures, functions, local functions, variable getters etc? Those are also fundamentally different things.