SE-0380: `if` and `switch` expressions

What do you think the problem is in this example? Looks straightforward: if bool parameter is true then 43 is returned, otherwise 21 is returned.

I don't have any strong opposition on this. It will be useful in some cases like this. It can enable guard like usage of if expression.

let x_root = if x >= 0 { sqrt(x) } else { return }

If I'm forced to say, I can imagine cases where this expression can be misunderstood as closure like behavior (return is just returning value for outside of the if expression).

1 Like

Yep, missed that, thank you. Exploring the further direction section:

Often enthusiasm for guard leads to requests for guard to have parity with if. Returning a value from a guard's else is very common, and could potentially be sugared as

guard hasNativeStorage else { nil }

Then, logically, if there is no return value:

guard hasNativeStorage else { return }

this could be further reduced down to:

guard hasNativeStorage else {}

which leads to a question why to have "else {}" noise at all:

guard hasNativeStorage

which you can use for "default return". Unless you want something else, in which case you can still express it:

guard hasNativeStorage else {
    print("something wrong")
    return // perhaps we still want this return here, not 100% sure
}
```
2 Likes

I see. I tend to think this was a mistake to choose the same syntax for closures and if/etc body blocks:

func foo() -> Bool {
    if foo {
        return true // returns from "foo"
    }
    ...
}

func foo() -> Bool {
    items.filter { item in
        return true // does not return from "foo"
    }
    ...
}
2 Likes

To some extent, this proposal helps with this situation, as it makes return inside a function body vastly less likely. Even with multi-statement function bodies, since you will be able to write return if { ... } else { ... } rather than if { return ... } else { return ... }

This may change swiftUI in some interesting and helpful ways

var body: some View {
  if expression {
    Image(…)
  } else {
    Text(…)
  }.modifier(modifierToApplyToBoth)
}

This construct would not previously be possible.

Will it be possible now?

UPDATE: I read more thoroughly, and this case is not in the implementated cases in the 'detail' section of the doc.

How will the new expressions interact with ViewBuilders? Result builders already have support for constructs like if and switch!

Result builders also more aggressively expect expressions instead of statements in their blocks, might there be confusion here between the result builder if builder and the result builder's desire to find a view expression?

How would the expression version resolve the type difference on the two paths?

To be clear, from the proposal authors perspective, deprecating the ternary operator is very much a non-goal. While it's appreciated that adding this feature goes in that direction, and it's reasonable to desire that direction, it isn't the motivation. Speaking personally, even if/when if statements can be full-blown expressions, I would still use the ternary operator inside expressions because it is terse and (to me) readable. I would much rather see "a" + (p ? "b" : "c") than "a" + if p { "b" } else { "c" }.

edit: thinking about it more, even better than both is if p { a+b } else { a+c } which this proposal facilitates :)

At this point, I don't think it would ever be reasonable to deprecate the ternary operator even under a language variant, given the maturity of the language and the churn that would cause, even if support were universal.

While reinforcing what @xedin said about not making a bad situation worse w.r.t. expressions checking performance, I also want to state that I believe bidirectional inference is a highly dubious feature for Swift full stop, as it leads to very counter-intuitive behavior.

To take one example, is this expression (equivalent to find(where:) lazy?

let x = mySequence.lazy.filter(g).first

Answer: no, it is eager. Sequences do not have a first property (because they aren't mutli-pass). So the expression checker finds another way: it backtracks on filter and instead of running the lazy one, it runs the eager one that produces an Array, because that has a first property.

There are many cases where this has caused real production bugs, and personally, I'm pretty convinced that bidirectional inference like this, instead of something like left-to-right type narrowing, was a misadventure even if it weren't for the unfortunate superlinear algorithmic complexity it imposes on the type checker.

That said, despite this, much like with the ternary operator (but in this case with real regret) we probably are stuck with it. But we shouldn't make the problem worse. And it would be much worse if we extended it to type checking branches of if and switch. Should one branch produce an array, and another produce a lazy map, it is far better to generate an error surfacing to the user that their laziness is being defeated, than to silently drop it and do the map eagerly. This will give the user a chance to think again about how to write the code to preserve that laziness if it's important.

And, as the proposal notes, this approach is consistent with two other recently-accepted language proposals – opaque result types and multi-statement closure inference.

Allowing specific carve-outs for nil and empty collection literals is a really interesting area to explore, but if there were a risk that it leads to this proposal having to be deferred while its explored, I would strongly encourage putting it, like several other natural next steps, into the Future Directions bucket.

I agree, and I'd love to consider doing this in Swift 6 as part of the path to allowing postfix member expressions on if statements. Again, it would be really unfortunate if combining that with this proposal meant denying this otherwise very beneficial feature for a long time or even worse restricting it to Swift 6 mode.

The current implementation allows for this too (I'll clarify in the proposal) and compiles with the experimental toolchain.

But to get picky I'd resist describing this as a cast, and discourage writing it this way for exactly this reason. What this is doing is providing type context, so is equivalent to writing let x: Double?. I encounter a lot of developers who think too much in terms of "casting" and get very upset when they then try and e.g. write someInt as Double when 1 as Double works just fine. Explaining that this is disambiguating the type instead of casting often helps them overcome this.

6 Likes

I'd rather see bidirectional inference gone than ternary operator. At first glance bidirectional inference sounds clever, but it brings more problems than solves IMHO.

1 Like

I think this was briefly discussed in the pitch thread, but I don’t think these types of expressions should work with implicit returns.

func takeClosure<T>(_: (T) -> T) {}

func getId() -> Int {}

takeClosure {
  if .random() {
    getId()
  } else {
    getId()
  }
}

In this case, the closure wouldn’t get an explicit type annotation. This means that to someone not familiar with the code, this closure could expect a specific return type, like Void, or a generic one, like <T> above. Further, without quick access to the definition of getId, which is quite common on platforms like Github, it would be unclear whether this function performs a side effect or/and returns a value. All these issues assume a comprehensive understanding of language features. For someone not entirely familiar with Swift, someone unfamiliar with the code would probably be surprised that a value is indeed returned.

2 Likes

I don't really follow how this is different from takeClosure { getId() } and from my perspective it would be a significant negative to exclude this with no clear motivation other than "if makes the existing issue worse" which, speaking personally, I don't buy.

As noted in the proposal, Swift is really the outlier here. Many modern programming languages treat if as an expression. So the lack of familiarity with Swift doesn't seem like a big factor.

6 Likes

Would this syntax be supported by this proposal?

var body: some View {
    switch selection {
    case .a:
        Text("A")
    case .b:
        Text("B")
    }
    .onAppear {
        doSomething()
    }
}
1 Like

No, switch/if expressions are handled by the result builder transform in a special way and this proposal doesn't change that, see The expression is not part of a result builder expression section for more details.

I’m a big +1 on this general direction. I like this proposal a lot as a first step. I don’t think it’s sufficient as a destination for this design direction (and realize the authors may not necessarily be presenting it as such). I’m skeptical of whether it’s sufficient as a stepping stone.


There’s some skepticism about this whole feature direction. My experience points in favor of it. The review prompts ask:

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I’ve used conditionals as expressions extensively in Ruby, Elm, Scheme, SML, and Python. The middle three are of course functional languages, where there is naturally no other kind of conditional. Ruby was the first time I’d encountered if-as-expression in an imperative language, and it was bizarre to me at first. (Some reactions in this review thread could have been me circa 2006.) That feeling passed quickly! I’ve come to appreciate the feature a great deal, and miss it in Swift.


My concerns about this proposal all stem from the ways in which it stops short of full generality. In particular, I’m concerned about:

  1. Lack of multi-statement support
  2. Lack of support for use in arbitrary expressions
  3. Lack of the same type inference the ternary expression provides

Taken together, I’m concerned about whether this proposal leaves the language in a good intermediate state even if we do plan on future build-out in this space. Some specific thoughts:

Lack of multi-statement support

(Detailed analysis hidden because it's long; key point: Is there *anywhere* else in Swift where a single statement enclosed in braces cannot transform into multiple statements?)

Consider the vexation of someone who has the following perfectly reasonable code:

let message = if let thinger = fetchThinger() {
    "Found \(thinger.name)"
} else {
    "ERROR: no thinger available"
}

…and wants to log the error. They have to do one of the two following awkward (and I think non-obvious) things:

// Option 1
let message: String
if let thinger = fetchThinger() {
    message = "Found \(thinger.name)"
} else {
    log("Thinger fetch failed")
    message = "ERROR: no thinger available"
}
// Option 2
let message = if let thinger = fetchThinger() {
    "Found \(thinger.name)"
} else {
    {
        log("Thinger fetch failed")
        return "ERROR: no thinger available"
    }()
}

Is there anywhere in Swift where a single statement enclosed in braces cannot transform into multiple statements? I don't think so…?

The precedent we've set throughout the language is that any single statement within braces can become many statements, with the addition of a keyword iff the single statement was an expression whose result matters. This holds for closures, properties, and functions. It should hold here too.

It's clear to me that return is the wrong keyword for if-/case-expressions, but for the example above, by the language's own precedents, a solution with the following general shape ought to be possible (using result as a placeholder keyword):

let message = if let thinger = fetchThinger() {
    "Found \(thinger.name)"
} else {
    log("Thinger fetch failed")
    result "ERROR: no thinger available"
}

I'm concerned that this proposal as it stands makes a false promise to language users: what looks like a flexible approach is in fact an underpowered ternary, a dead end that breaks precedent and ends up requiring syntactic backtracking that can't help but feel just incredibly frustrating to a language user.

Maybe this is a good enough stepping stone…but part of me thinks it would be better to just steer people to Option 1 above in the first place until if expressions can pull their weight.

Lack of support for use in arbitrary expressions

(Detailed analysis hidden because it's long)

If I see this code:

let message = "Fetching thinger..."
displayStatus(message)

…my instinct is to consider refactoring away the intermediate variable:

displayStatus("Fetching thinger...")

For this code, shouldn't the same principle apply?

let message = if let thinger = fetchThinger() {
    "Found \(thinger.name)"
} else {
    "ERROR: no thinger available"
}
displayStatus(message)

We ought to be able to transform it in the same way, but the proposal disallows it:

displayStatus(
    if let thinger = fetchThinger() {
        "Found \(thinger.name)"
    } else {
        "ERROR: no thinger available"
    }
)

Again, the language has made a false promise. Normal, basic syntactic rules mysteriously don't apply. ** developer frustration intensifies **

I'm sympathetic to the proposal's concern over extreme cases here. That concern is well-founded.

I proposed allowing if/switch expressions in arbitrary expression position when immediately enclosed in parentheses (further examples here). Could we make that our first stepping stone? It seems safe enough, and does not present the same mysterious barrier as the current proposal.

Future proposals might be able to allow dropping those parens in more situations, for example as @beccadax suggested here:

(…but then presumably still allow them if enclosed in parens.)

Requiring more parentheses now and maybe making some elidable in the future seems far preferable to making common usage patterns illegal now because we might be able to make them parens-optional in the future.

Lack of the same type inference the ternary expression provides

I agree with Becca's remarks on type inference in her post above.

I'm always sympathetic to attempts to mitigate the horrors of multi-directional type unification, but I simply cannot get my head into a place where I'm comfortable with single-statement if expressions with a boolean condition (i.e. not if let) being anything other than completely equivalent to ternary expressions.

Could multi-statement if branches, and perhaps switch vs if, be the bridge where type inference becomes less robust as proposed here?


Again, I like where this proposal is going. And I am tempted to vote +1 as it stands: it hits the most common cases, and doesn't propose anything I'd imagine we'd have to retract in a source-breaking way. (Edit: That last part might not be true.) However, the way it stops short of generality makes me uncomfortable; I suspect its lack of generality is going to cause a lot of frustration, and make developers hate the whole idea when they would have appreciated it in a more fully built-out form. Opinions tend to calcify around those kinds of strong initial reactions.

16 Likes

I don't think we should allow return statements in these branches. This is confusing to read, is inconsistent with other value-producing expressions, and as far as I can see isn't motivated in the proposal or by existing Swift coding practices.

I don't have the same kind of problem with allowing throw statements, probably since value-producing try statements would work as expected.

8 Likes

Better not to overload the meaning of the return.

How about yield instead?

func foo(_ bool: Bool) -> Int {
    let value = if bool { 
        42
    } else {
        yield 21
    }
    return value + 1
}

To clarify, are you suggesting that if expressions with multiple-statement branches should have more limited type inference than single-expression branches? This seems to go against your earlier arguments that single-expressions should easily be evolvable into multiple statements:

Changing type inference when you evolve an if expression with single-expression branches into multiple-statement branches seems like that would be another source of frustration. We certainly found that to be the case for single-expression closures versus multi-statement closures - having to annotate the return type of multi-statement closures was a huge pain point until SE-0326 was implemented.

Could you please elaborate on why you believe changing type inference behavior between single-expression and multiple-statement branches would not lead to the same sorts of frustration/confusion? Or please correct me if I've misunderstood your suggestion!

Will if and switch expressions be allowed for property initializers?

struct T {
  var x: Int = Bool.random() ? 1 : 0
  var y: Int = if Bool.random() { 1 } else { 0 }
  var z: Int = switch Bool.random() { case true: 1 case false: 0 }
}

Swift 5.7.1 allows property x (with the ternary expression).

Yes — though not advocating so much as wondering out loud. I’m casting about for some policy that preserves parity with ternaries without creating a type inference performance explosion.

Yes, that certainly is a flaw (likely a fatal flaw) in my suggestion!


Your mention of SE-0326 might be useful guidance. @jrose mentioned a hypothetical choose function upstream; let’s spell it out:

func choose<Result>(
    _ condition: Bool,
    _ trueBranch: () -> Result,
    else falseBranch: () -> Result
) -> Result {
    return condition ? trueBranch() : falseBranch()
}

choose(x < y) {
    print("true branch")
} else: {   // (Swift lets us get away with this! Heh)
    print("false branch")
}

SE-0326 is released in Swift 5.7.1, right? Experimenting with what I think (?) is SE-0326 behavior gives fascinating results:

// ✅ compiles
choose(.random()) { 0 } else: { 1.0 }

// ✅ compiles
choose(.random()) {
    return 0
} else: {
    return 1.0
}

// ❌ error: cannot convert return expression of type 'Double' to return type 'Int'
choose(.random()) {
    print()
    return 0
} else: {
    print()
    return 1.0
}

// ✅ compiles
print(
    choose(.random()) {
        print()
        return 0
    } else: {
        print()
        return 1.0
    }
)

Would achieving parity with whatever's happening here be acceptable for multi-statement if expressions?

Edit to add: While the 0 / 1.0 example above does show parity between choose and the ternary expression, the other example from the proposal at hand does not:

let x = .random() ? nil : 2.0                   // ✅ compiles
let x = choose(.random()) { nil } else: { 2.0 } // ❌ error: 'nil' is not compatible with closure result type 'Double'

…so ¯\_(ツ)_/¯

3 Likes

What does this code mean? In any case I think this usage of yiled will make it difficult to co-use if expression in _read and _modify.

1 Like

IMO, yield is also the wrong keyword to use in case the language ever wants to support coroutines. However, I would support exactly this approach using some other keyword.

I suggested result as a placeholder up above, which has its own obvious problems:

…but I do think a resolution is lurking in this design space. And we could (I think?) use a word like result that isn't currently a reserved word, since the context in which this word would exist is not currently syntactically valid.

Although…hmm, accepting this proposal with only single-statement if expressions would negate what I just wrote: result or whatever keyword we ultimately want would appear as an identifier inside legal single-statement if expressions, making the multi-statement migration a breaking change. Yikes. Perhaps another strike against accepting the single-statement flavor as a stepping stone.

2 Likes