[Pitch] if and switch expressions

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

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 :+1:t2: Edit: +0 :man_shrugging:t3:

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.

1 Like

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

3 Likes

Ah, I did not :see_no_evil: 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.

9 Likes