Skip to content

Instantly share code, notes, and snippets.

@erica
Last active September 21, 2017 10:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erica/04835de3d3d9121ef7308dd9b093158a to your computer and use it in GitHub Desktop.
Save erica/04835de3d3d9121ef7308dd9b093158a to your computer and use it in GitHub Desktop.

Adding continue to Switch Statements

Introduction

This proposal completes the switch statement's control flow transfer suite by introducing continue. This change adds functionality that many developers expect (but do not get) from fallthrough.

Motivation

At this time, Swift does not allow pattern matching and execution to continue within the scope of the current switch statement. A case can fallthrough to execute the next clause but it cannot resume pattern matching. The fallthrough executes the next case clause regardless of whether the value matches its pattern or not.

switch value {
    case pattern: 
        // ... handle pattern
        fallthrough
    case another pattern:
        // ... execute code for both "pattern" and "another pattern"
}

Fallthrough does not enable you to choose between disjoint outcomes, like those shown in the following example:

switch value {
    case pattern:
        // ... code to handle this case
        **do something here to continue pattern matching**
    case pattern where condition A:
        // ... additional code where A holds
    case pattern where condition B:
        // ... additional code where B holds
    case pattern where condition C:
        // ... additional code where C holds
    ...:
    default: break
}

Introducing continue means "resume pattern matching after executing this case clause". It offers more nuanced control than currently exists within switch. Continuing has several advantages:

  • It minimizes redundant code by permitting multiple pattern matches.
  • It differentiates cases where code applies to one condition and not others without requiring branching within a single case clause.
  • It uses an existing control flow transfer keyword that's philosophically in-step with how the keyword is used in other parts of the language.
  • It works around scenarios where control should transfer but additional variables need to be bound.

FizzBuzz

To provide an example that differentiates fallthrough behavior from continue, consider "Fizz Buzz". You cannot implement this algorithm in Swift with a switch statement using fallthrough. "Fizz" and "Buzz" print for every number value, regardless of whether that number is divisible by 3 or 5.

switch value
{
case _:
    print("\(value) ", terminator: "")
    fallthrough
case _ where value % 3 == 0:
    print("Fizz", terminator: "") // prints for every number
    fallthrough
case _ where value % 5 == 0:
    print("Buzz", terminator: "") // prints for every number
    fallthrough
default:
    print("")
}

With continue, FizzBuzz works as expected, only executing matching cases while allowing control flow to continue down each successive case.

switch value
{
case _:
    print("\(value) ", terminator: "")
    continue
case _ where value % 3 == 0:
    print("Fizz", terminator: "") // prints if divisible by 3
    continue
case _ where value % 5 == 0:
    print("Buzz", terminator: "") // prints if divisible by 5
    continue
default:
    print("")
}

Fallthrough Limitations

In some cases, the differentiation between fallthrough and continue is less clear-cut, as in the following example. This code raises an error because fallthrough cannot transfer control to a case label that declares variables, whereas continue easily could:

func myFunction(point: CGPoint) {
    switch (point.x, point.y) {
    case (let x, _) where x > 10:
        // ... Handle x 
        fallthrough // error 
    case (_, let y) where y > 10:
        // ... Handle y
    case (let x, let y) where x <= 10 && y <= 10:
        print("the point is too close from the border")
    }
}

Using continue resolves this limitation.

Fallthrough and Naive Confusion

In Swift, fallthrough does not mean: "execute this case and then continue pattern matching", which is what many naive users expect. Given the following code where x is 5, they anticipate the function to print "5" and then "anything else". This is wrong. Swift prints "5" and then "6". Introducing continue would allow the desired "continue pattern matching" behavior.

func test(x : Int) {
    switch x {
    case 5:
        print("5")
        fallthrough
    case 6:
        print("6")
    default:
        print("anything else")
    }
}

Those coming from C-like languages might have the insight to expect (wrongly, it should be noted) "5", then "6", then "anything else", which is what you'd get with the following flawed C-ish code, where case statements are missing break.

int x = 5;
switch (x) {
    case 5: NSLog(@"5"); // no break;
    case 6: NSLog(@"6"); // no break;
    default: NSLog(@"anything else");
}

Swift's fallthrough statement means "continue by executing the code defined in the next case clause". It is best suited for executing sieves where the most restrictive conditions execute specialized code and then execute code for less restrictive conditions, and so on:

case simple where even more subconditions hold: ... do complex things ...; fallthrough
case simple where subconditions hold: ... do other things ...; fallthrough
case simple: ... do base things ...

Because of its limited utility, Swift Evolution discussed removing fallthrough on-list in early December. At that time, the list arrived at a consensus that fallthrough offers sufficient utility to retain the feature in the language. fallthrough has at least one solid use-case, which is demonstrated in this example.

Detailed Design

While switch statements allow users to break execution, return or throw from scope, they do not allow control flow to continue to additional pattern matches. The only option available uses fallthrough. In the current design, switch statements support the following subset of control flow transfer:

control-transfer-statement → break-statement
control-transfer-statement → fallthrough-statement
control-transfer-statement → return-statement
control-transfer-statement → throw-statement

Notably missing is "continue", which this proposal would adopt.

control-transfer-statement → continue-statement

The definition of continue in a switch statement would mean "after executing the statements in a matching case clause, continue pattern matching the remaining cases until a match or default is found."

  • Like break, continue is specific to the switch statement. You must use labels to break or continue with respect to a surrounding loop.
  • Both continue and fallthrough allow execution to continue to successive cases, however their intent is distinct. fallthrough moves forward without testing, automatically matching the next case. continue moves forward but will not execute until it finds a matching case. Because of this, I believe fallthrough and continue states should be mutually exclusive. When a potential execution trace through a case clause allows both strategies to trigger (i.e. not if x { continue} else { fallthrough}), the compiler should emit an error.

Mutating Variables

Rob Mayoff asks: "To be clear, under your proposal, what does the following program print?"

var x = 1
switch x {
case 1:
    print("one")
    x = 2
    continue
case 2:
    print("two")
default:
    break
}

My interpretation would be that the an expression passed to a switch statement is evaluated once and that value used for pattern matching:

// switch-statement → switch expression {switch-cases*}
// e.g.
switch 2 + 3 {
case 5: print("Five!")
default: break
}

I'd expect this to print "one" and not "one", "two". I defer to the core team and will happily update this section should they disagree with this interpretation.

Impact on Existing Code

A switch statement within a loop may be broken by the introduction of continue semantics. To fix, the loop must be labeled and the continue must use that label to differentiate between switch continuation and loop continuation. This exactly matches the annotation of break with respect to loops when used in switch statements.

Alternatives Considered

Not adopting this idea

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment