Skip to content

Commit ff654e4

Browse files
authored
Update xxxx-ease-protocol-nesting.md
1 parent ffe6e0e commit ff654e4

File tree

1 file changed

+61
-125
lines changed

1 file changed

+61
-125
lines changed

proposals/xxxx-ease-protocol-nesting.md

Lines changed: 61 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ class AView { // A regular-old class
3535
protocol Delegate: class { // A nested protocol
3636
func somethingHappened()
3737
}
38-
weak var delegate: Delegate? // Unqualified lookup: AView.Delegate
38+
weak var delegate: Delegate?
3939
}
4040

41-
class MyDelegate: AView.Delegate { // Qualified lookup: AView.Delegate
41+
class MyDelegate: AView.Delegate {
4242
func somethingHappened() { /* ... */ }
4343
}
4444
```
@@ -65,16 +65,16 @@ protocol Scrollable: class { // A regular-old protocol
6565
protocol Delegate: class { // A nested protocol
6666
func scrollableDidScroll(_: Scrollable, from: Position)
6767
}
68-
weak var delegate: Delegate? // Unqualified lookup: Scrollable.Delegate
68+
weak var delegate: Delegate?
6969
}
7070

7171
class MyScrollable: Scrollable {
7272
var currentPosition = Position.zero
7373

74-
weak var delegate: Scrollable.Delegate? // Qualified lookup: Scrollable.Delegate
74+
weak var delegate: Scrollable.Delegate? // Qualified name: Scrollable.Delegate
7575
}
7676

77-
extension MyController: Scrollable.Delegate { // 'MyScrollable.Delegate' would _not_ be ok here, does not exist!
77+
extension MyController: Scrollable.Delegate { // <- Notice _not_ 'MyScrollable.Delegate'
7878
func scrollableDidScroll(_ scrollable: Scrollable, from: Position) {
7979
let displacement = scrollable.currentPosition.x - from.x
8080
// ...
@@ -84,28 +84,52 @@ extension MyController: Scrollable.Delegate { // 'MyScrollable.Delegate' would _
8484

8585
**Namespacing:**
8686

87-
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 which must be used by _every_ type which conforms to the protocol. Taking `FloatingPoint.Sign` as an example, conformance to the `FloatingPoint` protocol means that every conforming type has a property called `sign` whose value may one of a few enum cases defined _as part of the protocol_.
87+
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).
8888

89-
Since nested types are members of the protocol and not the conforming type, they are not implicitly imported in to the namespace of conforming types at all.
90-
91-
Take the standard library's `EmptyCollection<Element>` and `CollectionOfOne<Element>` types as an example of when a protocol may wish to define canned instances with a particular basic behaviour. With nesting, we would call these `Collection.Empty<Element>` and `Collection.One<Element>`; but suddenly every type which conforms to `Collection` has the pleasure of welcoming these irrelevant types in to its namespace:
89+
Since nested types are members of the protocol and not the conforming type, they are not implicitly imported in to the namespace of conforming types. Consider the following example of a struct which is added to `RandomAccessCollection` by an extension; if the type of the result was `Array<T>.Concurrent` the user might expect that they are getting some kind of Array, with specialist Array methods, which is not the case.
9290

9391
```swift
94-
let myArray = Array.Empty<Int>() // type: Collection.Empty<Int>, nothing to do with Array!
92+
extension RandomAccessCollection {
93+
/// A view of a collection which provides concurrent implementations of
94+
/// map, filter, forEach, etc..
95+
struct Concurrent<T: RandomAccessCollection> { /* ... */ }
96+
97+
var concurrent: Concurrent<Self> { return Concurrent(self) }
98+
}
99+
100+
let _: = [1, 2, 3].concurrent // type is: RandomAccessCollection.Concurrent<Array<Int>>, not Array<Int>.Concurrent
95101
```
96102

97-
For `Array.Empty<T>` to have its expected meaning, `Empty` would have to be static function requirement of `Collection` (which would make it `Array<T>.Empty` anyway -- but that's not part of this proposal). In short, `Array.Empty<T>` and `Collection.Empty<T>` have nothing whatsoever to do with each other. So we shouldn't import protocol types.
103+
There are cases, however, when the protocol wishes its confomers to express a particular type. For example, `FloatingPoint` may want its confomers to have a `Sign` type. That is already expressible in the language today, as a typealias. As an exception, we allow protocols to define a typealias with the same name as a nested type, in order to have it inherited by conformers.
104+
98105

99106
```swift
100-
// See: FloatingPoint.Sign example above
107+
protocol FloatingPoint {
108+
enum Sign {
109+
case plus
110+
case minus
111+
}
112+
typealias Sign // name-conflict allowed. Points to (enum) Sign.
113+
114+
var sign: Sign { get }
115+
}
101116

102117
struct Float: FloatingPoint {
103-
var sign: FloatingPoint.Sign { /* ... */ } // Qualified name!
118+
var sign: Sign { /* ... */ } // Can use sugared name
104119
}
120+
```
105121

106-
let _: Double.Sign = (3.0 as Float).sign // ERROR: 'Double.Sign' does not exist - did you mean 'FloatingPoint.Sign'?
122+
This is only a syntactic sugar. Typealiases are overridable, in which case the members revert to their unsugared name, `FloatingPoint.Sign`:
123+
124+
```swift
125+
struct MyFloat: FloatingPoint {
126+
struct Sign { var isBillboard = true; var message = "Howdy!" } // This is potentially poor API design, but allowed.
127+
128+
var sign: FloatingPoint.Sign { /* ... */ }
129+
}
107130
```
108131

132+
109133
**Access Control:**
110134

111135
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.
@@ -203,29 +227,29 @@ Given that there is some friction between protocols with associated types ("gene
203227

204228
- Protocols may not capture generic type parameters:
205229

206-
Firstly, even if we wanted to do this via some sort of implicit associated type, as mentioned above we couldn't represent the existential in the parent. Secondly, there is a concern about parameterised protocols.
230+
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. Secondly, there is a concern about parameterised protocols. So expect an error:
207231

208232
```swift
209233
struct MyType<X> {
210234
protocol MyProto {
211-
var content: X { get set } // ERROR: Cannot capture 'X' from MyType
235+
var content: X { get set } // ERROR: Cannot capture 'X' from MyType<X>
212236
}
213237
var protoInstance: MyProto
214238
}
215239
```
216240

217241
- Structural types *may* capture generic type parameters, but not through a protocol
218242

219-
Structural types can already have nested structural 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 notable:
243+
Structural types can already have nested structural 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:
220244

221245
```swift
222246
struct Top<X> {
223247
protocol Middle {
224248
enum Bottom {
225-
case howdy(X) // ERROR: Cannot capture 'X' from Top
249+
case howdy(X) // ERROR: Cannot capture 'X' from Top<X>
226250
}
227251

228-
var bottomInstance : Bottom { get } // If it _was_ allowed, this reference would also capture 'X'
252+
var bottomInstance : Bottom { get } // Would require capturing 'X'
229253
}
230254
}
231255
```
@@ -234,41 +258,22 @@ Given that there is some friction between protocols with associated types ("gene
234258

235259
- Structual types may not capture associated types
236260

237-
This is quite a common pattern that we find in the standard library, particularly for the dyanmic-'Self' associated type. For example: `Collection.Slice`, `LazyCollection`. This can lead to some really awkward-named types, like `RangeReplaceableBidirectionalSlice`...
261+
Consider the `RandomAccessCollection.Concurrent` example from before, if it were allowed to capture associated types from its enclosing protocol:
238262

239263
```swift
240264
// Note: Pretend there is something called 'Parent' which is a captured 'Self' of the parent protocol.
241-
protocol Collection {
265+
protocol RandomAccessCollection {
242266

243-
struct Slice: Collection {
244-
typealias Element = Parent.Element // ERROR: Cannot capture 'Element' from Parent
245-
typealias Index = Parent.Index // ERROR: Cannot capture 'Index' from Parent
246-
init(from: Index, to: Index, in: Parent) { /* ... */ } // ERROR: etc...
267+
struct Concurrent: RandomAccessCollection {
268+
typealias Element = Parent.Element
269+
typealias Index = Parent.Index
270+
init(with: Parent) { /* ... */ }
247271
}
248-
249-
associatedtype SubSequence: Sequence = Slice
272+
var concurrent: Concurrent { return Concurrent(self) }
250273
}
251274
```
252275

253-
By capturing an associated type, the type `Collection.Slice` would also become existential (something like `Collection.Slice where Parent == Array`). We could theoretically map the capture of 'Parent' in to a generic parameter (although it is _not a part of this proposal_). This pattern is currently prevalent in the standard library, but they're not yet nested, because we couldn't nest structs inside protocols before.:
254-
255-
```swift
256-
protocol Collection {
257-
258-
struct Slice<Base: Collection>: Collection { // generic param _could_ be implicit, hidden
259-
typealias Index = Base.Index
260-
typealias Element = Base.Element
261-
262-
init(base: Base, bounds: Range<Slice.Index>) { /* ... */ }
263-
}
264-
265-
associatedtype SubSequence: Sequence = Slice<Self> // In the source of the capture, Slice would then imply Slice<Self>
266-
}
267-
268-
let slice: Array<Int>.SubSequence = Collection.Slice<Array<Int>>(base: [1, 2, 3, 4, 5], bounds: 0..<2)
269-
```
270-
271-
This would only work for would-be captures from the immediate parent, before we start having protocols capturing associated types. So it's best to leave this idea for now and approach it again when we have a more comprehensive solution.
276+
By capturing associated types, the type `RandomAccessCollection.Concurrent` would also become existential (something like `RAC.Concurrent where Parent == Array<Int>`). Consider if we mapped the capture of 'Parent' in to a generic parameter automatically (like `Concurrent` used to be, earlier in this document), but the compiler did that automatically. This kind of capturing between nesting types would be valuable, but it is _not a part of this proposal_. That is because it 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.
272277

273278
```swift
274279
protocol Top {
@@ -277,11 +282,11 @@ Given that there is some friction between protocols with associated types ("gene
277282
protocol Middle {
278283
associatedtype AssocMiddle
279284

280-
enum Result { // implicit: <Parent: Middle, Parent_Parent: Top>
281-
case one(Parent.AssocMiddle)
282-
case two(Parent_Parent.AssocTop)
285+
enum Result { // implicit: Result<Parent: Middle, Parent_Parent: Top>
286+
case one(AssocMiddle) // implicit: Parent.AssocMidle
287+
case two(AssocTop) // implicit: Parent_Parent.AssocTop
283288
}
284-
var result: Result { get } // implicit: Result<Self, ???> - would need to capture 'Self' from Parent
289+
var result: Result { get } // implicit: Result<Self, ???> - would need to capture 'Self' from Parent
285290
}
286291
}
287292
```
@@ -290,91 +295,22 @@ That's a long explanation of why it's best to just bar any kind of capturing bet
290295

291296
## Source compatibility
292297

293-
This change is mostly additive, although there are several places in the standard library where we can organise things better after this change. Specifically:
294-
295-
- The `FloatingPoint{Sign,Classification,RoundingMode}` enums will become members of the `FloatingPoint` protocol
296-
- The `MirrorPath` protocol will become a member of the `Mirror` struct, and renamed `Path`
297-
298-
- Lots of types in the global namespace will become nested to `IteratorProtocol`, `Sequence`, or some type of `Collection`
299-
**Special instances:**
300-
```swift
301-
// Empty
302-
EmptyIterator<T> /* --> */ IteratorProtocol.Empty<T>
303-
EmptyCollection<T> /* --> */ Collection.Empty<T>
304-
305-
// Single instance
306-
IteratorOverOne<T> /* --> */ IteratorProtocol.One<T>
307-
CollectionOfOne<T> /* --> */ Collection.One<T>
298+
Standard library changes making use of this feature will be part of another proposal.
308299

309-
// Default indexes
310-
DefaultBidirectionalIndices<T: BidirectionalCollection> /* --> */ BidirectionalCollection.DefaultIndices<T>
311-
DefaultRandomAccessIndices<T: RandomAccessCollection> /* --> */ RandomAccessCollection.DefaultIndices<T>
312-
```
300+
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:
313301

314-
**Parameterized views:**
315302
```swift
316-
// Enumerated
317-
EnumeratedIterator<T: IteratorProtocol> /* --> */ IteratorProtocol.Enumerated<T>
318-
EnumeratedSequence<T: Sequence> /* --> */ Sequence.Enumerated<T>
319-
320-
// Joined
321-
JoinedIterator<T: IteratorProtocol> /* --> */ IteratorProtocol.Joined<T>
322-
JoinedSequence<T: Sequence> /* --> */ Sequence.Joined<T>
323-
324-
// Reversed
325-
ReversedCollection<T: Collection> /* --> */ Collection.Reversed<T>
326-
ReversedRandomAccessCollection<T: RandomAccessCollection> /* --> */ RandomAccessCollection.Reversed<T>
327-
328-
// Flattened
329-
FlattenIterator<T: IteratorProtocol> /* --> */ IteratorProtocol.Flattened<T>
330-
FlattenSequence<T: Sequence> /* --> */ Sequence.Flattened<T>
331-
FlattenCollection<T: Collection> /* --> */ Collection.Flattened<T>
332-
FlattenBidirectionalCollection<T: BidirectionalCollection> /* --> */ BidirectionalCollection.Flattened<T>
333-
334-
// Sliced
335-
Slice<T: Collection> /* --> */ Collection.Slice<T>
336-
Bidirectional{*}Slice<T: BidirectionalCollection> /* --> */ BidirectionalCollection.{*}Slice<T>
337-
RandomAccessSlice<T: RandomAccessCollection> /* --> */ RandomAccessCollection.Slice<T>
338-
RangeReplaceable{*}Slice<T: RangeReplaceableCollection> /* --> */ RangeReplaceableCollection.{*}Slice<T>
339-
Mutable{*}Slice<T: MutableCollection> /* --> */ MutableCollection.{*}Slice<T>
303+
@deprecated("Use UITableView.Delegate instead")
304+
typealias UITableViewDelegate = UITableView.Delegate
340305
```
341306

342-
**Lazy views:**
343-
```swift
344-
LazySequence<T: Sequence> /* --> */ Sequence.Lazy<T>
345-
LazyCollection<T: Collection> /* --> */ Collection.Lazy<T>
346-
LazyBidirectionalCollection<T: BidirectionalCollection> /* --> */ BidirectionalCollection.Lazy<T>
347-
LazyRandomAccessCollection<T: RandomAccessCollection> /* --> */ RandomAccessCollection.Lazy<T>
348-
349-
LazyFilterIterator<T: IteratorProtocol> /* --> */ IteratorProtocol.LazyFilter<T>
350-
LazyFilterSequence<T: Sequence> /* --> */ Sequence.LazyFilter<T>
351-
LazyFilterCollection<T: Collection> /* --> */ Collection.LazyFilter<T>
352-
LazyFilterBidirectionalCollection<T: BidirectionalCollection> /* --> */ BidirectionalCollection.LazyFilter<T>
353-
LazyFilterRandomAccessCollection<T: RandomAccessCollection> /* --> */ RandomAccessCollection.LazyFilter<T>
354-
355-
LazyMapIterator<T: IteratorProtocol, E> /* --> */ IteratorProtocol.LazyMap<T, E>
356-
LazyMapSequence<T: Sequence, E> /* --> */ Sequence.LazyMap<T, E>
357-
LazyMapCollection<T: Collection, E> /* --> */ Collection.LazyMap<T, E>
358-
LazyMapBidirectionalCollection<T: BidirectionalCollection, E> /* --> */ BidirectionalCollection.LazyMap<T, E>
359-
LazyMapRandomAccessCollection<T: RandomAccessCollection, E> /* --> */ RandomAccessCollection.LazyMap<T, E>
360-
```
361-
362-
Source migration can be handled with a typealias and deprecation notice (with fixit), for example:
363-
364-
```swift
365-
@deprecated("Use FloatingPoint.Sign instead")
366-
typealias FloatingPointSign = FloatingPoint.Sign
367-
```
368-
369-
It is also 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 handled as above, with a deprecated typealias.
370-
371307
## Effect on ABI stability
372308

373-
Would change the standard library interface, platform SDK interfaces.
309+
This proposal is only about the language feature, but it is likely to result in standard library and platform SDK changes.
374310

375311
## Effect on API resilience
376312

377-
Since all capturing is disallowed, this type of nesting would only change the name (both in source and symbolic) of the relevant types.
313+
Since all capturing is disallowed, this type of nesting would only change the name (in source and symbolic) of the relevant types.
378314

379315
## Alternatives considered
380316

0 commit comments

Comments
 (0)