[Pitch] if and switch expressions

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

How does this rule apply to leading-dot syntax (e.g. for enums) when the type of the expression is already known (e.g. when it's the return value)? Consider the following use case:

func statusColor(forCode code: Int) -> Color {
	switch code {
		case 200..<300: return .green
		case 300..<400: return .yellow
		case 400..<600: return .red
		default: return .primary
	}
}

With this limitation, I'm not sure the expressions would be inferred to be of type Color, allowing me to leave away the type. Instead I'd be left with this:

func statusColor(forCode code: Int) -> Color {
	switch code {
		case 200..<300: Color.green
		case 300..<400: Color.yellow
		case 400..<600: Color.red
		default: Color.primary
	}
}

…which I'm not sure is an improvement. Ideally, I'd be able to write e.g. case 200..<300: .green This use case covers around a third of case x: return y occurrences in my code, so it's pretty important to me.

2 Likes

The type context of Color would still be provided, so you could use case 200..<300: return .green

1 Like

Oh, that's perfect! I'm all for this pitch then: I think the limitations make sense, at least for now.

Hi everyone – thanks for the feedback so far. There's now a proposal PR and implementation.

The PR incorporates feedback from this thread:

  • do, break/continue, multi-statement branches as future directions
  • explicit that binding in if and switch is supported
  • clarifies just if builder expressions, not assignments, are unchanged in result builders
  • alternatives considered covers a -> variant
13 Likes

I don't really see why it would be, any more than a throw in an if statement today might. You need try on function calls that throw to make sure it's clear your function might exit. But open-coded throws inside a function are clear enough.

as that snuck through and is legal under the proposal but had a surprising side effect that he didn't intend

I don't think a compiler can stop you writing incorrect code. There is a big difference between things a compiler can catch – type errors, unreachable code, flagging calls that would exit the function abruptly because they throw – and then there are logic errors (like getting your if and else branches the wrong way around) that are just unavoidably on the programmer. The extra return typo falls into the latter bucket IMO.

2 Likes

That's a fair counterpoint. I don't love that "let x = try ..." isn't a reliable scan-marker for my eye but you're right.

I half-heartedly ponder that the syntax could allow a try here, but I dislike even more the idea that there isn't exactly one "correct" way to write this.

2 Likes

I dislike the invisible returns of the current proposal quite a bit. Seems like a clear bug generator when someone comes to work on the code later and writes code below the switch statement thinking it'll execute.

So this, or some other way that makes it clear these are not simple switch statements but returns/exits from the enclosing function seem very important to me.

5 Likes

Nice work, Ben and Hamish.

Is there any chance of getting this much-upvoted suggestion into the proposal — if not as part of what’s actually proposed for this round, then at least as a future direction?

Lack of full generality was one of the most common objections above, and doubtless will be so in formal review as well. People seem to like the solution above. It addresses all of the concerns in the Future Directions | Full Expressions section of the proposal:

// ❌ wat
for b in [true] where switch b { case true: true case false: false } {}

// ✅ meaningless, but easy enough for humans to parse visually
for b in [true] where (switch b { case true: true case false: false }) {}
var body: some View {
    VStack {
        if showButton {
            Button("Click me!", action: clicked)
        } else {
            Text("No button")
        }
        .someProperty  // ❌ parsing ambiguity: static property, or property of View?
    }
}

var body: some View {
    VStack {
        (
            if showButton {
                Button("Click me!", action: clicked)
            } else {
                Text("No button")
            }
        ).someProperty // ✅ no parsing ambiguity
    }
}

…and I don’t think anybody identified a downside.

I don’t want the idea to get lost. It may well be too much to consider in this proposal, but at a minimum, it’s a useful answer to those future direction concerns.

9 Likes

Thanks everyone for your feedback so far! I've kicked this off for review over here:

Please provide further feedback over on the review thread. Thank you!

Holly Borla
Review Manager

2 Likes

Sorry, I didn't get to incorporate it yet but I will put this approach to full expressions into the future directions section. It is an interesting question whether, if/when we go that direction, parentheses should be mandatory vs just ensure sure compatibility.

2 Likes