> On Feb 6, 2017, at 9:00 PM, Karl Wagner via swift-evolution > <swift-evolution@swift.org> wrote: > - Nested protocols in generic types are not parameterised by the parent's > generic parameters. So if I write GenericType<Int>.SomeProto and GenericType<String>.SomeProto, is it the same protocol? What about GenericType.SomeProto, is that allowed?
Slava > - Some observations as to how capturing might be implemented in the future, > standard library benefits. > > This is a chance for everybody to go over it once more before the actual > review. > > Github: https://github.com/apple/swift-evolution/pull/552 > <https://github.com/apple/swift-evolution/pull/552> > Raw: > -------- > # Ease restrictions on protocol nesting > > * Proposal: [SE-XXXX](xxxx-ease-protocol-nesting.md) > * Authors: [Karl Wagner](https://github.com/karlwa > <https://github.com/karlwa>) > * Review Manager: TBD > * Status: **Awaiting review** > > *During the review process, add the following fields as needed:* > > * Decision Notes: > [Rationale](https://lists.swift.org/pipermail/swift-evolution/ > <https://lists.swift.org/pipermail/swift-evolution/>), [Additional > Commentary](https://lists.swift.org/pipermail/swift-evolution/ > <https://lists.swift.org/pipermail/swift-evolution/>) > * Bugs: [SR-NNNN](https://bugs.swift.org/browse/SR-NNNN > <https://bugs.swift.org/browse/SR-NNNN>), > [SR-MMMM](https://bugs.swift.org/browse/SR-MMMM > <https://bugs.swift.org/browse/SR-MMMM>) > * Previous Revision: > [1](https://github.com/apple/swift-evolution/blob/...commit-ID.../proposals/NNNN-filename.md > > <https://github.com/apple/swift-evolution/blob/...commit-ID.../proposals/NNNN-filename.md>) > * Previous Proposal: [SE-XXXX](XXXX-filename.md) > > ## Introduction > > Protocols define a way to express a syntactic and semantic contract. This > semantic nature means that protocols are often intended to used in the > context of one specific type (such as a 'delegate' protocol). Similarly, > protocols sometimes wish to define specific types to be used within the > context of that protocol (usually an `enum`). > > This proposal would allow protocols to be nested in other types (including > other protocols), and for structural types to be nested inside of protocols > -- subject to a few constraints. > > Swift-evolution thread: [Discussion thread topic for that > proposal](https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20161017/028112.html > > <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20161017/028112.html>) > > ## Motivation > > Nesting types inside other types allows us to scope their usage and provide a > cleaner interface. Protocols are an important part of Swift, and many popular > patterns (for example, the delegate pattern) define protocols which are > intended to be used in the semantic context of other types. It would be nice > to apply type-nesting here: `MyClass.Delegate` reads better than > `MyClassDelegate`, and literally brings structure to large frameworks. > > Similarly, we have examples of protocols in the standard library which define > supporting types to be used in the context of that protocol - > `FloatingPointClassification`, `FloatingPointSign`, and > `FloatingPointRoundingRule` are enums which are used by various members of > the `FloatingPoint` protocol. These types are part of the contract which the > protocol defines, and so it would be nice if they could be nested within the > protocol to reflect that (i.e. `FloatingPoint.Classification`, > `FloatingPoint.Sign`, `FloatingPoint.RoundingRule`). > > ## Proposed solution > > The first part is to allow protocols to be nested inside of structural types > (for example, in the delegate pattern): > > ```swift > class AView { // A regular-old class > protocol Delegate: class { // A nested protocol > func somethingHappened() > } > weak var delegate: Delegate? > } > > class MyDelegate: AView.Delegate { > func somethingHappened() { /* ... */ } > } > ``` > > The second part is to allow nominal types to be nested inside of protocols > (for example, `FloatingPoint.Sign`). > > ```swift > protocol FloatingPoint { > // 'Sign' is required for conformance, therefore good candidate for > nesting. > enum Sign { > case plus > case minus > } > var sign: Sign { get } > } > ``` > > Similarly, protocols may be nested inside other protocols: > > ```swift > protocol Scrollable: class { // A regular-old protocol. > var currentPosition: Position { get } > > protocol Delegate: class { // A nested protocol. > func scrollableDidScroll(_: Scrollable, from: Position) > } > weak var delegate: Delegate? > } > > class MyScrollable: Scrollable { > var currentPosition = Position.zero > > weak var delegate: Delegate? > } > > extension MyController: Scrollable.Delegate { > func scrollableDidScroll(_ scrollable: Scrollable, from: Position) { > let displacement = scrollable.currentPosition.x - from.x > // ... > } > } > ``` > > **Namespacing:** > > It is important to draw a distinction between a protocol's nested types and > its associated types. Associated types are placeholders (similar to generic > type parameters), to be defined individually by each type which conforms to > the protocol (e.g. every `Collection` will have a unique type of `Element`). > Nested types are standard nominal types, and they don't neccessarily have > anything to do with the conforming type (e.g. they may have been added in a > protocol extension). We would like to avoid importing _irrelevant_ types in > to the namespaces of conformers, but at the same time provide a convenient > interface for any _relevant_ nested types (e.g. `FloatingPoint.Sign` is > clearly relevant to `Float`, and it would be nice to call it `Float.Sign`). > > To achieve this, the compiler will generate hidden typealiases for all nested > types which are used by the protocol's requirements. The `FloatingPoint` > protocol requires that its members have a property of type > `FloatingPoint.Sign`, so it is clearly a relevant type for anybody who > conforms to `FloatingPoint`, and should be made convenient. The exception is > nested protocols, which are not implicitly imported even if used by a > requirement. For example: > > ```swift > protocol FloatingPoint { > enum Sign { > case plus > case minus > } > var sign: Sign { get } // 'Sign' is used by a requirement. exported via > typealiases. > } > > struct Float: FloatingPoint { > // [implicit] typealias Sign = FloatingPoint.Sign > var sign: Sign { /* ... */ } // Sugared type: Float.Sign (via typealias) > } > > let _: FloatingPoint.Sign = (4.0 as Float).sign as Double.Sign // all good B) > ``` > > Looking at the Scollable example again, we want to ensure programmers write > `Scrollable.Delegate` when writing their conformances and do not want to > allow `MyScrollable.Delegate` as an alternative. For that reason, the > compiler will not synthesise typealiases for nested protocols: > > ```swift > protocol Scrollable: class { > protocol Delegate: class { > func scrollableDidScroll(_: Scrollable, from: Position) > } > weak var delegate: Delegate? // nested protocol 'Delegate' used by > requirement, but _not_ imported!. > } > > class MyScrollable: Scrollable { > // _no_ implicit typealias! > weak var delegate: Delegate? // Unqualified lookup. Type is: > Optional<Scrollable.Delegate> > } > > extension MyController: Scrollable.Delegate { // <- We don't want to allow > 'MyScrollable.Delegate' here! > func scrollableDidScroll(_ scrollable: Scrollable, from: Position) { > // ... > } > } > ``` > > Other nested types (i.e. those not used by requirements) are also not > implicitly imported. Consider the following example of a struct which is > added to `RandomAccessCollection` by an extension; it has nothing to do with > any particular Collection (because it is generic), so it is irrelevant to any > particular conforming type: > > ```swift > extension RandomAccessCollection { > /// A view of a collection which provides concurrent implementations of > map, filter, forEach, etc.. > struct Concurrent<T: RandomAccessCollection> { /* ... */ } > > var concurrent: Concurrent<Self> { return Concurrent(self) } // is not a > requirement. No implicit typealiases for Concurrent<T>. > } > > let _ = [1, 2, 3].concurrent // type is: > RandomAccessCollection.Concurrent<Array<Int>>, _not_ Array<Int>.Concurrent > ``` > > It would be possible to work around this with a typealias. We might consider > synthesising these too once we have the ability to have default parameters > for generic types. This would give us a convenient way to namespace 'views' > of protocol conformers, as the standard library already does with > `LazySequence<S>` and `JoinedSequence<S, T>`: > > ```swift > extension RandomAccessCollection { > // Default is bound to 'Parent' (`Self` of the parent context), which we > implement by creating a typealias. > struct Concurrent<T: RandomAccessCollection = Parent> { /* ... */ } > > // [implicit] typealias Concurrent = Concurrent<Self> > } > > let _: Array<Int>.Concurrent = [1, 2, 3].concurrent // sugared type: > Array<Int>.Concurrent > // canonical type: > RandomAccessCollection.Concurrent<Array<Int>> > ``` > > **Access Control:** > > Currently, members of a protocol declaration may not have access-control > modifiers. That should apply for nested type declarations, too. The nested > type itself, however, may contain members with limited visibility (including > a lack of visible initialisers). The exception is that class types may > include `open` or `final` modifiers. > > ```swift > public protocol MyProtocol { > final class Context { // 'MyProtocol.Context' may not > have any access control modifiers. Visibility: public. > fileprivate let _parent: MyProtocol // 'MyProtocol.Context._parent', > however, may have limited access. > // No public initialisers. That's allowed. > } > } > ``` > > Nested types may also be declared inside of protocol extensions. Consistent > with current language rules, nested type declarations inside of protocol > extensions _may_ have access control: > > ```swift > extension FloatingPoint { > internal enum SignOrZero { // 'FloatingPoint.SignOrZero' may have > limited access. > case plus > case minus > case zero > } > internal var signOrZero: SignOrZero { > if self == 0.0 { > return .zero > } > else switch self.sign { > case .plus: return .plus > case .minus: return .minus > } > } > } > ``` > > **Constrained extensions:** > > Nested types may also be defined inside of constrained protocol extensions, > although they share a single namespace with unconstrained extensions: > > ```swift > // View as a series of UTF8 characters > extension Collection where Element == UInt8 { > struct UnicodeCharacterView: Collection { let _wrapped: > AnyCollection<UInt8> } // Type: Collection.UnicodeCharacterView > } > > // View as a series of UTF16 characters > extension Collection where Element == UInt16 { > struct UnicodeCharacterView: Collection { let _wrapped: > AnyCollection<UInt16> } // ERROR: Redefinition of type > 'Collection.UnicodeCharacterView'. Workaround is to make unique, e.g. > 'Collection.UTF16View'. > } > ``` > > **Generic types:** > > Protocols may be nested inside of generic types, but they are not > parameterised by any generic parameters. In the example below, there is only > one nested protocol: `MyCollectionView.SelectionDelegate`, not > `MyCollectionView<Book>.SelectionDelegate` or > `MyCollectionView<Movie>.SelectionDelegate`: > > ```swift > class MyCollectionView<MediaItem> : UICollectionView { > > protocol SelectionDelegate: class { > func itemWasSelected(at: Index) > } > weak var selectionDelegate: SelectionDelegate? > } > > class MyDelegate: MyCollectionView.SelectionDelegate { // Use of > unparameterised 'MyCollectionView' to refer to nested protocol. > func itemWasSelected(at: Index) { /* ... */ } > } > > MyCollectionView<Book>.selectionDelegate = MyDelegate() > MyCollectionView<Song>.selectionDelegate = MyDelegate() > MyCollectionView<Movie>.selectionDelegate = MyDelegate() > ``` > > > ### Limitations > > This proposal leaves one major limitation on protocol nesting: that nested > types may not capture any types from (or through) a parent protocol. There is > a 2x2 matrix of cases to consider here: when a nested protocol/nominal type > captures a type parameter from a parent protocol/nominal types. The TLDR > version is: > > | Capture from parent (V)\ by nested (H) | Protocol | Nominal Type | > | ------------- | ------------- |---| > | Protocol | No | No | > | Nominal Type | No | Yes! but not through a protocol. | > > Essentially this is due to compiler limitations around existentials. If/when > the compiler is capable of more comprehensive existentials, we can revisit > capturing across nested generic protocols/types. There are enough useful > cases which do not depend on this ability (including in the standard library) > that it's worth implementing what we can today, though. > > > ## Detailed Design > > Given that nesting infers some context to the inner type, and that there is > some friction between protocols with associated types ("generic protocols") > and generic types, this section seeks to clarify when capturing is/is not > allowed. Although it references compiler limitations surrounding > existentials, any such changes are _not a part of this proposal_. > > - Protocols may not capture associated types > > ```swift > protocol Stream { > associatedtype Content > protocol Receiver { > func receive(content: Content) // ERROR: Cannot capture > associated type 'Content' from 'Stream' > } > var receiver: Receiver { get set } > } > ``` > > Fundamentally, this is a compiler limitation. Ideally, we would like to > represent the capture roughly so: > > ```swift > protocol Stream { > associatedtype Content > protocol Receiver { > // [implicit] associatedtype Content > func receive(content: Content) > } > var receiver: Any<Receiver where .Content == Content> { get set } // > Not possible today. > } > ``` > > Should this limitation be lifted, we can revisit capturing of associated > types. > > > - Protocols may not capture generic type parameters: > > Even if we wanted to do this with an implicit associated type, as > mentioned above we couldn't represent the constrained protocol existential in > the parent. Since it is a stated decision to avoid parameterised protocols, > any capturing which may be allowed in the future would be expressed via > existentials on an unparameterised protocol anyway, possibly looking > something like this: > > ```swift > class MyCollectionView<MediaItem> : UICollectionView { > > protocol Source { > // [implicit] associatedtype MediaItem > func item(at: Int) -> MediaItem > var numberOfItems: Int { get } > } > var source: Any<MyCollectionView.Source where .MediaItem == > MediaItem> // Not possible today. > } > > class BookSource: MyCollectionView.Source { > typealias MediaItem = Book > > func item(at: Int) -> Book { /* ... */ } > var numberOfItems: Int { /* ... */ } > } > > class DummySource<MediaItem>: MyCollectionView.Source where MediaItem: > DummyConstructable { > // associatedtype 'MediaItem' bound to generic parameter. > > func item(at: Int) -> MediaItem { /* ... */ } > var numberOfItems: Int { /* ... */ } > } > > MyCollectionView<Book>().source = BookSource() > MyCollectionView<Book>().source = DummySource<Book>() > MyCollectionView<Song>().source = DummySource() // type is: > DummySource<Song> > MyCollectionView<Movie>().source = DummySource() // type is: > DummySource<Movie> > ``` > > - Nominal types *may* capture generic type parameters, but not through a > protocol > > Nominal types can already have nested types which capture parameters from > their parents, and this proposal does not change that. However if we consider > the possible capture hierarchies when protocols are involved, one situation > is noteworthy: > > ```swift > struct Top<X> { > protocol Middle { > enum Bottom { > case howdy(X) // ERROR: Cannot capture 'X' from Top<X> > } > > var bottomInstance : Bottom { get } // Would require capturing 'X' > } > } > ``` > > It isn't possible to refer to `Bottom` (or any types nested below it) > from `Middle`, due to the above limitation on protocols capturing generic > type parameters. Therefore the nesting is meaningless and should not be > allowed. > > - Nominal types may not capture associated types > > Consider the `RandomAccessCollection.Concurrent` example from before, if > it were allowed to directly capture associated types from its enclosing > protocol (rather than a magic potion of generics, defaults and typealiases): > > ```swift > // Note: Pretend there is something called 'Parent' which is a captured > 'Self' of the parent protocol. > protocol RandomAccessCollection { > > struct Concurrent: RandomAccessCollection { > typealias Element = Parent.Element > typealias Index = Parent.Index > init(with: Parent) { /* ... */ } > } > var concurrent: Concurrent { return Concurrent(self) } > } > ``` > > By capturing associated types, the type > `RandomAccessCollection.Concurrent` would also become existential (something > like `RAC.Concurrent where Parent == Array<Int>`). We could theoretically map > the capture of 'Parent' in to a generic parameter behind-the-scenes, but this > kind of capturing would only work for would-be captures from the immediate > parent before we start having the familiar problem of protocols capturing > associated types. It would be better to tackle capturing between nested > protocol types seperatetely at a later date. > > ```swift > protocol Top { > associatedtype AssocTop > > protocol Middle { > associatedtype AssocMiddle > > enum Result { // [implicit] Result<Parent: > Middle, Parent_Parent: Top> > case one(AssocMiddle) // [implicit] Parent.AssocMidle > case two(AssocTop) // [implicit] Parent_Parent.AssocTop > } > var result: Result { get } // [implicit] Result<Self, ???> - > would need to capture 'Self' from Parent > } > } > ``` > > That's a long explanation of why it's best to just bar any kind of capturing > between protocols and structural types for now. We can maybe address this > limitation at a later date, as part of broader support for existentials and > according to demand. > > ## Source compatibility > > Standard library changes making use of this feature will be part of another > proposal. > > Outside of the standard library, it is likely that the Clang importer could > make use of this feature, as the delegate pattern is very common in Apple's > platform SDKs. Changes such as `UITableViewDelegate` -> > `UITableView.Delegate` can be migrated with a deprecated typealias: > > ```swift > @deprecated("Use UITableView.Delegate instead") > typealias UITableViewDelegate = UITableView.Delegate > ``` > > ## Effect on ABI stability > > This proposal is only about the language feature, but it is likely to result > in standard library and platform SDK changes. > > ## Effect on API resilience > > Since all capturing is disallowed, this type of nesting would only change the > name (in source and symbolic) of the relevant types. > > ## Alternatives considered > > - The alternative to nesting is to namespace your types manually with a > prefix, similar to what the standard library, Apple SDK overlays, and > existing Swift programs already do. However, nested types and cleaner > namespaces are one of the underrated benefits that developers - espcially > coming from Objective-C - have always been excited about. We like clean and > elegant APIs. From time to time somebody pops up on the mailing list to ask > why we don't have this feature yet, and the changes proposed here usually are > met with broad support. > > - Nesting a structural type (with function bodies) inside of a protocol > declaration is expected to be a little controversial. An alternative would be > to require nested types to be defined inside of protocol extensions. This > proposal leaves that as an option (indeed, that is the only way to provide > access control for nested types), but does not require it. We don't encourage > protocol declarations containing large concrete type bodies because those > details are usually irrelevant to those looking to conform to the protocol, > but for trivial cases it may be acceptable; that judgement is left to the > programmer. > > _______________________________________________ > swift-evolution mailing list > swift-evolution@swift.org > https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution