Does the order of P1 and P2 matter in: extension P1 where Self: P2?

Given:

protocol P1 { /* ... */ }
protocol P2 { /* ... */ }

does the order of P1 and P2 in:

extension P1 where Self: P2 {
    // ...
}

and:

extension P2 where Self: P1 {
    // ...
}

matter (at all)?

Oh, never mind, I realized just after posting: Yes of course the order matters, one is extending P1, and the other is extending P2 ...

1 Like

Semantically there's no real difference, since the members will be available on any type conforming to (P1 & P2) either way. Documentation generators and other tools will however take the primary protocol being extended as a hint for whether the contents of the extension ought to be categorized as members of P1 or P2.

That was my first thought too. But then I realized that there are (afaict valid) reasons for why this

extension P1 where Self: P2 {
    // Identical Content
}

might produce an error, while this

extension P2 where Self: P1 {
    // Identical Content
}

might compile successfully and work as expected.

Here's the shortest example I could come up with while trying to reduce my real world scenario (which doesn't include Array or Collection):

protocol P {
    associatedtype A: Collection where A.Element == Self
}
extension Collection where Self: P {
    typealias A = Array<Self>
}

The above compiles successfully, but swapping Collection and P in the extension will result in compile time errors.


Details

collection_first.swift:

protocol P {
    associatedtype A: Collection where A.Element == Self
}
extension Collection where Self: P {
    typealias A = Array<Self>
}

p_first.swift:

protocol P {
    associatedtype A: Collection where A.Element == Self
}
extension P where Self: Collection {
    typealias A = Array<Self>
}

I can reproduce the behavior using both the default toolchain of Xcode 9.4 and the most recent snapshot:

› swiftc --version
Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
Target: x86_64-apple-darwin17.5.0
›
› /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2018-06-05-a.xctoolchain/usr/bin/swiftc --version
Apple Swift version 4.2-dev (LLVM 031e148970, Clang b58a7ad218, Swift 0747546bd7)
Target: x86_64-apple-darwin17.5.0

collection_first.swift compiles successfully:

› swiftc collection_first.swift 
› 
› /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2018-06-05-a.xctoolchain/usr/bin/swiftc collection_first.swift 
› 

p_first.swift fails to compile with the following errors (which are slightly different for the two versions of the compiler):

› swiftc p_first.swift 
p_first.swift:2:40: error: type alias 'A' references itself
    associatedtype A: Collection where A.Element == Self
                                       ^
p_first.swift:5:15: note: type declared here
    typealias A = Array<Self>
              ^
p_first.swift:2:40: error: 'A' is ambiguous for type lookup in this context
    associatedtype A: Collection where A.Element == Self
                                       ^
p_first.swift:2:20: note: found this candidate
    associatedtype A: Collection where A.Element == Self
                   ^
p_first.swift:5:15: note: found this candidate
    typealias A = Array<Self>
              ^
p_first.swift:2:50: error: same-type requirement makes generic parameter 'Self' non-generic
    associatedtype A: Collection where A.Element == Self
                                                 ^
› 
›
› /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2018-06-05-a.xctoolchain/usr/bin/swiftc p_first.swift 
p_first.swift:2:40: error: type alias 'A' references itself
    associatedtype A: Collection where A.Element == Self
                                       ^
p_first.swift:5:15: note: type declared here
    typealias A = Array<Self>
              ^
p_first.swift:2:40: error: 'A' is ambiguous for type lookup in this context
    associatedtype A: Collection where A.Element == Self
                                       ^
p_first.swift:2:20: note: found this candidate
    associatedtype A: Collection where A.Element == Self
                   ^
p_first.swift:5:15: note: found this candidate
    typealias A = Array<Self>
              ^
› 

Note that the same-type requirement is not necessary to reproduce the behavior, Self can be replaced with eg Int and the two variants of the program will still demonstrate this behavior.

This example is perhaps too reduced and contrived to show why I think there might be valid reasons rather than a compiler bug behind this. But I might well be wrong.

Maybe I can post a better example later, unless someone can bring clarity to this before that?

I guess your issue is related to Reconsider how type aliases are used within protocols and their extensions

I'm not sure there if there is an issue or not, regarding the examples of this thread. They are at least not similar to the issue in the specific post you linked to. But I assume that if the change mentioned in this post (of the same thread) is made, then the above example should compiler no matter the order, right @anthonylatsis?

Regardless of the details, I would say the order dependency here is a bug. It ought to behave consistently independent of the ordering of constraints.

3 Likes

This is rather a bug regardless of whether we have default specifying behavior for type aliases or not. But! What causes the bug with the associated type might very likely be an intended decision made before we had where clauses on associated types implemented, which can reference themselves. A declaration in a protocol must not conflict with any declaration in extensions. Here are two shorter examples to illustrate the same problem – referencing A.

protocol P {
  associatedtype A where A: P
}
extension P {typealias A = Bool}
protocol P {
  associatedtype A
  func foo() -> A
}
extension P {typealias A = Bool}

then the above example should compiler no matter the order, right @anthonylatsis?

Well, the idea is to get that fixed along the way. But I am not sure anymore whether it's a bug or a feature (namely the second example). I'll have to ask and if it ends up being a bug then we just fix that and continue thinking about type aliases in protocols.

