Pitch: Implicit and Optional Switch Statements

(Graham Preston) #1

Implicit and Optional Switch Statements

Introduction

I suggest adding implicit and optional switch statements, to fix the annoyances many have with the exhaustion detection for the current switch statements, which requires the use of the default statements in many unnecessary situations. This is because exhaustion is only detected with enumerations.

Motivation

The switch statement is a powerful way to control flow, despite this, it acts unpredictably, specifically with how and when it requires an explicit default statement during an attempt to become exhaustive. See the following example where we are using a switch statement to find out the sign of foo:

let foo: Int = 2

switch foo {
case 0: print("Zero")
case ...0: print("Negative")
case 0...: print("Positive")
} 

In this example, we get the following:

Error: Switch must be exhaustive

This switch statement is, in fact, exhaustive, but the compiler cannot detect this. There are many solutions to this, such as:

let foo: Int = 2

switch foo {
case 0: print("Zero")
case ...0: print("Negative")
default: print("Positive")
} 

But given more complex examples it can be ambiguous which case the default handles, and comments explaining can create clutter within the switch statement.

I have even seen some strange cases like:

let foo: Int = 2

switch foo {
case 0: print("Zero")
case ...0: print("Negative")
case 0...: fallthrough
default: print("Positive")
} 

While this solves the previous problems of ambiguity, it is non-standard and syntactically messy.

There are many similar examples of a switch statement not being identifiably exhaustive by the compiler.

switch Int.random(1...3) {
case 1: ...
case 2: ...
case 3: ...
} 

Error: Switch must be exhaustive

One solution to this would be to make it such that the compiler understands the switch statement's exhaustively for types other than enumerations. There are many more exhaustion detection issues, so finding a solution to all these various cases seems pedantic and pointless hence, why I am pitching this change.

Furthermore, there are many times when a default case is not necessary.

enum Foo { case A, B, C, D, E, F, G }

let foo: Foo = .A

switch foo {
case .A: ...
case .B: ...
case .C: ...
case .D: ...
default: break //Foo.E, Foo.F, Foo.G are not needed
}

Here is another example while extreme shows how this default is unnecessary:

let foo = true

switch foo {
case true: ...
case false: ...
default: fatalError()
}

switch statements are used for more than just pattern matching and checking against all value types of enumerations, they are also used for cleaner and easier to read code.

For example, if we have chaining else-if statements, that requires no final else statement, it would be cleaner to write it as a switch statement, but then a default: break is necessary.

if foo == condition1 {}
else if foo == condition2 {}
else if foo == condition3 {}
...
else if foo == condition9 {}

// This is better written as

switch foo {
case condition1: ...
case condition2: ...
...
case condition9: ...
default: break
}

There should a better, cleaner method to explicitly stating that a default should be optional or implicit rather than adding a adding a default: break or a default: fatalError()

Proposed Solution

Optional Switch Statements: switch?

Allows for skipping the required default statement

switch? foo {
case ...: break
case ...: break
} 

This is functionally equal to:

switch foo {
case ...: break
case ...: break
default: break
} 

Implicit Switch Statements: switch!

Allows for skipping the required default statement, but will fail if no case statement is run

switch! foo {
case ...: break
case ...: break
} 

This is functionally equal to:

switch foo {
case ...: break
case ...: break
default: fatalError()
} 

Detailed Design

This addition would create three different types of switch statements:

  • switch: The current switch statement, no functionality change.
  • switch?: An optional switch statement, which would break out of the switch statement if no case is run.
  • switch!: An implicit switch statement, which would call fatalError() if no case is run.

This design choice was made to match the various forms of optional/conditional and implicit/forced castings and keywords:

  • as, as?, as!
  • try, try?, try!
  • foo.method()?, foo.method()!

!: is used to force something, a runtime crash occurs if this fails
?: is used to attempt something and skip over it if it fails

Impact on Existing Code

This change is only additive and will not affect any current code.

1 Like
(TJ Usiyan) #2

Do you have other motivating examples for the features? My take on the example that you gave is that we should aim to support exhaustivity checking for ranges (which is difficult). That said, if you had more examples you might be able to avoid that general response.

I don't think that optional and implicit switches are a good idea, really, as you give up both the safety of exhaustivity checking and a single character opts out of diagnostics in potentially large swaths of code.

3 Likes
#3

I just want to point out that if you want to match the behaviour of other exclamation mark thingies in swift, you should use fatalError() or preconditionFailure(), because assertionFailure() doesn't do anything in optimised builds

4 Likes
(Graham Preston) #4

Thank you, my mistake. Fixed!

(Konrad `ktoso` Malawski) #5

I understand where you are coming from with the proposal but I'd be a bit more reluctant to make default cases optional/implicit. I think I'm speaking for "people with larger enums which may evolve over time" I guess.

Specifically, let's consider that we have some state machine / state encoded using an enum, and the exhaustivity checking allows me to handle all cases.

The issue I have with this change can be summed up as: "It would break the "top down" reading of switches."

Examples follow;


Order matters in switches, esp. if perhaps some cases have a where clause even etc, so reading a switch is always really a top-to-bottom thing. I read the cases and if I don't see a default I can safely assume that all cases are handled. This matters to me a lot in code review for example. If someone adds a case, I read top to bottom and know how it'll be handled. If someone changes the data type I'm switching over, I do the same.

Another example is adding a case to an enumeration; when reviewing how to now handle the new case, I read top to bottom an existing switch, and see if there's either an explicit default, or if all "don't care" cases are listed explicitly (case .nope, .nie, .nein: log.trace("ignoring \(value)") and I'm good.

Specifically: I do not need to "scroll up" to check what kind of switch I'm in -- I can ride on the guarantee that "I'm switching over an enum, there must be explicit handling or a default."

The switch! I'm also not so sure about, since e.g. in a server environment you may want to not just drop an empty fatalError() in the default clause, but at least attempt to provide a bit more information why this should have never happened, helping later diagnosis.

I'd be worried that introduction of this to the language would introduce more "worrying" and traps to switch statements than the convenience is worth it. For single liners you could also do the if case let ... = x {} syntax after all, which is a bit weird but achieves the goal of not needing to write the default branch if you only have one case to handle. One could argue "so use a linter which bans switch! / switch?" but I'm not sure adding the feature in the first place is going to help in so many cases).

1 Like
(Daniel Höpfl) #6

I like the idea, fits well with SE-0192 (Handling Future Enum Cases).

1 Like
(Chéyo Jiménez) #7

I don’t think it fits well. 192 is about being explicit about cases tha get added in the future.

I don’t understand why the below is not acceptable.

case _ : break
Or
case _: fatalError()