Resume Pattern Matching in Switch Statements

I want to see how the community feels about the introduction of resume in switch statements, as this addition would greatly increase the power and flexibility of switch statements.

Note: This topic has already been briefly addressed by this pitch a while ago.

Grammar of a resume Statement:

resume-statementresume label-name (opt)


Problem

I was recently writing an expression simplification program with an incredibly lengthy switch statement. I encountered a situation wherein I was in an already matched case and realized that, in the remaining cases, there were more simplification cases that could possibly be matched and applied to the value. To accomplish this, I considered placing the entire switch statement into a loop, but the cases above the aforementioned matched case would have incorrectly handle the value if I just reiterated through the entire switch statement. What I wanted to do, was only continue trying to match the value with the succeeding cases, thus correctly matching and applying the proper simplifications to the value.

To get around these limitations of switch statements in their current state in Swift, I want to propose the introduction of the resume keyword in switch statements. This will allows for a much greater degree of flexibility and control with switch statements and prevent situations like nested switch statements, embedding switch statements in a loop. I think this change has the potential to significantly increase the power of our switch statements.


Proposed Functionality

The resume keyword, when used inside a switch statement, should resume pattern matching as if it hasn't yet been matched. I also think that one should be able to label a case as they would a loop or a do block, to tell Swift where to start pattern matching at. Just as when used for loop to skip to the next element, it should skip to the next case and try to match it.

This adds functionality different from that of the fallthrough keyword for many reasons:

  • It resumes matching rather than just falling through into the next case
  • It allows you to specify the case that you would like to resume pattern matching at
  • It allows you to bind new variables as well

I think that mutation of the values being switched while inside the switch statement should be allowed, and all conditions be evaluated on the current value of whatever values are being switched at the time the condition is checked.

With the proposed functionality, the use of resume followed by a labeled case that is above that of the caller, would allow for recursion.


Example 1:

let n = 35

switch n {
case 20...: 
    print("n is greater than or equal to 20")
    resume

case 10...:
    print("n is greater than or equal to 10")
    resume

case let a where a.isMultiple(of: 3):
    print("n is a multiple of 3")

case let a where a.isMultiple(of: 5):
    print("n is a multiple of 5")
     
default: 
    break
}
// Prints:
// n is greater than or equal to 20
// n is greater than or equal to 10
// n is a multiple of 5

Example 2 (with labeled case):

let n = 64.0

switch n {
case 100...: 
    print("n is greater than or equal to 100")
    resume squareCheck

case 50...80:
    print("n is between 50 and 80")
    resume squareCheck

case ..<100:
    print("n is a value which I quite frankly, do not care about")

squareCheck: case let a where sqrt(a) == floor(sqrt(a)):
    print("n is also a perfect square")
    resume

case let a where cbrt(a) == floor(cbrt(a)):
    print("n is also a perfect cube")
}
// Prints:
// n is between 50 and 80
// n is also a perfect square
// n is also a perfect cube

Example 3

In this example, I show some of the multiplication simplifications that I perform on an Expression. The use of resume in the following example allows the matching of multiple different select simplifications.

enum Expression {
    indirect case add(Expression, Expression)
    indirect case subtract(Expression, Expression)
    indirect case multiply(Expression, Expression)
    indirect case divide(Expression, Expression)
    indirect case power(base: Expression, exponent: Expression)
    indirect case log(base: Expression, Expression)
    case n(Int)

    /// A Boolean value indicating whether the expression is a number.
    var isNumber: Bool {
        if case .n = self {
            return true
        }
        return return
    }

    /// A Boolean value indicating whether the expression is negative.
    var isNegative: Bool {
        if case let .n(x) = self, x < 0 {
            return true
        } else if case .subtract(.n(0), _) = self {
            return true
        }
        return false
    }
}

