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

Fixing one would fix the other because they use the same mechanism under to hood.

I don't believe there are many issues with parsing ambiguities, as the case keyword allows us to know that we don't have a trailing closure, and ditto for the else keyword (which an if expression is required to have). There is an ambiguity for an empty switch with no cases, but that doesn't seem like a common thing to want to write in that position, and we'd continue to parse it as a trailing closure. In general, from an implementation perspective we should be able to allow if/switch expressions in arbitrary expression positions, it's just a question of ensuring that SILGen can correctly handle them in those arbitrary positions (I believe it should basically Just Work in most if not all cases, but we'd need to go through and verify it).

It's a syntactic use restriction, so enabling them in other positions is just a question of removing the diagnostic and ensuring SILGen can handle them okay.

Personally I would prefer if the rules were consistent across if and switches, e.g it would seem odd to me that:

enum E {
  case a, b, c
}

func foo(_ e: E) -> Int? {
  let x = if e == .a {
    0
  } else if e == .b {
    1
  } else {
    nil
  }
  return x
}

would not have the same type-checking behavior as:

func foo(_ e: E) -> Int? {
  let x = switch e {
  case .a:
    0
  case .b:
    1
  default:
    nil
  }
  return x
}

Syntactically, this would be fine to allow (indeed it would be fine to allow even without parentheses in most cases). But, as said in my reply to Becca above, we'd need to go through and ensure code generation handles it okay. if/switch expressions as proposed are novel in that they allow embedding arbitrary statements in an expression position (closures are emitted as separate functions instead).

2 Likes

I'd like to echo this, though I'd refine @avladimirov's example to:

let foo = if bar { 1 } // fails, incomplete expression returns Int
let foo: Int? = if bar { 1 } // produces .none

I expect the more common circumstances people might run into this is when conditionally mutating a variable:

var foo = 5
// …
foo = if bar { 42 } // replaces: if bar { foo = 42 }

This is admittedly starting to look Rubyish (foo = 42 if bar), but I kinda like it. Not enough to suggest that we add unless to the language keywords, but I think incomplete conditionals should probably be allowed when the variable has provably been initialized (or else can be inferred nil)

1 Like

Note that Ruby’s behavior differs from what you're describing:

# Ruby
foo = 5
bar = false

foo = 42 if bar    # Leaves foo unmodified, but...
(foo = 42) if bar  # ...that's only because Ruby parses the previous line like this.
foo = (42 if bar)  # This sets foo to nil.
foo = if bar       # This also sets foo to nil.
  42
end
foo = if bar then 42 end  # This also sets foo to nil.

At this point this seems very targeted to allowing elision of the else specifically when dealing with optionals. And personally, I don't think this is a good bit of sugar to add (but then, I'm a bit skeptical of the fact that we allow implicit nil-initialization of optional variables...).

It feels like this is a different proposal to what is appearing here, and in the form of a single if statement, does not seem worth doing. It's just a different way of ordering the expression, but (unlike with other examples in this thread) no benefits in terms of reducing ceremony – it's purely a reordering. And as such, not useful except as an aesthetic preference.

Now, if you combine it with multiple if else branches too, it starts to become more than just a simple reordering:

foo = 
  if bar { 42 } 
  else if baz { 99 }
  else if boz { 123 }
  // but no else here – don't perform any assignment in that case

then we are maybe talking about some ceremony reduction.

I'm pretty skeptical this is a good direction – it seems just that bit too subtle to notice (with a long chain) that there is a fall through. I would much rather it always be explicit. You can always write else { foo }. Or you can go back to the old way, and put the assignments inside the branches – nothing too bad about that.

(And of course this capability would also introduce a difference between declarations vs assignments)

I suspect the best way to decide if this is a good direction is to accept this proposal as-is (I would say that, of course :) and then gain community experience. If there is a strong feeling that, now that we have expressions, this "only assign a new value when these conditions are met" form now really feels like a gap that comes up often, it is easy to add with a follow-up proposal.

1 Like

Thanks for doing the analysis. I was actually teetering on the brink of agreeing maybe allowing return wasn't such a good idea and we could drop that part – and this post pulled me back to thinking it's a good idea.

Another reason is, there's a particular form in Swift that would benefit from return... failable initializers. Which are often of the form "either assign something to self, or return nil from this function", e.g.

    init?(rawValue: String) {
        self = switch rawValue {
        case "foo": 1
        case "bar": 2
        case "baz": 3
        default: return nil
        }
    }

