[Pitch] if and switch expressions

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.

5 Likes

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.

1 Like

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 think I found what you’re referring to:

@Ben_Cohen, is this a typo? Should the default branch really include a return keyword?

3 Likes

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 return instead 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.

7 Likes

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).

2 Likes

One could also imagine something like this:

let foo = do {
    try bar()
} catch SomeError {
    someValue
} catch {
    anotherValue
}
1 Like

This is a very obscure reason to introduce a new control-flow statement.

If we need a solution for this, I think we either need to accept final-expression production:

var x = if value == 0 {
    print("zero")
    true
  } else {
    false
  }

or just accept that we're going to be forcing the closure workaround when people need multiple statements in one of these blocks:

var x = if value == 0 {
    { print("zero")
      true }()
  } else {
    false
  }
2 Likes

One thing that I think makes me uneasy about the final-expression thing is that this code is has been valid and common Swift for a few years:

VStack {
    if someCondition {
        producesSomeView()
        producesAnotherView()
    } else {
        aDifferentView()
    }
}

and it is to be read in a fundamentally different way than this very similar looking newly-proposed code:

var backgroundColor: Color {
    if someCondition {
        vaguelyNamedFunctionThatMightReturnAValue()
        otherFunctionDoesWhoKnowsWhat()
        theOneAndOnlyReturnValue()
    } else {
        theOtherReturnValue()
    }
}
9 Likes

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()
    }
}

I would expect this to be equally as valid as

func foo(x: Int = (z == nil) ? 200 : 100)

I was going to say "that is, not at all", but to my surprise this is compiles given a global variable z.

Without a global variable z, it says it was unable to produce a diagnostic, even though it did:

1 Like

The "current scope" for this yield is actually the guard, not the innermost if, which I think pretty clearly demonstrates why this is not a good idea.

4 Likes

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.

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.

7 Likes

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.

11 Likes