extension Expression: Equatable {
    static func ==(lhs: Expression, rhs: Expression) -> Bool {
        switch (lhs, rhs) {
        case let (.n(a), .n(b)) where a == b:
            return true
        // Account for commutativity of addition and multiplication
        case let (.add(a, b),      .add(c, d))      where (a == c && b == d) || (a == do && b == c),
             let (.subtract(a, b), .subtract(c, d)) where a == c && b == d,
             let (.multiply(a, b), .multiply(c, d)) where (a == c && b == d) || (a == do && b == c),
             let (.divide(a, b),   .divide(c, d))   where a == c && b == d,
             let (.power(a, b),    .power(c, d))    where a == c && b == d,
             let (.log(a, b),      .log(c, d))      where a == c && b == d:
            return true

        default:
            return false

        }
    }
}

extension Expression {
    static prefix func -(expression: Expression) -> Expression {
        if case let .n(x) = self {
            return .n(-x)
        } else if case let .subtract(.n(0), x) = self {
            return x
        }
        return .subtract(.n(0), self)
    }
}

/// Returns the greatest common denominator of `m` and `n`.
func gcd(_ m: Int, _ n: Int) -> Int {
    var a = 0
    var b = max(m, n)
    var r = min(m, n)

    while r != 0 {
        a = b
        b = r
        r = a % b
    }
    return b
}

extension Expression {
    mutating func simplify() {
        switch self {
        case .n:
            return

        // Simplifications for addition...

        // Simplifications for subtraction...

        // Simplifications for multiplication
        case var .multiply(lhs, rhs):

            // Recursively simplify operands
            lhs.simplify()
            rhs.simplify()

            // Set value of expression composed of simplified operands to self
            self = .multiply(lhs, rhs)
 
            // Apply the applicable simplifications to self
            switch self {

            // 0 * x = 0
            case .multiply(.n(0), _), .multiply(_, .n(0)):
                self = .n(0)

            // 1 * x = x
            case let .multiply(.n(1), x), let .multiply(x, .n(1)):
                self = x

            // -1 * x = -x
            case let .multiply(x, .n(-1)), let .multiply(.n(-1), x):
                self = -x
                return

            // Move numeric terms to the left of the expression for collecting like terms and factorization
            case let .multiply(x, .n(y)) where !x.isNumber:
                self = .multiply(.n(y), x)
                // Try to match other multiplication simplifications
                resume

            // a * (b * c) = ab * c
            case let .multiply(.n(a), .multiply(.n(b), c)):
                self = .multiply(.n(a * b), c)
                // Try to match other multiplication simplifications
                resume

            // x * (y / x) = y
            case let .multiply(a, .divide(b, c)) where a == c:
                self = b


            // x * (1 / y) = x / y
            case let .multiply(x, .divide(.n(1), y)), let .multiply(.divide(.n(1), y), x):
                self = .divide(x, y)
                simplify()

            // Let x rep. gcd(a, c)
            // a * (b / c) = (a / x) * (b / (c / x))
            case let .multiply(.n(a), .divide(b, .n(c))):
                let x = gcd(a, c)
                self = .multiply(.n(a / x), .divide(b, .n(c / x)))
                resume singleFractionReduction

            // Let x rep. gcd(a, d)
            // (a / b) * (c / d) = ((a / x) / b) * (c / (d / x))
            case let .multiply(.divide(.n(a), b), .divide(c, .n(d))):
                let x = gcd(a, d)
                self = .multiply(.divide(.n(a / x), b), .divide(c, .n(d / x)))
                resume

            // Let x rep. gcd(b, c)
            // (a / b) * (c / d) = (a / (b / x)) * ((c / x) / d)
            case let .multiply(.divide(a, .n(b)), .divide(.n(c), d))
                let x = gcd(b, c)
                self = .multiply(.divide(a, .n(b / x)), .divide(.n(c / x), d))
                resume

            // Combine into fraction
            // (a / b) * (c / d) = ac / bd
            case let .multiply(.divide(a, b), .divide(c, d)):
                self = .divide(.multiply(a, c), .multiply(b, d))
                simplify()

            // a * (b / c) = ab / c
            singleFractionReduction:
            case let .multiply(a, .divide(b, c)), let .multiply(.divide(b, c), a):
                self = .divide(.multiply(a, b), c)
                simplify()

            // a * b = ab
            case let .multiply(.n(a), .n(b)):
                self = .n(a * b)

            // Other multiplication simplifications in conjunction with powers and logs...

            // Multiplication simplifications completed
            default:
                break

            }

        // Simplifications for division...

        // Simplifications for powers...

        // Simplifications for logs...

        }
    }
}