It'd be a shame to require them to lose the benefit of this proposal.

11 Likes

That is indeed a solid example of where allowing return is good, since you have to return nil in that context! Thanks @Paul_Cantrell and @Ben_Cohen for making the case.

6 Likes

This basically sums up my feelings as well. I've enjoyed using if-as-expression in other languages, and I'd be happy to see it introduced to Swift, but I think it's important for this proposal to leave us in a place that we might be okay being forever. It's not clear to me thus far that we'd be able to come to successful consensus on whatever the next step might be, so IMO it's important to make sure we're okay with the resting place SE-0380 provides.

In particular, I'm not that pleased with the story for multi-statement support, as Paul discusses:

I agree with Paul that an expression enclosed in curly braces strongly suggests the ability to relatively easily expand the contents to multiple statements. I don't really see the response of "you can just wrap that branch in a closure" as a valid resolution, because that's exactly the type of workaround that this proposal aims to tackle!

As I mentioned in the pitch thread, I think the proposal in its current form really starts Swift down the road towards a "statement block evaluates to the final expression" rule supported by many other languages which support if-expressions. I'm not necessarily against that, but I'm also not necessarily for it and I wouldn't really want to see justification for that feature get 'trojan-horsed' into the language with SE-0380, at least not without deliberate consideration.

The proposal discusses this briefly in Future directions but I'm not sure that it sufficiently grapples with its own role in pushing us towards needing to address the issues it raises, and it doesn't fully convince me that we'd ever be able to come to a happy resolution as opposed to being stuck at the SE-0380 state of affairs in the long run.


I'd also more mildly desire arbitrary expression positions, but I think this proposal covers 90% of the cases where I'd want to use the feature, and the potential future extensions aren't as potentially problematic in my mind as for multi-statement branches. If I had to make one further allowance, it would be to additionally permit switch and if expressions to appear recursively as the single expression branch of an outer if/switch expression, e.g.:

return if conditionOne {
  if conditionTwo {
    0
  } else {
    1
  }
} else {
  2
}
6 Likes

This is covered by the proposal:

Within a branch, further if or switch expressions may be nested.

4 Likes

Oop, glossed over that line since it wasn't part of the top-line list of allowed locations for if/switch expressions. Thanks! In that case, no notes on this aspect :slightly_smiling_face:

I am positively surprised after reading through this proposal and catching up with the very thoughtful comments thus far. I think there's some very promising stuff here.

I am, however, unmoved by the following problem statement in the proposal text:

The lack of this feature puts Swift's claim to be a modern programming language under some strain. It is one of the few modern languages (Go being the other notable exception) not to support something along these lines.

I don't think it should be a goal, simply because we use the word "modern" on an about page, for Swift to be sort of an evergreen collection of the hottest idioms and fashions in programming language design. Indeed, I think we should overtly disclaim this sort of direction. Over "modernity," I'd think it much more essential to prioritize our longstanding proposal evaluation criterion that a feature has to fit well with the feel and direction of the language.

I'd argue that we should be rather ruthless in rejecting designs—even very fashionable, modern ones—that require contortions in order to retrofit into the language as we have it. And, even if it's possible to retrofit without too much trouble, a new feature should enable us in some way to write better, more correct code—not just different code because the existing ways are so last century.


With the benefit of time and the example of other languages, if we were inventing a new language today, I'd agree that the "modern" solution available in Rust and other languages with if and switch expressions is superior to what Swift has today, and I would emulate the former.

I would also agree if someone were to argue that ?: is a bit of an odd duckling in Swift as the only ternary operator in the language, with the otherwise inconsistent meaning of ?, and all the type inference shenanigans already discussed at length.

However, given that we have an established language which has, and will probably forever have, the ?: operator, and given that nothing in this proposal makes possible what is currently impossible to express in the language, I think we have to evaluate this proposal rather differently.


Were if expressions to be a proposal on their own, my overall impression of that proposal would be that the feature doesn't hold its own weight and actually fits very poorly with the feel and direction of Swift:

  • I agree with @beccadax 's feedback (and @Paul_Cantrell, among others) on the oddness of type inference working differently for ?: and for if expressions
  • I'm also troubled, like them, that the latter cannot subsume the former in terms of where it's allowed to be used (and, indeed, in result builders, will never be able to subsume the former)
  • The issue with expressions wrapped in braces suggesting the possibility of multiple statements, just raised by @Jumhyn, is also a distinct drawback of if expressions

