If I'm understanding you correctly, I think any usage of if besides with simple booleans would serve as the killer example, wouldn't it?
let x = if let a = b { someValue(a) } else { someOtherValue()
let y = if case .foo (let a) = b { someValue(a) } else { someOtherValue() }
To be clear I'm just providing some examples of what I think can't be done with the ternary operator that could be done under this proposal, but at the moment I actually don't know if I like the above code or if I want the ability to write it in Swift.
This pitch seems to be suggesting that returns (both implicit and explicit) should be defined as a return from scope instead of return from function. I already think using “return” to return from a closure is confusing since it may look like a return from function. Having return work differently depending on if the switch returns a value or not is very confusing. Maybe “yield” or “break” should return from switch statements to make it obvious there isn’t a return from function. Rust uses the break statement to return a value.
If this pitch allowed multi-line in if/switch statements what would a return in a nested guard statement do? Would it return from the switch or the function?
EDIT: I think the explicit use of return may have been a typo in the pitch. I realize this is just step one toward returning from control statements, but hopefully the final proposal will at least explore future directions for breaking out of the switch, extending to loops (like Rust), and supporting multi-line.
I don’t see how this follows from the pitch. In fact, the pitch goes out of its way to illustrate how the immediately-evaluated closure workaround can cause this exact problem with return:
The example you show is from the part of the pitch where they cover existing solutions, not the proposed solution. The proposed solution has an example where return sets the let expression instead of returning from the function.
I'd say in that case it is returning a value out of the current function / closure – it can be of a different type either and unrelated to the result of "switch" expression – in which case "y" won't be initialised (e.g. you won't be able using it in "defer")
All these will be valid as written, without the need of parens, right?
let a = switch x { case 1:true; case 2:false; default:false }
let b = !switch x { case 1:true; case 2:false; default:false }
let c = -switch x { case 1:100; case 2:200; default:0 }
let d = switch x { case 1:100; case 2:200; default:0 } * 1234
let e = if let y = z { 100 } else { 200 }
let f = if x { CGRect.null } else { CGRect.zero }.size.width
if x { funcA } else { funcB } (123) // function call
if x { objA } else { objB } [123] // subscript
func foo(x: Int = if let y = z { 100 } else { 200 }) // parameter default value
struct S {
let x = if let y = z { 100 } else { 200 }
}
That is indeed a typo. Though the proposal is suggesting that you can returninstead of providing an expression for a branch, in which case the expression is never produced because the function exits. But that code wasn't supposed to be demonstrating that.
Not as proposed here. This pitch only introduces the 3 cases – returning, declaring and initializing from if and switch statements. It doesn't propose allowing them to be sub-expressions of other expressions. So -switch or if p { f } else { g } (123) would not be valid (yet... this is clearly a reasonable future direction, though still debatable whether it's the right direction).
If the new control flow statement were to allow us to return a value from the current scope then, when used with an implicit return value of Void, it could be also be used for control flow that's currently not possible, which could maybe be useful?:
func demo (input: String) throws -> Int {
if someCondition {
if someOtherCondition {
guard let someValue = generateValue(from: input) else { yield }
return useValueToMakeReturnValue(someValue)
}
return generateDefaultReturnValueBecauseWeYieldedAbove()
} else {
throw someKindOfError()
}
}
Is it perhaps less obscure upon considering the existing confusion that return-in-closures can cause, which is even called out specifically in the pitch? I’ve long wished in both ObjC and Swift that returning from closure were spelled differently, because it makes code that hoists common codepaths into closures harder to follow.
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.
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:
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.