1 Like

My interpretation of the above (and SE-0092) is that

  • Types conforming to P are required to have an associated type A.
  • The extension defines a type alias for Bool, also called A, that all conforming types will have.

So there are two separate things which both happen to be called A here (an associated type requirement and a type alias). And that's why we get the following compile error:

protocol P {
    associatedtype A
    func foo() -> A // ERROR: 'A' is ambiguous for type lookup in this context
}
extension P { typealias A = Bool }

which can be resolved as referring to the associated type like this:

protocol P {
    associatedtype A
    func foo() -> Self.A
}
extension P { typealias A = Bool }

But there doesn't seem to be any way to refer to the type alias.

And if I try to conform a type to it, it's impossible to have the associated type A to be anything but Bool:

protocol P {
    associatedtype A
    func foo() -> Self.A
}
extension P { typealias A = Bool }

struct ThisWorks : P {
    func foo() -> Bool {
        return true
    }
}

struct ThisFails : P { // ERROR: Type 'ThisFails' does not conform to protocol 'P'
    typealias A = Int
    func foo() -> Int {
        return 123
    }
}

struct AsDoesThis<A> : P { // ERROR: Type 'AsDoesThis<A>' does not conform to protocol 'P'
    func foo() -> A {
        fatalError()
    }
}

But perhaps this behavior is to be expected if you decide to call both an associated type and a type alias A.

EDIT:

struct ButThisWorks<A> : P {
    func foo() -> ButThisWorks.A {
        fatalError()
    }
}
1 Like

Honestly, I think the proper thing to do is to simply not let a protocol's associated type and a type alias in an extension of that protocol have the same name. I have no problem with having type aliases in protocols, but unresolvable (or nearly so) name collisions are bad.

As @Nobody1707 said.

That's a fine restriction at a single file or module level, but is going to be tricky across modules and when considering code evolution.

You can't retrospectively introduce an associated type:

  1. You can't have associated type in extensions.
  2. If during code evolution you add an associated type you are breaking a ton of code anyway.

Therefore the only ways the ambiguity arrises are:

  1. A protocol declaration with both an alias and an associated type of the same name.
  2. Introducing a typealias in an extension that clashes with an already existing associated type.

Both these cases are easy to diagnose. Therefore I think the suggestion of banning using the same name is a good one.

1 Like

Sure, the latter 2) is exactly what I had in mind. It's nothing to do with being easy to diagnose, it's that you might have a dependency graph and one of your dependencies adds an associated type and it conflicts with a typealias in another dependency. I was just saying that it needs some thought so it fits in with the Swift themes of resilience and library evolution.

Going back to the specific topic of this thread: If the order of P1 and P2 is intended to be truly irrelevant in:

extension P1 where Self: P2 { /* ... */ }

then perhaps it would be more clear if this could be expressed like this:

extension P1 & P2 { /* ... */ }

Unless there are reasons why that wouldn't make sense?


For more than two protocols, it would simply be:

extension P1 & P2 & P3 { /* ... */ }

which in today's syntax has to be:

extension P1 where Self: P2 & P3 { /* ... */ }

or, if acting as if & wasn't allowed in this context either:

extension P1 where Self: P2, Self: P3 { /* ... */ }

EDIT:

From my naive perspective, and despite being aware of the Dunning-Kruger effect, I have to say that It seems like much of the stuff related to type aliases, associated types, extensions (constrained, conditional etc) is an entangled mess that can't be tackled piecemeal / iteratively. If decisions and changes will be based solely on Swift Evolution and core team discussions without a common understanding of the intended over-all system, it seems to me like this mess can at best change shape, if not increase.

2 Likes

Though there's no intended difference from a language usage standpoint, the main issue is the question of how to present the extension members in documentation. The protocol that is the subject of the extension is the one that the members will be documented under.

1 Like

Slightly off topic, but related to documentation of constrained extension members:

SR–7512 “Documentation omits required conformances” points out that sort() is defined on MutableCollection where Self: RandomAccessCollection, Element : Comparable, but its documentation (which is taken verbatim from the doc-comment in the standard library) states “You can sort any mutable collection of elements that conform to the Comparable protocol by calling this method.”

That is, the documentation ignores the RandomAccessCollection constraint. I have not done an exhaustive search, so there could potentially be other things in the standard library for which the documentation misstates when it is available—and I am reasonably confident that there are many places where the documentation does not mention the constraints at all.

What *should* the documentation say for constrained extension members?

2 Likes

In my view, these extensions should be documented under all of the protocols involved, with the additional constraints clearly visible. Given, as you say, that there is no other distinction intended, there is no a-priori reason for a user to look in one place but not another for the documentation.

2 Likes

You know, considering conditional extensions are kind of commutative,

extension P2 where Self: P1 {...} 
extension P1 where Self: P2 {...} // Equivalent

this type of existential sugar is a not a bad idea and suites extensions in general. Besides, these 'non-nominal type' extensions can safely and consistently be nominal type extensions under the hood.

extension P1 & P2  ->  extension P1 where Self: P2
extension P2 & P1  ->  extension P2 where Self: P1