I'm starting this thread after an arduous fight with subclassing in Swift that's left me saddened by the current mental model the language holds, which in my opinion manages to be both limiting and overcomplicated at the same time.
Motivation
I'm writing up a new Swift package made out of a few modules. One of the modules we'll call Base
, and it's where our Base
class resides:
// Module: Base
open class Base {
package func printMessage() {
print("Hello!")
}
}
My other module Foo
that depends on Base
would like to override printMessage
in its subclass, however this is not currently possible:
// Module: Foo
import Base
public class Foo: Base {
package override func printMessage() { // 🛑 error: Overriding non-open instance method outside of its defining module
print("Hello, I'm Foo!")
}
}
So then I'm forced to mark printMessage
as open
, which I don't want to do because printMessage
is supposed to stay hidden from end users of my library:
// Module: Base
open class Base {
open func printMessage() {
print("Hello!")
}
}
The only other realistic workaround is using @_spi
, which is exactly what the introduction of package
was trying to improve upon:
// Module: Base
open class Base {
@_spi(package) open func printMessage() {
print("Hello!")
}
}
// Module: Foo
@_spi(package) import Base
public class Foo: Base {
@_spi(package) public override func printMessage() {
print("Hello, I'm Foo!")
}
}
Proposed Solution
I believe open
should not be an access control level, but a keyword in its own right that can apply to any class entity along with an actual access control level. For example:
// Module: Base
public open class Base {
package open func printMessage() {
print("Hello!")
}
}
// Module: Foo
import Base
public class Foo: Base {
package override func printMessage() { âś…
print("Hello, I'm Foo!")
}
}
One nice side effect of this is that final
isn't even needed in the language anymore, as all classes become actually un-subclassable by default, unless explicitly stated via open
:
class Base {}
class Foo: Base {} // 🛑 error: Inheritance from non-open class 'Base'
open class Base {} // subclassable, but still implicitly internal
class Foo: Base {} // âś…
This new behavior would obviously bring on a fair amount of churn, so realistically it could only become default behavior as part of Swift 6, with a feature flag for incremental adoption.
Would love to know everyone's thoughts on this.
PS: looking through the notes on the original pitch for open
, I see this idea was considered but discarded at the time. This was before we had additional things in the language like a package
ACL. It feels to me it might be time to revisit the question of how much the current behavior is serving us as opposed to hindering the developer experience.