I'd also point out that I'm not aware of any languages that have both a ternary operator and if expressions. Overall, I'd say that it doesn't feel like a very satisfactory "resting place" for language evolution.

On the other hand, if switch expressions were their own standalone proposal, I'd be rather delighted with what's been proposed:

  • The limitations on where it's allowed to be used make a lot of sense and have precedent in the current limitations on non-parenthesized trailing closure syntax; the wonkiness of a multi-line expression (which switch expressions undoubtedly will be) being followed by for-loop braces strongly recalls the issues that motivate trailing closure syntax limitations
  • Not having type inference stretch across multiple branches makes very good sense here, and there's no existing language feature here with which it would could be compared which gives different results
  • Keeping each case to a single expression (unless returning or throwing, the justifications for allowing being discussed just above and very convincingly) also makes sense and avoids some of the difficulties seen in Java (which, as I understand, has had to innovate a new yield syntax and distinguish "arrow" cases from "colon" cases)—meanwhile, the lack of any surrounding braces for each case avoids a "closure-like" appearance that may tempt folks to want to write multiple statements

Overall, then, my opinion on this proposal is that the switch expression portion may be a very good addition to the current language, with good fit and an improvement in expressivity. Meanwhile, if expressions do not seem as well motivated and are faced with a slew of unsolved cons, and may be better left out. If that makes Swift a little less "modern," then so be it.


I have used some of the other languages that have switch expressions or their analog (although the bulk of my experience with other languages that have this feature date from when they in turn didn't have the feature), and I have used Python extensively with sufficient love and appreciation for its take on the ternary operator. I have studied this proposal in depth as well as undertaken an examination of how other languages (Rust, C#, Java, Kotlin) implement similar language constructs.

16 Likes

Ruby does, FWIW. That said, its excessive love of having multiple syntactic options don't make it a clear precedent-setter for Swift.

(Java does not have if expressions, but does have both switch expressions and the ? : ternary operator.)

You have a point: it certainly it is notable that Rust, Kotlin, and Scala all opted to support multi-statement if expressions and also opted not to include ? :. I'm not convinced that implies that Swift should avoid supporting this feature, however. While this is technically true:

…in the sense that Swift is already Turing-complete, I've found the expressive power of multi-statement conditional expressions to be well worth the syntactic weight in languages that offer them.

It's like string interpolation: it looks excessive and unnecessary until you've lived with it for a while, and then you wonder how you lived without it.

4 Likes

IMHO nobody actually evaluated ternary from type-checking performance perspective unlike in this case where splitting expression into multiple doesn't make any sense.

2 Likes

This seems a bit spurious to me. The fact is you can write multiple statements in a switch case in Swift. So when writing expressions, you would assume you still could, same as you can in the brace of an if. I don't think it's likely that many developers, when finding their multi-statement case in a switch expression doesn't compile, will think "huh, I guess there are no braces in cases so that makes sense".

Of course, from my perspective, the solution here is it's fine: if you write a multi-statement case in a switch expression you'll get an error that sorry you can't do that. And you'll get that same error with a multi-statement if.

I see your "braces imply multiple statements will work, so we shouldn't do if" and raise you a "switch expressions will imply if expressions will work, so we should do them too" :)

In particular, if expressions will give users the ability to unwrap multiple optional values using a familiar syntax, and then combine them into a new value, while also providing a default in the else. Can you write this things today? Yes, but not easily – this is something that is pretty hard to do well, involving good knowledge of how to combine various optional sugar like ?? and map together to get the result you want. if let expressions unlock a new concise yet expressive way to do this that I suspect most Swift developers will find very easy to grasp and benefit from immensely.

4 Likes

Basically happy to see this kind of flexibility considered in Swift, welcome feature.

However a point I did not understand much is a future direction: Multi-statement branches, because without this feature I don't really see the big benefit or differences, as you already can express them with closures just like presented in the proposal:

let bullet = {
    if isRoot && (count == 0 || !willExpand) { return "" }
    else if count == 0 { return "- " }
    else if maxDepth <= 0 { return "▹ " }
    else { return "▿ " }
}()

I think I'm missing a point how all of this feature is designed, however can't help stop think like, why not force use of return just like it does in getters:

let decoded =
  if isFastUTF8 {
    Log("Taking the fast path")
    return withFastUTF8 { _decodeScalar($0, startingAt: i) }
  } else
    Log("Running error-correcting slow-path")
    return foreignErrorCorrectedScalar(
      startingAt: String.Index(_encodedOffset: i))
  }

edit: I mean just like computed variables or functions with return values:

var value: Int { 0 } // OK
var value: Int { print(); return 0 } // Even with multiple statements, with explicit return, it's OK
3 Likes

Such returns would be confusable with returns from the function itself.

Somewhat related: To avoid confusion about where return returns from I'd use two different types of brackets: one for functions / blocks and another for statements:

func foo() [
    if condition {
        return // from function
    }
    for item in items {
        ...
        return // from function
    }
    foo.bar.baz [ // aha, square brackets! so this is a closure (block), not a statement.
        ...
        return // from closure
        if condition { return } // from closure
    ]
    DipatchQueue.main.async [ // ditto
        return // from closure
    ]
]

It doesn't look too odd or radical, although I didn't find precedents in other languages (other than the opposite: using { } for array constants in C). This would make different things slightly more (visually) different and could help during debugging (how many times I was stepping though a line ".... {" expecting to be on the next line but ending up elsewhere because that was a closure.)

1 Like

So I think I'm a +0.9 on this. The general idea of if and switch statements as expressions does seem good, though I'm not a big fan of them being purely inline with nothing really signifying the difference. For example, I don't believe

let bullet =
    if isRoot && (count == 0 || !willExpand) { "" }
    else if count == 0 { "- " }
    else if maxDepth <= 0 { "▹ " }
    else { "▿ " }

is inherently better than

let bullet = {
    if isRoot && (count == 0 || !willExpand) { return "" }
    else if count == 0 { return "- " }
    else if maxDepth <= 0 { return "▹ " }
    else { return "▿ " }
}()

The former does remove the return but I feel like the closure syntax gives a LOT of clarity. I understand that you can still use the closure syntax, but I'd still find it nice for Swift to enforce better readability over the small writability gain with some sort of "expression" syntax. I think what Tera suggests above is certainly something worth considering. I'm not sure if [] is the best thing to use, but I feel like we could do with something

2 Likes

I read through this with growing joy at the thought that one day a switch expression could return a value, without the actual, ahem, return.
I am a big fan of this. If dropping the return ceremony in functions was considered a good thing, then dropping them in switch statements should be too. It's consistent with the practice in functions, and it just makes sense.
So many times I have wanted to do this and been annoyed at the lack of it.

For "if" statements, I'm less clear on the benefit, and have never found the lack of it a problem or even an inconvenience. I haven't read other reviewer comments, and am only reporting my own experience here.

2 Likes

This strikes me as being a 'solution' only from the standpoint of the compiler—for the user who attempts to write the multi-statement if branch, the fact that the compiler emits an error is itself the problem, not the solution!

The workarounds that exist today for the lack of if expressions at least have the benefit that they evolve relatively gracefully. If I've written:

let result: Int
if condition {
  result = 3
} else {
  result = 4
}

then it is straightforward to update this code to add some intermediate computation:

let result: Int
if condition {
  let intermediateResult = someOtherComputation()
  result = intermediateResult * 3
} else {
  result = 4
}

But with if expressions as-proposed, and as the proposal notes, the available options as a branch evolves aren't so satisfying.

It would be one thing if this proposal took the position that multi-statement if expression branches are actively undesirable, and that any more complex branches ought to, say, be refactored into a separate function so that they could be called as a single expression. In this case I'd say we should definitely choose a different syntax to make it clear that these expressions are fairly different from their statement counterparts.

But AFAICT this proposal remains fairly agnostic on the merit of supporting multi-statement branches while simultaneously choosing a syntax that, IMO, actively suggests that they should be supported.

IMO it is more surprising for a brace-enclosed region to support only a single expression than it is for if and switch to support different feature sets. Notably, switch out-of-the-box supports exhaustive pattern matching, which this proposal relies on. We of course have the else escape hatch for if expressions as a practical matter, but IMO it's not unreasonable that if and switch support different things. I think PHP is like this—there's a match expression, but no equivalent if expression (other than the ternary operator).

That said, I'm very sympathetic to the ergonomic/familiarity points you note about if vs. switch.

8 Likes

I would be surprised by that behavior. My understanding is that if bar { 42 } is an expression of type Int?, which you couldn't assign to foo because foo isn't optional.

3 Likes