Some thought on why I still prefer a marker on the declaration side.
Consider an evolving protocol:
protocol AsyncSequence {
associatedtype AsyncIterator: AsyncIteratorProtocol
associatedtype Element where Self.Element == Self.AsyncIterator.Element
func makeAsyncIterator() -> Self.AsyncIterator
}
protocol AsyncIteratorProtocol {
associatedtype Element
mutating func next() async throws -> Self.Element?
}
After primary associated types become a thing, the author of the protocol decides to extend this functionality with that feature.
Does the author need to #if
around the entire protocol? Let's assume so.
#if condition
protocol AsyncSequence<Element> where Element == AsyncIterator.Element {
associatedtype AsyncIterator: AsyncIteratorProtocol
func makeAsyncIterator() -> Self.AsyncIterator
}
#else
// old version
#endif
protocol AsyncIteratorProtocol {
associatedtype Element
mutating func next() async throws -> Self.Element?
}
Okay users of that protocol start to write some AsyncSequence<Element>
everywhere.
Great, let's consider that those protocols could be extended with a Failure: Error
associated type in the future where Swift has typed throws. Similarly to how some already used the some Publisher<A, ConcreteError>
example I would expect something similar to happen with AsyncSequence
.
Do we need to do yet another #if
dance here?
#if condition
// can properly mark this extension at all?
protocol AsyncSequence<Element, Failure: Error>
^~~~~~~~~~~~~~ is this extension even legal, ABI compatible?
where
Element == AsyncIterator.Element,
Failure == AsyncIterator.Failure
{
associatedtype AsyncIterator: AsyncIteratorProtocol
func makeAsyncIterator() -> Self.AsyncIterator
}
protocol AsyncIteratorProtocol {
associatedtype Element
associatedtype Failure: Error
mutating func next() async throws<Failure> -> Self.Element?
}
#else
#if previous_condition
// previous version
#else
// old version
#endif
protocol AsyncIteratorProtocol {
associatedtype Element
mutating func next() async throws -> Self.Element?
}
#endif
While we're at it: Does this break existing code such as some AsyncSequence<Element>
, because of the sudden requirement of a secondary primary associated type?
I think a pure marker on the associated type wouldn't suffer from all this gigantic #if
dance.
protocol AsyncSequence {
@mark_marker_availabitlity
marker
associatedtype Element where Self.Element == Self.AsyncIterator.Element
@available(...)
associatedtype Failure: Error where Self.Failure == Self.AsyncIterator.Failure
associatedtype AsyncIterator: AsyncIteratorProtocol
func makeAsyncIterator() -> Self.AsyncIterator
}
protocol AsyncIteratorProtocol {
associatedtype Element
@available(...)
associatedtype Failure: Error
mutating func next() async throws<Failure> -> Self.Element?
}
That's where this example originated. I also would like to know if it's considered a breaking change to exposing a set of primary associated types and adding another one in another library iteration.
// today
protocol P {
primary associatedtype A
associatedtype B
}
some P<ConcreteA>
// future
protocol P {
primary associatedtype A
primary associatedtype B
}
// does this break?
some P<ConcreteA>
It seems to me that if the some P<ConcreteA>
part was written with a non-sugar general form, it would still function just fine, however the special 'primary' syntax seems to lead us into a 'require and break' corner.
Please provide clarifications on that.