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-statement →
resume
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?