Using Expression.simplify():

In the expression simplification method, whenever an operator is encountered, both its left and right operands are themselves simplified before moving forward. This allows the simplifications to be applied to the expression recursively.

Lets say we are given the expression:

var expression: Expression = .multiply(.divide(.n(3), .n(16)), .n(40))

// Simplify expression
expression.simplify()

print(expression)
// Prints ".divide(.n(15), .n(2))"

Now lets go through the steps of the multiplication simplifications in the order they occur.

The first three steps of this simplification are executed in only one call. The last step is executed in during the division operation's simplification of its operands. This allows me to apply simplifications faster, skipping unnecessary cases and avoid writing a large amount of extremely specific cases.


Design

This design is consistent with the grammar of other control flow statements (e.g. break, continue) followed by an optional labeled argument. The original keyword in this pitch was continue, but upon more careful consideration and the suggestion of @karim, I chose to go with the keyword resume instead. This is better because the addition of this feature would not break any code that already utilizes continue in for loops. Also, the ambiguity of whether it restart the execution of the switch statement from the very start or whether it would continue pattern matching as intended is eliminated. The use of the word resume is also specific to switch statements, as it would indicate the resuming of pattern matching.


Thoughts?

3 Likes

On first though, this is going to break a lot of code already using continue inside a switch, like this:

for element in elements {
   switch element {
   case 25:
      continue // go to next element in the loop
   default:
      break
   }
}

So this syntax is no good. That aside, the feature might be interesting, though I'm not sure I ever needed that. It's easy to model this with a sequence of ifs too.

2 Likes

This makes control flow more difficult to reason about, and I agree with @michelf that the expressivity gain would be limited.

A big red flag for me suggesting that this feature would be confusing to users is that, in an example of your own devising, there’s a mistake in reasoning about the control flow:

This should print:
n is greater than or equal to 20
n is greater than or equal to 10
n is a multiple of 5

Moreover, in the case where you would want this code to tell you if a number is both a multiple of 3 and 5, it would fail to do so. This should be immediately obvious to a reader but took me studying the code at least four times before realizing. That suggests to me that this feature would be harmful to writing correct code.

1 Like

Sorry, that was a mistake on my part. I’ve updated the pitch.

Both the examples provided would be more clearly expressed as two separate switch statements - I imagine this would be the case most of the time.

1 Like

I feel like using multiple if or switches is far more effective and clear on the intent here. Switches in every language I am aware of don't have a mechanism to skip to the next matching case. They all just have fallthrough which that alone has caused multitudes of bugs. This is why many of the newer languages such as swift have an explicit fallthrough keyword.

Have you already seen https://forums.swift.org/t/idea-pitch-add-match-statement-as-switch-like-syntax-alternative-to-if-case-pattern-matching?

3 Likes

I'm generally in favor of the idea of an obvious goto-like statement. Goto is usually distasteful, but there are real cases where it is useful: for instance, if you need to generate Swift from a control flow graph representation.

fallthrough unconditionally drops into the next case, whereas OP wishes to match against all of the later cases without rematching the already matched case.

1 Like

This may just be my opinion/approach, but when I see a switch in Swift I generally expect only one case to be true with a given input. I see overlapping cases as something to avoid and that they're usually a code-smell. Personally I would even be in favor of a compiler warning with overlapping cases.

In your examples, you're not really switching on a value. You're inspecting the value to determine attributes. I would argue this is better expressed with traditional if-statements.

let n = 35

