Skip to content

Instantly share code, notes, and snippets.

@jonhull
Last active July 12, 2016 08:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonhull/a5ac84a16b7a0ffc0c00747998f390d9 to your computer and use it in GitHub Desktop.
Save jonhull/a5ac84a16b7a0ffc0c00747998f390d9 to your computer and use it in GitHub Desktop.
An Alternative for Extensibility Modifiers

An Alternative for Extensibility Modifiers

  • Proposal: SE-NNNN
  • Author: Jon Hull
  • Status: Awaiting review
  • Review manager: TBD

Introduction

Proposal 0117 brought up a heated debate with many good points on both sides. This is an attempt at a solution which takes into account the needs/goals of everyone involved in those discussions, as well as to simplify the syntax overall.

This proposal introduces 3 levels of extensibility: Open, Sealed, and Final. These concepts are kept orthogonal to access levels and can be combined with them in a similar way to property setters (e.g. internal(open)). These terms work for both classes and methods, and could be used if other constructs gain extensibility in future versions of Swift.

The default for unannotated classes/methods would be sealed internal(open), which allows extensibility within the module, but is sealed outside of it.

Motivation

There are several different motivations for this proposal.

Framework authors may want to make their classes final to prevent clients from subclassing them in unexpected ways, but this currently stops them from providing their own subclasses as part of the framework. Current workarounds do not play well with other swift features. By allowing different extensibility levels for different access levels (e.g. final internal(open)) this problem is avoided.

With open by default, adding final to a class later will break (or disallow) any subclasses (making frameworks more brittle). With final by default, framework consumers are unable to subclass/extend any classes/methods which were accidentally left as final (making frameworks less usable). Neither is a good default for constructs which haven't received consideration around extensibility yet. Introducing a third option sealed solves this problem by discouraging subclassing/overriding, but still allowing it in extreme cases using the unsafe keyword.

Thoughtful framework authors would still annotate their classes/methods as either open or final. They do have the option of leaving as sealed parts of the API which are not yet mature, or where they want to allow extensibility only by experts who are putting extra thought into the interaction with the base class.

Proposal 0117 mixes the idea of access modifiers and extensibility. The word public was replaced with subclassable for a class which could be publicly subclassed, and overridable for methods which could be publicly overridden. This adds needless complexity around the idea of access levels (e.g. Are these different access levels? Why does a struct say public while a class says subclassable?) These also repeat the word class making parsing the meaning a little more difficult (e.g. subclassable class)

This proposal simplifies things by keeping the concepts separate. It also simplifies by keeping the terminology the same for classes and methods. The most common annotations for classes/methods would be public open and public final which together are roughly the same length as the originally proposed keywords. It also allows for more specific annotation where required. For example final fileprivate(open) which would allow subclassing within the same file, but be final everywhere else. This type of use is rarer, but should not be forbidden.

Proposed solution

This proposal would introduce three levels of extensibility for classes and their methods:

  • Open - The class or method is open to be extended through subclassing/override.
  • Sealed - Attempting to subclass/override the class/method will result in a compiler error saying that it is not intended to be extended. Using the unsafe keyword (see examples below) will allow compilation.
  • Final - The class or method can not be subclassed/overridden. Attempting to do so will result in a compiler error.

These modifiers apply directly to the class/method and do not affect visibility/access level. In other words, final class means that the class is final and cannot be subclassed. public final class means that a class is both public and final.

To apply different extensibility levels for different access levels, a syntax similar to property setters is used: access_level(extensibility_level). The extensibility for a wider access level must be more restrictive or equal to that of a narrower access level.

For example public sealed internal(open) means that the class/method is sealed publicly, but open internally. open private(sealed) would not be allowed because the narrower access level (private) is more restrictive (sealed) than the wider access level (internal and open).

When there are both a naked modifier (e.g. final) and one associated with an access level (e.g. internal(open)), the naked modifier applies to all access levels wider than the associated level. In the case of final internal(open), final applies to public since that is the only level wider than internal.

The default level for unannotated classes/methods would be sealed internal(open). When associated with the public modifier, this would mean that the class/method is open inside the module, but sealed outside of it. When associated with internal this is equivalent to open

###Examples of use:

//Module A:

//This class is public and open, so it can be subclassed
public open class MyClass {
	
	//This is public & final, and thus can not be overriden by subclasses
	public final func myFinalFunc {} 
	
	//This method is internal (and open within the module)
	func myInternalFunc() {} 
	
	//This method is sealed publicly, but open inside module
	public func myOtherFunc() {} 
	
	//This is public and open, and can be openly subclassed
	public open func myOpenFunc() {}
}

//Because the author has not annotated it, this class is sealed publicly,
//but open inside the module
public class MySealedClass {

	//This method is sealed publicly, but open inside module
	public func myFunc() {}
}

//This is public and final, so it can not be subclassed
public final class MyFinalClass {
}

Outside the module:

//Module B:
import ModuleA

//'MyClass' was open, so we can subclass
class MySubclass:MyClass { 
	//We can't subclass myFinalFunc() because it was final
	//We can't override myInternalFunc() because it was internal
	
	//We need 'unsafe' to unseal. A compiler error will be raised without it
	unsafe override func myOtherFunc() {} 
	
	//This method was defined as open, so we can override
	override func myOpenFunc() {}
}

//We needed 'unsafe' because MySealedClass was sealed. This tells the 
//subclasser that their subclass is not supported/expected and is fragile.
unsafe class MyUnsafeSubclass:MySealedClass {
	
    //We need 'unsafe' to unseal
	unsafe override func myFunc() {} 
}

//We are unable to subclass MyFinalClass because it is final

Detailed design

TBD if there is interest in this approach...

e.g. What happens when you try to fileprivate(open set) and more...

Future Directions

Also TBD if there is sufficient interest.

Are there other modifiers which we can add to express proper/desired subclassing?

For example:

  • Can be subclassed, but not called from outside of class
  • Must call super

Impact on existing code

This would break existing code where a public non-final class had been subclassed outside of the module where it was defined... but in a way where fixits can suggest the needed corrections (i.e. adding unsafe to subclasses)

Alternatives considered

  1. Proposal 0117

  2. Do nothing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment