[Accepted with modifications] SE-0380: `if` and `switch` expressions

The review of SE-0380: if and switch expressions ran from December 7th to December 30th. Feedback was generally positive; many reviewers noted that the usability of if expressions in other languages is missed in Swift, and selection statements producing a single value in each branch is a common pattern across their Swift code. Most of the discussion focused on whether the narrow scope of the proposal is an acceptable resting place for if and switch expressions.

Overall, the Language Workgroup believes that SE-0380 is a valuable usability improvement that reduces unnecessary ceremony in a very common pattern across Swift programs. The Language Workgroup has decided to accept the proposal with the modification to ban all non-error-handling control flow in if and switch expressions.

I have enumerated the specific details of the proposal that were the focus of constructive discussion below, along with the Language Workgroup’s decision and justification on each point.

Type inference across if and switch expression branches

SE-0380 proposes isolated type inference across branches of if and switch expressions, meaning types in one branch cannot influence type inference in another branch. Reviewers noted that this type inference behavior is different from the ternary operator, which may be unexpected.

The Language Workgroup believes that isolated type inference for if/switch expression branches is a reasonable resting place for SE-0380. Enabling full bidirectional type inference for if and switch expressions would proliferate an existing type checking performance pain point in Swift — notably, a problem that does not exist with the statement form of if and switch, which may make it more difficult to transition from a statement to an expression.

The Language Workgroup also acknowledges the value of bidirectional type inference, and encourages exploring bidirectional type inference for results of if/switch expressions, opaque result types, and multi-statement closures in a later proposal, perhaps in a more limited form. This exploration can be informed by real-world adoption of these features, and existing code can be used as a baseline for type checking performance impact.

Allowing return out of if and switch expressions

SE-0380 proposes allowing return statements in if and switch expressions to return out of the enclosing function. Many reviewers noted that this behavior is unexpected, and they anticipate control flow out of expressions to become a source of bugs.

The Language Workgroup agrees that new control flow out of expressions is unexpected and error-prone. Swift currently only allows control flow out of expressions through thrown errors, which must be explicitly marked with try as an indication of the control flow to the programmer. Allowing other control flow out of expressions would undermine this principle. The control flow impact of nested return statements would become more difficult to reason about if we extend SE-0380 to support multiple-statement branches in the future. The use-cases for this functionality presented in the review thread were also fairly niche. Given the weak motivation and the potential problems introduced, the Language Workgroup accepts SE-0380 without this functionality.

Multiple-statement if and switch expression branches

Much of the review discussion centered around the single-expression limitation of if and switch expression branches. Reviewers noted that programmers will expect the ability to evolve a single-expression if or switch into one with multiple statements in its branches, similar to evolving a single-expression closure into one with multiple statements in its body.

The Language Workgroup believes that multiple-statement branches for if and switch expressions are a compelling direction for Swift. Evolving single-expression bodies into multiple statements is a very common workflow; a common example is adding a logging statement inside such a body. Requiring workarounds using an immediately-executed closure or reverting back to the statement form of if or switch is a suboptimal programming experience.

There is significant design space to explore for enabling multiple statement if and switch expressions, and further discussion on whether implicit returns should be extended to function bodies with multiple statements. The Language Workgroup believes SE-0380 is valuable enough on its own — similar to the value of single-expression closure type inference despite the longstanding limitation on multi-statement closures that was lifted in SE-0326 — and has decided to accept the single-expression limitation while the design space for multiple statement branches is explored.

Extending other statements to have expression forms

SE-0380 only supports expression forms of if and switch statements; other statements are not included. Reviewers noted that other statements, such as do statements, would also be valuable as expressions. The Language Workgroup believes that introducing expression forms of other statements would be useful in future proposals, and narrowing SE-0380 to selection statements is an appropriate scope. do expressions are also much more expressive with multiple-statement support, so exploration of the multi-statement design space will likely inform future generalization to other statements.


As always, thank you to everyone who participated in the pitch and proposal review. Your contributions help make Swift a better language.

Holly Borla
Review Manager

34 Likes

Thanks, Holly and core team.

There was also a lot of discussion about allowing conditional expressions in more expression positions. (The proposal limits them to return and assignment statements.) Was there any discussion of this, and will there be any future work on it?

2 Likes

Could somebody add a couple of examples of what is accepted and what is not accepted?

4 Likes

Yes, the Language Workgroup discussed if and switch expressions in arbitrary subexpressions. We ultimately agreed with the proposal authors' decision to subset arbitrary subexpressions out of the proposal, as we anticipate the current proposal covers the majority of cases where this feature will reduce ceremony. The proposal authors also clarified during the review that if and switch expressions can be nested, which assuaged some reviewers' concerns about the limitation.

The Language Workgroup is primarily concerned with whether a given proposal closes off compelling future directions. Generalizing this feature to arbitrary subexpressions certainly can happen in a future proposal, but the Language Workgroup does not decide whether a future direction in a given proposal will definitively be pursued.

Writing return statements inside if or switch expressions is not accepted:

func test() -> Int {
  let value = if true { 
    0 
  } else { 
    return 1  // not allowed
  }

  return value + 10
}
7 Likes

