19 July 2016
The following is a discussion of Swift 3’s controversial approval of the ‘sealed by default’ proposal that puts constraints on subclassability. To contextualise the decision, it is first necessary to review how Swift approaches access control.
In Swift, types and members are defaulted to
internal visibility. This means they are only visible within the scope of the same module. In another module, internal types are not accessible at all. Making these things accessible requires a
public keyword for every symbol. This means nothing is exposed to the wider project by default unless it is explicitly marked; only things that the developer have chosen to be available to other modules are.
This sounds onerous but it actually makes sense from a codebase design perspective. Generally, most methods and properties written into a class or struct are implementation details which are irrelevant to other consumers. As code is read more often than it is written, the benefits of distinguishing a public and private API surface outweigh the burden of having to write a public declaration every so often.
This ideology is central to Swift, favouring explicit statements over implicit behaviours. This is done primarily, but not entirely, to express the best coding practices. Developers have to make a conscious decision which parts of the interface are public and which aren’t. It enables for potential performance benefits like static dispatch and intelligent high-level features like Generated Headers.
All of this strictness is uncomfortable to Objective-C developers which is a lax language; it lets everything be ambiguously public or private at the mercy of the programmer. It was uncomfortable to me. Swift allows for the same dynamic runtime features1, but it wants those capabilities to be explicitly defined and constrained only to the symbols that requires them.
The title of the post has nothing to do with any of this functionality, of course. There are parallels that you can draw though with clear similarities in how Swift is thought about and designed.
‘Sealed by default’ is a separate concept to runtime manipulation or access control in regard to its functionality; sealed classes cannot be subclassed outside of the module they are declared in. The underlying premise of only enabling functionality when it is appropriate is the same, using keywords to denote special entitlements.
Objective-C barely has the concept of modules, let alone being sealed. Any class in Objective-C can be inherited and overridden regardless of what framework it resides in. Swift 2 already has some limitations on this freedom. Although anything can be subclassed by default in Swift 2, there is a
final keyword that prevents any source from subclassing it (essentially becoming a reference type struct).
final is more restrictive than
sealed which is more restrictive than
open (the implicit Objective-C behaviour). Sealed classes are still open inside their own module. This allows flexibility for the module maker (supporting the common class cluster pattern) whilst remaining closed to the rest of the codebase.
The concept of sealed classes does not exist in Swift 2 at all but is going to be the new default in Swift 3. Developers of modules can add the ability for classes to be subclassed by anyone using the
open keyword on relevant type declarations.
This choice for classes to be sealed by default with Swift 3 has caused a lot of controversy; even the core team admitted there was no consensus in their mailing list post approving the change. I think it is the right thing to do but it’s not hard to see why others are angry.
The change removes the capability for application developers to subclass third-party library and framework code. The module defines what can and can’t be overridden. Sealed doesn’t affect a developer’s own classes, but it does stop developers from overriding framework classes, like those found in UIKit and AppKit.
Developers can use clever subclassing tricks to resolve some bugs that exist in third-party frameworks. These are almost always unsupported brittle changes, though, that aren’t guaranteed to be stable or keep working between OS versions.
To be frank, it is a fluke that this stuff even works. Subclassing where you aren’t supposed to is essentially injecting code into someone else’s private components. Ask any Apple engineer and they will tell you never to subclass UIKit. In Objective-C, this is only expressed via documentation and guidelines. With Swift 3, it can be enforced in the code and is compulsorily adhered to.
Perhaps there is a debate here about the usefulness of subclassing to combat bugs. I don’t think it is very useful though and will get even less useful as people write Swift frameworks in Swift where classes aren’t even that common and instead relying on structs or enumerations. A good example here is to look at the adoption of C libraries, here is any C library, which are made up of free functions. These functions can and do have bugs with no recourse via inheritance. This has not stunted adoption.
In general, language design should not be decided by the possible existence of buggy code. However much we strive to make perfect code, there will always be bugs. Sealed by default also prevents a different swathe of bugs from happening as API users don’t have to rely on humans to check documentation about whether something is supported. Sealed, final and open allow coders to accurately convey how their APIs are meant to be used, at least more accurately than Objective-C did.
As highlighted by the preface of this post, I hope the parallels between stricter rules about inheritance and stricter public-private access control are self-evident.
Designing and enforcing rules for inheritance is aligned with Swift as a language. It would be inconsistent not to have sealed by default with explicit keywords to allow for stricter or looser inheritance. It brings several benefits. Static dispatch can be employed more frequently when the compiler can guarantee there are no external subclasses. Performance benefits for a GUI application are minimal, granted, but every little helps.
Of course, the primary reason is creating a programming model that is more correct with proper encapsulation and containment. Classes that aren’t meant to be subclassed, can’t be. That has to be better than an ambiguous spaghetti mess.
I think if you can understand and agree with the explicit marking of things as public or not, then you should hold no objection to the sealed by default proposal. Explicitness in cases of ambiguity is a theme of Swift. Rather than guessing or choosing a lazy default that accepts anything, it is stringent in its enforcement. Accommodating debugging or monkey patching — when it flies in the face of the overall language — makes no sense to me.
The last thing I’ll say is that doing ‘sealed by default’ with Swift 3 makes the most sense when you consider the project’s roadmap. Apple wants Swift 3 to be the last major source-breaking release. Deciding to be restrictive now, with sealed by default, and then backtracking later is not a source-breaking change. Apple can freely make things open again … if the change really is destructive. It’s not possible to go back the other way, from open to sealed, in a source-compatible matter later on.
Even without any knowledge of the pros or cons of the argument, logic indicates to do the more bullish thing now as the option to reverse it remains available.
1 Current Swift doesn’t have the full range of dynamic features that Objective-C has, not even close. I don’t think there is a philosophical aversion to adding that stuff in future Swift versions, however.