[Swift] Allow way to specify subclasses are a closed set, for pattern matching completeness

Originator:AustinZheng
Number:rdar://20247227 Date Originated:20-Mar-2015 05:19 PM
Status:Open Resolved:
Product:Developer Tools Product Version:
Classification: Reproducible:
 
Summary:
For a Swift module, a non-final class declared as public can be subclassed by consumers of the module arbitrarily. However, it's possible to declare closed class hierarchies using a "base" class that is private or internal; in this case, all subclasses must also be private or internal, and as a result must reside within the module or file.

The pattern-matching case statement allows matching on subclass. However, a 'default' clause is always required, even if the subclasses of the predicate are a known closed set. This makes switch statements on subclasses more error-prone, since adding a new subclass does not break the switch at compile-time (whereas adding a new case in an enum would).

This request is for Swift's compiler to be enhanced so that it can figure out if a given class's subclass hierarchy can be completely known at compile time because of finality and access control, and allow total matching in the cases where this is true.

Optimally, a Swift file with the following code should work:

private class Foo { }

private class Foo1 : Foo { }
private class Foo2 : Foo { }
private class Foo3 : Foo { }

private func buildFoo() -> Foo {
  // Flip a coin and return one of the three subtypes...
}

private func test() {
  let a : Foo = buildFoo()

  switch a {
  case is Foo1: println("foo1")
  case is Foo2: println("foo2")
  case is Foo3: println("foo3")
  // (no need for default:, since the compiler knows there can't be any other subclasses)
  }
}

Steps to Reproduce:
See sample code in description.

Expected Results:
Swift compiler figures out subclasses are a closed set, allows switch statement to cover all cases without "default:".

Actual Results:
Swift compiler complains unless the switch statement has an unused "default:" clause.

Version:
Xcode 6.3b3, OS X 10.10.2

Notes:
Even if checking for classes across multiple files is too difficult, this should be possible on a per-file basis for private classes (whose subclasses must necessarily live within the same file).

Comments

Thanks for commenting, and suggesting a workaround.

The base class point is a good one. I think the overall point still stands, though, since the set of possible concrete types is still closed (it just includes the original class in addition to the subclasses).

The problem at hand is one of compile-time correctness. Adding "default:" to a switch statement removes the compile-time guarantees the exhaustiveness checker gives you. Unfortunately, this is a requirement even when it is impossible for the set of possible classes an object can be to be unbounded. One can make a legitimate argument that most cases of switching on a type would be better served by implementing a method in the base class that must be overriden by its children, but given that matching on subtypes is possible (and useful in some cases), the correctness checking that matching gives you should also be provided whenever possible. (As well, abstract methods don't exist, so it's not possible to define a method that must be overriden by subclasses; nor do mix-ins exist, so it's not possible to define a protocol that provides default implementations for some methods.)

By AustinZheng at April 7, 2015, 6:02 a.m. (reply...)

Sometimes a Foo is just a Foo

An issue with your example code is that there isn't any case which catches an actual instance of the Foo class rather than an instance of a subclass (which would be a valid return value from your buildFoo() function).

Would it work to wrap your private classes in an enum , something like this?

private class Foo { }

private class Foo1 : Foo { }
private class Foo2 : Foo { }
private class Foo3 : Foo { }

private enum FooType
{
    case FooType1(Foo1)
    case FooType2(Foo2)
    case FooType3(Foo3)
}    

private func buildFoo() -> FooType {
    // Flip a coin and return one of the three subtypes...
    return FooType.FooType1(Foo1())
}

private func test() {
    let a : FooType = buildFoo()

    switch a {
        case .FooType1: println("foo1")
        case .FooType2: println("foo2")
        case .FooType3: println("foo3")
        // (no need for default:, since the compiler knows the enum is complete)
    }
}

test()

Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!