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)?
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 ...
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.
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 the semantics of type aliases in protocol extensions - #17 by anthonylatsis
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.
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.
My interpretation of the above (and SE-0092) is that
P
are required to have an associated type A
.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()
}
}
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:
Therefore the only ways the ambiguity arrises are:
Both these cases are easy to diagnose. Therefore I think the suggestion of banning using the same name is a good one.
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.
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.
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?
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.
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