Overall I am glad this is accepted, makes Swift a bit more concise.


Note that we didn't explore yet optimising syntax in regards to dropping braces in the common case of one statement expressions: brace-less syntax is less noisy and allows for easier chaining (in combination with the typical right associativity of ternary operation):

C:

int x = a ? 1 : b ? 2 : c ? 3 : 4

int x = a ? 1 : (b ? 2 : (c ? 3 : 4))

int x = a ? 1 :
            b ? 2 :
                c ? 3 : 4

Python:

 x = 1 if a else 2 if b else 3 if c else 4

Swift (accepted):

let x = if a { 1 } else { if b { 2 } else { if c { 3 } else { 4 }}}

let x = if a { 1 } else {
    if b { 2 } else {
        if c { 3 } else { 4 }
    }
}

// Better versions (thanks to Jumhyn):

let x = if a { 1 } else if b { 2 } else if c { 3 } else { 4 }

let x = if a { 1 }
    else if b { 2 }
        else if c { 3 }
            else { 4 }

let x = if a { 
  1
} else if b {
  2
} else if c { 
  3 
} else {
  4
}

Swift (potential future alternative):

let x = if a then 1 else if b then 2 else if c then 3 else 4

let x = if a then 1 else
               if b then 2 else
                   if c then 3 else 4

Interestingly, however peculiar the Python ternary operation looks in isolation, the whole expression reads very easy and natural when several subexpressions are chained together.

1 Like

Presumably, the idiomatic Swift solution under this proposal would be something like:

let x = if a { 
  1
} else if b {
  2
} else if c { 
  3 
} else {
  4
}

(modulo preferred line break/indent styles)

10 Likes

Great work! I think this arrived at a good place. I'm happy to see the ability to return from expressions removed from the original proposal. That really bothered my functional programming sensibilities.

2 Likes

The ternary operator is still there if you want maximal terseness. Because of that, I would view trying to make the if/else syntax as compact as possible as a mistake.

9 Likes

I'd actually prefer something like this for a terse form. I've never liked how the ternary operator doesn't work well with optional-chaining. SE-0380 has a similar issue, but I think it isn't an issue where it will be commonly used.

@inlinable func when<T>(_ cond: Bool, then result: () -> T) -> T? { 
    if cond { return result() } else { return nil } 
}

let x = when(Bool.random()) { 1 } ?? 2
let y = when(Bool.random()) { 1 } ?? when(Bool.random()) { 2 } ?? 3

This would be a cool ternary alternative, but can't be implemented without a language feature due to order-of-operations of boolean expressions. Inspired by the pattern match operator.

let x = Bool.random() ?= 1 ?? Bool.random() ?= 2 ?? 3

Can you provide an example of this causing problems?

Sure. It is primarily hard to read in nested expressions. This is a pretty nonsense example, but it shows how ternary is difficult with heavy option chaining. It gets even worse when adding in other higher order functions like map and filter.

This doesn't require a new language feature, but it might be nice to see something like this in the standard library.

func maybeGetVal() -> Int? { ... }
extension Int { func process() -> Int? { ... } }

// ternary version requires escaping out with explicit nil to support optional chaining
let x = (Bool.random() ? maybeGetVal() : nil)?.process() ?? 2
// better terse option 
let x = when(Bool.random()) { maybeGetVal() }?.process() ?? 2

// ternary version has horrible nesting behavior for multiple cases
let x = Bool.random() ? 1 : (Bool.random() ? 2 : 3)
// better terse option works nice with multiple cases 
let x = when(Bool.random()) { 1 } ?? when(Bool.random()) { 2 } ?? 3

EDIT: Fix bad example.

Note, that ternary operator associates right to left – in all languages where it exists except for PHP – so parens are typically not needed.

1 Like

Ternary is only good for expressions, it won't handle:

condition ? a = b : c = d

In the statement like below:

if condition { a = b } else { c = d }

I don't see why braces are important (and many languages think so optimising the single expression syntax), although if to drop braces it would read somewhat nicer with "then":

if condition then a = b else c = d

aside: it was with great sadness that I discovered (via another post on here recently) that this does actually compile and works as you'd (in a sense) expect:

condition ? (a = b) : (c = d)
12 Likes

And this works because all the compound assignment operators are plain operator function calls, which are expressions, and it would be weird for just regular assignment to not be an expression.

5 Likes

Why does it work only when parenthesised?

I didn’t check, but I think it’s just unintended effects from having a = b ? c : d implicitly parenthesize the ternary, which is obviously what you’d want. So perhaps the other version is being parsed as (a ? b = c : d) = e, which is obviously nonsense. Or maybe it’s something else.

4 Likes

Indeed, assignment precedence is lower than ternary. This works:

condition ? a = 1 : (b = 1)

This seems not great, as rationales go. There are plenty of places where "probably a mistake not genuinely legit code someone might write, so deserves special handling" trumps "falls out naturally from the rules". let x = a = b at least generates a warning.

2 Likes

Why would condition ? (a = b) : (c = d) be "probably a mistake"?

2 Likes