This can also come up with @discardableResult functions, e.g something like this would become illegal:
func foo(_ arr: [Int]) {
var setA = Set<Int>()
var setB = Set<Int>()
let fn = { elt in
if .random() {
print("inserted into set A")
setA.insert(elt)
} else {
print("inserted into set B")
setB.insert(elt)
}
}
arr.forEach(fn)
}
Since fn would become (Int) -> (inserted: Bool, memberAfterInsert: Int). I suspect these cases would be fairly uncommon though since it's much more common to write these kinds of closures inline in the function call (and yes the use of forEach here is very contrived). Additionally the types of the resulting expressions have to line up, otherwise we default to Void. I don't believe we saw any cases of this when we introduced if/switch expressions with single expression branches, it was only around the optional chaining case. It's possible these cases may be more common with multi-statement branches though.
I apologize if that has been mentioned upthread.
I would assume it's save to say that return, break, continue and even throw are already dedicated to other contexts. I also seen many mentions of do upthread as well.
Personally I would love do {} to gain the functionality and evaluate into a value (e.g. let value = do { … }). With all these different and deeply nested expression scopes things can become very complex on their evaluation paths. Therefore we might already need to take into the account a future extension of such expressions. I'm talking about 'labels'. continue and break are perfect examples where sometimes labels are necessary to exit a certain parent scope earlier.
For that matter I will use the shorter use keyword for the evaluation part of expressions.
Here's a small but illustrative snippet.
let value: SomeType = label: if condition {
for element in collection {
// another if expression
let x = if element == … {
use element
} else {
label use element.value // evaluate top expression earlier
}
…
}
use fallbackValue
} else {
print("failure")
use defaultValue
}
expression_label + use does read very well to me and it also aligns great with break and continue for similar reasons. Unlike the other two keywords we would start with the label first. Unless we will use evaluate as a keyword which could be written as:
evaluate label: value
Long story short. The consideration of a label extension provides some insights into the soundness of the proposed new keyword (repeat each is a good example for that as well).
This seems inevitable after SE-0380, the origins of the language simply didn't account for those expressions and this issue is the result. However if the language can come out better on the other side it will all be worth it.
Another keyword seems like the way to go, meaning something like "return" but exiting just the expression, not the function. I like "use", as it seems to lend itself well to the semantics of defining a result but then letting execution continue to statements that follow, and in fact I'd like to see this new keyword be generalized for use in any function or closure context as well. A generalized "use" keyword would open up a new compact way to express what we already do with clunky local result variables. (admittedly only slightly clunky, but still)
But I think what the big win would be allowing us to avoiding using "return" statements in closures and sub-functions within a function. These have somewhat of a cognitive load when reading Swift source, along the lines of: "okay this return only exits an internal context, ah this return really exits the function".
Overall, the direction here seems unintuitive. It looks like a half solution that was conceived of trying to work within some other constraint. The language doesn’t appear to have any meaningful need for this additional complexity.
Personally, I’d rather have braces added as scoping for cases. Or control expressions in non-return, non-assignment positions.
In general I’m in favor of having multi-statement switch expressions. But as the similarly named movie character would say “not like this”.
How often do we use labels? I'd rather see them removed from the language altogether than to build a new feature around how well it would sound with the presence of labels when read out as an English phrase.
You are adding something to the compiler, so technically you might be right stating an augmented complexity. The way I like think about it is that the proposal actually reduces the complexity of written code. And introducing e.g. the new keyword “use” aligns perfectly with the current use of the return statement, practically no additional thinking to learn.
I write quite complex algorithms from time to time, and labels are really great in some cases. I am not so sure about labels for this new use case, but one would have to fiddle around with some code to see if this would be a good thing (and maybe ask the compiler guys if this smart idea gives them headaches).
I meant adding complexity for the humans using the thing. And while it aligns with return, it is undeniably not return, and therefore another completely separate thing. Another thing to learn, to teach, to parse while reviewing and understanding code. It adds complexity.
Labels are niche, so they are rare. However when you need them they are really great for the task they are usually used for. I think if we had a keyword like for example use, labels to make sense as another niche extension to function similar like continue and break already do.
Isn't that difference exactly as subtle as it is today, when looking at local functions and closures, vs the main context? I find it very straightforward, but introducing a special keyword for the exact same role but in the "third context" of a switch expression on the other hand confuses me a lot.
I think the major difference is that if/switch statements are only sometimes expressions, meaning that return would have two possible meanings inside of them, and for such a large difference in meaning I think it’s a bit too subtle to look for the visual cue of a let x = at the top of the statement, especially given that if we allow multi-statement branches then that visual cue will often be off-screen relative to a given return statement that you’re trying to interpret.
I understand your point, but I would disagree. To me, an expression switch is a completely different thing from a normal switch, it will always behave differently. So you need to keep in mind what kind of switch it is, regardless. Just like you need to keep in mind if you happen to be in a local function/closure or not. So given that you already need to know that, I don't think return would add to the confusion at all.
To the extent that your argument holds water, I would say it's an argument against the entire expression switch concept, or really local scopes in general.
IMHO yes, it is, and personally I'd prefer a different kind of brackets to distinguish functions and control blocks more easily:
func foo(condition: Bool, items: [Int]) -> Int [
if condition {
print("1")
return 42 // returns from a function (the nearest outer [] block)
}
items.map [ item in
print("2")
return 42 // returns from a closure (the nearest outer [] block)
]
return items.count
]
A contrived example stressing the current behaviour gotchas:
func iƒ(_ condition: Bool, execute: () -> Void) {
if condition { execute() }
}
func foo() {
let value = 42
iƒ (value == 42) {
print("1")
return
}
print("2")
if (value == 42) {
print("3")
return
}
print("4")
}
foo()
// outputs: 1, 2, 3, but not 4
Note how similar is the use site, yet how different is the resulting control flow.
But you could apply the same logic to e.g. type-checking. "I know this stored property holds a double, so why should I have to write special code [: Double] for it - why can't I just always use the same, single keyword var?"
Why not have the compiler check your work?
If you never make a mistake, then you'll never notice the compiler's work here and it has no cost - you'll use return vs then correctly and there'll be no issues.
But that one time, perhaps, where you do accidentally write return inside a switch expression, wouldn't it be nice if the compiler caught your error?
Why wouldn't using return instead of then allow the compiler to check your errors here? If we say you can never return through an expression, only from it, then all the compiler needs to do is check whether you're returning the proper type. Additional checking gets you nothing. It's no different than using break in different contexts, or confusing continue vs. break in loops. All reasonable mistakes to make that the compiler can't really help you with since it can't know your intention.
On a slightly related note, if proponents of then believe that users shouldn't need to read the expression context (if they did, using return wouldn't be confusing), then how are you teaching people when to use then in the first place? It seems to me like most people will simply try using return, see the compiler complain about it (perhaps even suggesting then in the fixit), and then switch to using then? Why force them to go through the exercise? Just so we can say, "see, wasn't that better?" It's basically a worse version of @escaping at that point.
I don't think type-checking is reliable here. The 'return' type of the enclosing context (function, closure, etc) might be the same as the expression's intended result type. That's possibly more likely than not.
func example(arg: Bool) -> Int {
let tmp = if arg {
print("True")
return 1
} else {
return 0
}
return tmp * 2
}
A new reader cannot intuitively and unambiguously know what that's supposed to do. Depending on their background they might assume the behaviour of if statements and therefore expect the function to return either 1 or 0, or they might assume the behaviour of an immediately executed closure and therefore expect the function to return either 2 or 0.
An experienced reader at the very least has to scan back up to the top of the code block to try to find if there's an assignment to an lvalue - or some similar sign - somewhere that might imply the block is ultimately an expression, not a statement. Relatively trivial for the above example but I can imagine it being quite tricky in some more complicated real-world applications.
If instead the then keyword is the non-interchangeable 'equivalent' to return in an expression context, as opposed to a statement context, then it's immediately and unambiguously clear which context is in play.
Granted you then still have to scan backwards to figure out the top and extent of that context, since you could be e.g. inside an immediately executed enclosure inside an expression, but it makes it harder to get lost in code of sensible complexity.
Your example is immediately invalidated here. This is a strawman; there is no such thing a single type of "new reader". Randomly assigning programming knowledge to users and then designing a language towards those examples is pointless. It doesn't work like that. Especially when you say things like:
It certainly is not! You've just created a user which has random programming knowledge, so what possible expression could be immediately unambiguous to them? None!
This is exactly my point: the work needed to understand nested thens is exactly the same as nested returns in this context, so why not use the logic actual Swift users have built up around nested closures or functions to these expressions? Really the only difference is the lack of wrapping curly braces. (As an aside, motivating a change to the language with "there's too many curly braces" and then coming back later and motivating another change with "there aren't enough curly braces here for users to tell the difference" is, at best, self defeating.)