Evolution idea: implicit return in single-line switch cases

It may not be the first time something of this nature has been suggested, but I wasn't able to find a previous thread or proposal similar.

Motivation

I often find myself using a switch case to either return a value from a computed property, or assign a value to a variable. This is a simplified example, but often they contain many cases.

// flavor 1
var someComputedProperty: Int {
    switch someVar {
    case .foo:
        return 1
    case .bar:
        return 2 + 4
    }
}

// flavor 2
var someNewValue: Int
switch someVar {
case .foo:
    someNewValue = 1
case .bar:
    someNewValue = 2 + 4
}

Proposed Language Enhancement

It would be really fantastic if there were some syntactic sugar to allow switch blocks with single-line cases that return values to have some shorthand. Variable assignment and implicit return could be a nice way to do this.

// someNewVar is inferred to be Int from the switch block context
let someNewVar = switch someVar {
case .foo: 1
case .bar: 2 + 4
}

As a return value:

// return value is inferred to be Int from the switch block context
return switch someVar {
case .foo: 1
case .bar: 2 + 4
}

Which in the scenario of a scope that already allows for implicit return (SE-0255), would simplify down to:

var someComputedProperty: Int {
    switch self {
    case .foo: 1
    case .bar: 2 + 4
    }
}

Epilogue

All cases of the switch block would have to return the same value type of course, and type inference could be used to keep syntax to a minimum.

The benefits of this scale with the number of cases in the switch block, which can often be numerous.

The proposed syntax is self-evident/unambiguous and I think anyone would understand its intent.

Would there be any caveats to an implementation like this? I feel that it would likely not have any source-breaking implications and would only be an additive feature to Swift lang.

11 Likes

This thread is, I think, a good touchpoint regarding this issue. Dave focused the thread on if/else expressions, but switch gets raised a few times in there and the two are tied together on the Commonly Rejected Changes list. (I link this thread not to shut discussion down here, just to provide context for the previous extensive discussion of this issue.)


My personal opinion: I like using if/switch expressions in other languages that have them, and limiting them to single-expression bodies feels to me like at least a plausible fit for Swift. I'm on board with the sentiment that some expressed in that thread that it probably makes sense to treat if and switch as a package deal in this regard.

I also think that it would be great to explore how the landscape has changed since that thread with regards to alternatives to if/switch expressions. For instance, now that we have type inference for multi-expression closures, the following now compiles without error:

func f(_ b: Bool) -> Int {
  let x = {
    if b {
      return 0
    } else {
      return 1
    }
  }()

  return x
}


f(true) // 0
f(false) // 1
2 Likes

I noticed a very similar request in that thread here: Control Flow Expressions - #13 by Cameron_Knight

One other suggestion there was to use a dictionary but then that introduces Optional semantics to what is often a finite case selection and is still uncomfortable in ad-hoc situations where you want to define the switch cases in context.

Yes please! Although I do agree switch and if/else should probably be handled by a single proposal.

You can actually try this out today, if you can manage to ignore an operator and two braces. Using this result builder and operator:

@resultBuilder
struct ImplicitReturnBuilder<T> {
    static func buildBlock(_ component: T) -> T { component }
    static func buildEither(first component: T) -> T { component }
    static func buildEither(second component: T) -> T { component }
}

prefix operator ^
prefix func ^<T>(@ImplicitReturnBuilder<T> _ proc: () -> T) -> T { proc() }

We can do this:

let x = 42
let d = ^{ switch x {
case ...0: "nothing"
case ..<30:
    if x.isMultiple(of: 2) {
        "smol even"
    } else {
        "smol"
    }
case _: "big"
} }

As you can see, this supports if/else in addition to switch, and as a side effect also allows let bindings.


One thing I do think is worth considering is whether it might be a better approach to have a different syntax that addresses the repetitive part on the left side of the colon as well: the "case".

Maybe using a different keyword (Rust uses "match") that only allows a single expression for each pattern, or maybe we could somehow make the "case" part optional in switch statements. I know this is only tangentially related, but the "case" is no doubt my least favourite part of Swift syntax :smiley:

Taking Rusts match and sprinkling it with a dash of colons, we could have maybe something like this:

let res = match x {
    ...0: "nothing"
    ..<30: 
        if x.isMultiple(of: 2) {
            "smol even"
        } else {
            "smol"
        }
    _: "big"
}

Which to me looks so much cleaner and clearer than an equivalent switch with all its "case"s and weird C-ism indentation convention.

3 Likes

It's a very frequently asked feature and I'm +1 on this.

I wouldn't mind considering switch separately without if, as we have a ternary operation making if expression less badly needed. Besides switch the other statement that can benefit from being expression is do, current:

var x: Int?
do {
    x = try foo()
} catch {
    ...
}

vs a cleaner:

let x = do {
    try foo()
} catch {
    ...
}
3 Likes

This is something I have wished for ever since implicit returns were introduced.

However, I see it as purely a generalization of the current rules for implicit returns, to allow branching using control flow statements, rather than just treating control flow blocks as expressions universally.

IOW I’m not a huge fan of things like using a switch on the right hand of an assignment expression (let result = switch value { … }), but I do wish I could use implicit returns in control flow as a generalization of the current implicit return rules. :grinning:

[Note: I most often find myself wanting this when switching over self on an enum.]

enum Pet {
    case dog, cat, bird, other(String)

    var description: String {
        switch self {
            case .dog: "Dog"
            case .cat: "Cat"
            case .bird: "Bird"
            case .other(let petName): petName
    }
}
7 Likes

Nicely put - those are exactly my wishes too - not control statements as expressions, but a rule like: infer returns on single line case bodies of functions that only contain one switch statement.

I have so many use cases of ‘enumeration mapping’ like your example. Like getting a title from an ‘Action’ enum, getting an image from the same or even actually mapping between two similar enums.

3 Likes

Not to derail the basic idea, but keep in mind that returning the case value is something quite different from treating the switch statement as producing an expression (but not returning it, unless it's the only statement in a function and so the "normal" return elision would apply).

The implicit return has several issues that need to be worked out:

  • Control flow (such as fallthough and sequential statements after the switch) becomes a bit obscure.

  • Should it always be a return, or can it sometimes be break or continue etc when the switch is itself nested within a control construct?

Expression-producing kinda avoids those problems, but it's something the community has never been able to come to a consensus on. In particular, for example, I foresee problems with type inference performance.

3 Likes

In Kotlin there is when expression: Conditions and loops | Kotlin
It behaves like switch in swift but is an expression.

The main inconveniences with switch as a statement are:

  1. need to repeatedly write return in functions and computed var

var description: String {
switch self {
case .foo: return "Foo"
case .bar: return "Bar"
case .fooz: return "Fooz"
case .baz(let baz): return baz
}
}

  1. need to create intermediate let / var and repeatedly bind value to it

let descr: String
switch enumValue {
case .foo: descr = "Foo"
case .bar: descr = "Bar"
case .fooz: descr = "Fooz"
case .baz(let baz): descr = baz
}

In both cases 'switch as expression', like 'when' in Kotlin, will make code less noisy:

// theoretical syntax

var description: String {
  switch (self) -> {
    case .foo: "Foo"
    case .bar: "Bar"
    case .fooz: "Fooz"
    case .baz(let baz): baz
  }
}

let descr: String = switch (self) -> {
  case .foo: "Foo"
  case .bar: "Bar"
  case .fooz: "Fooz"
  case .baz(let baz): baz
}

// or

var description: String {
  switch case(enumValue) {
    .foo: -> "Foo"
    .bar: -> "Bar"
    .fooz: -> "Fooz"
    .baz(let baz): -> baz
  }
}

let descr: String = switch case(enumValue) {
  .foo: -> "Foo"
  .bar: -> "Bar"
  .fooz: -> "Fooz"
  .baz(let baz): -> baz
}
5 Likes

Maybe it’s been touched on already, but this is already implemented in SwiftUI (in essence).

In a view builder it treats conditionals like if/else and switch as implicit return. All I’m really proposing is that it become a wider Swift language feature with a little syntax sugar.

It even infers the return type — in this example, both cases are Text. If one is Text and the other is some other View, the complier complains.

var body: some View {
    switch foo {
    case .one:
        Text(“One”)
    case .two:
        Text(“Two”)
    }
}

I understand this is part of builder semantics but it feels like it could naturally translate to a core language feature.

3 Likes

Looks like I’m getting my wish! :tada:

9 Likes