[Pitch 2] Light-weight same-type requirement syntax

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. :thinking:

Please provide clarifications on that.

3 Likes