Allow Protocols to be Nested in Non-Generic Contexts
- Proposal: SE-NNNN
- Authors: Karl Wagner
- Review Manager: TBD
- Status: Awaiting review
- Implementation: apple/swift#66247
- Upcoming Feature Flag:
NestedProtocols
Introduction
Allows protocols to be nested in non-generic struct/class/enum/actor
s, and functions.
Motivation
Nesting nominal types inside other nominal types allows developers to express a natural scope for the inner type -- for example, String.UTF8View
is struct UTF8View
nested within struct String
, and its name clearly communicates its purpose as an interface to the UTF-8 code-units of a String value.
However, nesting is currently restricted to struct/class/enum/actors within other struct/class/enum/actors; protocols cannot be nested at all, and so must always be top-level types within a module. This is unfortunate, and we should relax this restriction so that developers can express protocols which are naturally scoped to some outer type.
Proposed solution
We would allow nesting protocols within non-generic struct/class/enum/actors, and also within functions that do not belong to a generic context.
For example, TableView.Delegate
is naturally a delegate protocol pertaining to table-views. Developers should be declare it as such - nested within their TableView
class:
class TableView {
protocol Delegate: AnyObject {
func tableView(_: TableView, didSelectRowAtIndex: Int)
}
}
class DelegateConformer: TableView.Delegate {
func tableView(_: TableView, didSelectRowAtIndex: Int) {
// ...
}
}
Currently, developers resort to giving things compound names, such as TableViewDelegate
, to express the same natural scoping that could otherwise be expressed via nesting.
As an additional benefit, within the context of TableView
, the nested protocol Delegate
can be referred to by a shorter name (as is the case with all other nested types):
class TableView {
weak var delegate: Delegate?
protocol Delegate { /* ... */ }
}
Protocols can also be nested within non-generic functions and closures. Admittedly, this is of somewhat limited utility, as all conformances to such protocols must also be within the same function. However, there is also no reason to artificially limit the complexity of the models which developers create within a function. Some codebases (of note, the Swift compiler itself) make use of large closures with nested types, and they beneift from abstractions using protocols.
func doSomething() {
protocol Abstraction {
associatedtype ResultType
func requirement() -> ResultType
}
struct SomeConformance: Abstraction {
func requirement() -> Int { ... }
}
struct AnotherConformance: Abstraction {
func requirement() -> String { ... }
}
func impl<T: Abstraction>(_ input: T) -> T.ResultType {
// ...
}
let _: Int = impl(SomeConformance())
let _: String = impl(AnotherConformance())
}
Detailed design
Protocols may be nested anywhere that a struct/class/enum/actor may be nested, with the exception of generic contexts. For example, the following remains forbidden:
class TableView<Element> {
protocol Delegate { // Error: protocol 'Delegate' cannot be nested within a generic context.
func didSelect(_: Element)
}
}
The same applies to generic functions:
func genericFunc<T>(_: T) {
protocol Abstraction { // Error: protocol 'Abstraction' cannot be nested within a generic context.
}
}
And to other functions within generic contexts:
class TableView<Element> {
func doSomething() {
protocol MyProtocol { // Error: protocol 'Abstraction' cannot be nested within a generic context.
}
}
}
Supporting this would require either:
- Introducing generic protocols, or
- Mapping generic parameters to associated types.
Neither is in in-scope for this proposal, but this author feels there is enough benefit here even without supporting generic contexts. Either of these would certainly make for interesting future directions.