if n >= 20 {
        print("n is greater than or equal to 20")
}
if n >= 10 {
        print("n is greater than or equal to 10")
}
if n.isMultiple(of: 3): {
         print("n is a multiple of 3")
}
if n.isMultiple(of: 5) {
        print("n is a multiple of 5")
}

I would say that's significantly clearer in indicating the intention of the code. It's also not only fewer lines, but allows much shorter and simpler code for the .isMultiple cases.

I would expect it to also be faster since you wouldn't get the performance boost of using static cases anyway, and you have to create additional variables with the .isMultiple cases.

For the rare cases where multiple case statements are needed and actually better, fallthrough is available, and if you expect the variable to change, you can put it in a function and handle that recursively.

I think I can get behind labeling and directly calling cases. Personally I still think it's a codesmell and the called case body should just be put in a separate function and called from both cases, but it at least enforces clarity.

If we're wanting switch to act as code organization rather than have a more constrained meaning, I would prefer adding something like kotlin's when and allow (or restrict?) it to have no subject and allow the cases to act essentially as functions, rather than a traditional switch case.

4 Likes

Could you elaborate on what you mean by this? I can't picture it.

These days, compilers work with code in the form of a graph, where each node is a block of code and each edge is a goto. For instance, here's code with the corresponding graph representation:

// A              (A)
if i {            / \
    // B        (B)  |
} else {         |   |
    // C         |  (C)
}                 \ /
// D              (D)

It's a notoriously difficult problem to lift that representation back to structured if/else/while statements, but it maps plainly to goto statements.

If you have a tool that works on a graph representation of code and you want it to output Swift code, currently, you have to do pretty annoying gymnastics and the compiled output appears sub-optimal.

I think we should not use continue here. My mental model of what continue should do in a switch is to restart the matching from the beginning. But this would be bad because we are now using continue as a goto statement which can create infinite loops. I don't think this should be added to switch. Replace the continue with goto and hopefully you will see what I mean. Thanks!

You are right @masters3d, that would be confusing. If another keyword were indeed chosen, it would make this a non-breaking change as well. Are there any keywords that you (or for that matter anyone else) can think of as an alternative?

We can use goto, but is there anything that you can think of that would be more specific to use in a switch statement. I think the keyword, when used alone, should connote continuing pattern matching at the next case, because requiring every call to this continue pattern matching keyword to be followed by a label seems excessive and will probably clutter code too much. And while using goto followed by a label makes some sense, it is not standalone.

Probably something that fits the standard control flow keyword grammar, but specific to a switch statement:

Grammar of a insert keyword here Statement:

keyword-statementkeyword label-name (opt)

Interesting! Is there a name for this problem so I can read up more?

Also, do you know if SIL has a goto instruction? It wouldn't surprise me, and that would be ideal for codegen tools to target.

The problem is called control flow structuring, but to be clear, it’s hard enough that unless it is your end goal, it’s a better idea to find a way to emulate gotos.

SIL doesn’t call them gotos and they aren’t exactly instructions (they’re “terminators” because they mark the end of a basic block), but yes, it uses them instead of if-else or loop statements.

I don’t have an opinion either way on this, but if in need of a keyword different from “continue”, I’d suggest resume.

3 Likes

As a person who is trying to improve the exhaustivity checking of switches, I beg of you, let this go.

Changing the nature of exhaustivity checking based on the presence of a keyword inside the body seems like a tough thing to reason about. Do we drop any diagnostics about repeated cases? Only for cases that have this new keyword? I would suggest additionally switching over a bitfield that keeps track of which cases have been handled.

(I do realize that integer doesn't really have exhaustivity checking at present but I still hope that we can change that)

2 Likes

I really like that keyword suggestion, I’m going to update my pitch with resume instead of continue.

Isn't this proposal only allowing a value to be matched against several patterns? If I understand this correctly it shouldn't change the exhaustivity checking. A switch that is exhaustive without the resume keyword, is also exhaustive with it, non? And vice versa?

2 Likes