Permalink
Find file Copy path
193 lines (134 sloc) 5.83 KB

Declare variables in 'case' labels with multiple patterns

Introduction

In Swift 2, it is possible to match multiple patterns in cases. However cases cannot contain multiple patterns if the case declares variables.

The following code currently produces an error:

enum MyEnum {
    case Case1(Int,Float)
    case Case2(Float,Int)
}
switch value {
case let .Case1(x, 2), let .Case2(2, x):
    print(x)
case .Case1, .Case2:
    break
}

The error is:

`case` labels with multiple patterns cannot declare variables. 

This proposal aims to remove this error when each pattern declares the same variables with the same types.

Swift-evolution thread: here

Motivation

This change reduces repetitive code, and therefore reduces mistakes. It's consistent with multi-pattern matching when variables aren't defined.

Proposed solution

Allow case labels with multiple patterns to declare patterns by matching variable names in each pattern.

Using the following enum:

enum MyEnum {
    case Case1(Int,Float)
    case Case2(Float,Int)
}

These cases should be possible:

case let .Case1(x, _), let .Case2(_, x):
case let .Case1(y, x), let .Case2(x, y):
case let .Case1(x), let .Case2(x):
case .Case1(let x, _), .Case2(_, let x):

Likewise for other uses of patterns:

let value = MyEnum.Case1(1, 2)
if case let .Case1(x, _), let .Case2(_, x) = value {
  ...
}

Detailed design

Allow case labels with multiple patterns if the case labels match the following constraints:

  • All patterns declare exactly the same variables.
  • The same variable has the same type in each pattern.

Therefore each pattern is able to produce the same variables for the case label.

In the case of if case let usage the syntax is the same, the only issue is whether this can be combined with other variables, and whether it is unambiguous.

The pattern grammar gets the following change:

+ enum-case-pattern-list → enum-case-pattern |
+                          enum-case-pattern , enum-case-pattern-list
+ pattern  → enum-case-pattern-list
- pattern  → enum-case-pattern

Impact on existing code

This should have no impact on existing code, although it should offer many opportunities for existing code to be simplified.

Alternatives considered

Using a closure or inline function

Code repetition can be reduced with one pattern per 'case' and handling the result with an inline function.

func handleCases(value: MyEnum, apply: Int -> Int) -> Int {
    func handleX(x: Int) -> Int {
        return apply(x) + 1
    }
    let out: Int
    switch value {
    case .Case1(let x, 2):
        out = handleX(x)
    case .Case2(2, let x):
        out = handleX(x)
    case .Case1, .Case2:
        out = -1
    }
    return out
}

This syntax is much more verbose, makes control flow more confusing, and has the limitations of what the inline function may capture.

In the above example apply cannot be @noescape because handleX captures it.

Also in the above example if out is captured and assigned by handleX then it must be var, not let. This can produce shorter syntax, but is not as safe; out may accidentally be assigned more than once, additionally out also needs to be initialized (which may not be possible or desirable).

Extending the fallthrough syntax

A similar reduction in code repetition can be achieved if fallthrough allowed variables to be mapped onto the next case, for example:

switch test {
    case .Case1(let x, 2): 
        fallthrough .Case2(_, x)
    case .Case2(3, .let x):
        print("x: \(x)")
}

This is not as intuitive, is a hack, and fallthrough should probably be discouraged. It is much more flexible, a programmer could adjust the value of x before fallthrough. Flexibility increases the chances of programmer error, perhaps not as much as code-repetition though.

Chainable pattern matching

In my opinion if case let syntax is a little clumsy. It's good that it's consistent with a switch statement, but it's not easy to chain. I think a different syntax may be nicer, if a few things existed:

  • A case-compare syntax that returns an optional tuple:
let x = MyEnum.Case1(1,2)
let y: (Int,Int)? = (x case? MyEnum.Case1)
assert(y == Optional.Some(1,2))
  • multiple field getters (similar to swizzling in GLSL). It returns multiple named fields/properties of a type as a tuple:
let x = (a: 1, b: 2, c: 3)
let y = x.(a,c,b,b)
assert(y == (1,3,2,2))

You could compose them like this:

enum MyNewEnum {
    case MyCase1(Int,Float), MyCase2(Float,Int)
}
let x = MyNewEnum.Case1(1,2,3)
let y: (Int,Float)? = (x case? MyNewEnum.Case1) ?? (x case! MyNewEnum.Case2).(1,0)

This is not a true alternative, it does not work in switch statements, but I think it still has value.

Future Considerations

It would be nice to use the if case let syntax to define variables of optional type.

Something like this:

 case let .Case1(x,_) = MyEnum.Case1(1,2)

Which would be the equivalent of this:

 let x: Int?
 if case let .Case1(t) = MyEnum.Case1(1,2) { x = t.0 } else { x = nil }

It would support multiple patterns like so:

 case let .Case1(x,_), .Case2(_,x) = MyEnum.Case1(1,2)

This is not necessary if chainable pattern matching was possible, but I've made sure this proposal is compatible.