Conditional conformance collisions


(Tony Allevato) #1

I ran into a potentially nasty issue with conditional conformances recently. I was importing the SourceKit-LSP code into our (Google's) monorepo so I could set it up building with Bazel; it depends on SwiftPM, which we already have imported and also build from source. The version of SwiftPM we have is tracking fairly close to HEAD, so it's moved beyond the last 0.3.0 release. Let's assume that rolling our copy of SwiftPM back to 0.3.0 is not possible.

SourceKit-LSP extends stdlib's Range: Codable where Bound == Position (Position is another type defined by SourceKit-LSP). SourceKit-LSP also depends on SwiftPM's Utility target, which depends on Basic, which provides a different but conflicting conditional conformance: Range: Codable where Bound: Codable.

SourceKit-LSP currently depends on SwiftPM 0.3.0 which doesn't have the conformance, but the next release will. Trying to build SourceKit-LSP against SwiftPM HEAD today produces the following error:

Sources/LanguageServerProtocol/Position.swift:44:18: error: conflicting conformance of 'Range<Bound>' to protocol 'Decodable'; there cannot be more than one conformance, even with different conditional bounds
extension Range: Codable where Bound == Position {
                 ^
Basic.Range<Bound>:1:11: note: 'Range<Bound>' declares conformance to protocol 'Decodable' here
extension Range : Codable where Bound : Comparable, Bound : Decodable, Bound : Encodable {
          ^

What's worse is that the conformances use different stringValues for their CodingKeys, so they're not runtime compatible—cooperatively pushing the conformance down to the lowest dependency wouldn't solve this. We could talk about various approaches to fixing the problem for SK-LSP specifically (e.g., introduce a separate type for text ranges), but that just dodges what seems like a tricky situation with conditional conformances in general.

Some random thoughts:

  • These conformances seem like they were added to be used in the project itself but are not necessarily useful as exported public API. However, since requirements must have the same visibility as the protocol itself, there's no way to avoid exporting a conditional conformance to a public protocol to all the targets that depend on you.

  • You can be broken by anyone in your build graph, not just your direct dependencies. In the case above, a dependency of a dependency of SK-LSP added a conformance that breaks it, and does so in a way that it could not know about and seemingly cannot recover without one of the two modules redesigning its types in a major way.

  • The two conformances in question seem like they could be prioritized; one is a protocol constraint Bound: Codable, and one is a concrete type constraint Bound == Position. Could the most-specific one win? (This is probably a disaster for conformance checking and dispatching to the correct implementation.) This wouldn't fix the problem everywhere but would probably reduce occurrences, especially where libraries add conditional conformances involving types that they define.

Unfortunately I have a lot of questions and not many answers, but has this issue come up before? It seems similar to other scenarios I've seen discussed (like operator declarations across modules), but I couldn't find anything specifically about conditional conformances.


Conformance of Range to Codable may cause problems for libSwiftPM clients
Conformance of Range to Codable may cause problems for libSwiftPM clients
(Ben Langmuir) #2

This particular conformance is a bit evil and I feel bad for adding it. We should probably use our own concrete type for this so that we can control serialization.


(Tony Allevato) #3

For SourceKit-LSP specifically, that might be the safest approach (although I think it's also reasonable to say that there are certain ergonomics that you get for free if you use Range that it would be unfortunate to lose or have to replicate).

What concerns me more (and why I raised it in the general forum) is the underlying problem separate from SK-LSP—we encourage this kind of retroactive conformance (conditional and otherwise), so extending Range as you did feels like a perfectly reasonable "Swifty" thing to do, but things can fall apart due to reasons entirely outside your control. I'm not sure what the answer is here, though.


(Matthew Johnson) #4

This is definitely a known behavior. It doesn't really have anything to do with conditional conformances - you can end up with conflicts any time a library adds a conformance where it does not declare either the conforming type or the protocol. IMO a library that adds such a conformance is a bad citizen. A wrapper type or other alternative design should be used. It makes code clunkier, but ensures that users of the library do not run into this issue.

@Joe_Groff has talked about enhancements to the type system that would be able to distinguish "conflicting" conformances like this in the type system. That may alleviate this issue in the future, but for now we just need to be aware of it and take responsibility as library authors to ensure we do not contribute to problems for our users.


Conformance of Range to Codable may cause problems for libSwiftPM clients
(Tony Allevato) #5

Ah, thanks for reminding me of that. I thought the conditional part of it was complicating the matter, but you're right—this happens with regular retroactive conformances as well. (A sample I came up with to test it didn't catch it at first because my imports were wrong originally.)


(Ben Rimmington) #6

There's also a proposal and implementation, which currently uses unkeyed containers.


(Jordan Rose) #7

See also Retroactive Conformances vs. Swift-in-the-OS, which I keep meaning to circle back to and then don't find the time for.


(Cory Benfield) #8

Agreed. SwiftNIO explicitly takes the position that either conforming one of our types to a stdlib protocol, or conforming a stdlib type to our protocol, is a SemVer minor change: that is, it is not considered source breaking. This is despite the fact that users may in fact have their source code broken if they (or, more likely, one of their dependencies) have written such a conformance themselves.

This is simply the only way to handle the fact that extensions are extremely powerful in Swift. Extending types you do not own with protocol conformances to protocols you do not own puts you at risk of breakage.


Conformance of Range to Codable may cause problems for libSwiftPM clients
(Daniel Dunbar) #9

I have not tried to work through the consequences of using wrapper types to avoid this, but it seems like it might be so painful as to be somewhat untenable in practice.

Is the right answer, as a start, that the stdlib should try hard to define conformances for compositions of types inexports? That also feels risky and bloats the standard library.

While inconvenient, could some of these conformance be relegated to external packages that could try and be the “authoritative” source of them (vending little else), to try and avoid the issue?


(Ankit Aggarwal) #10

I would argue that compiler should provide a way to disallow extending types that you don’t own in library modules. Not everyone is aware of this collision problem until they run into it. This problem will only grow with Swift’s package ecosystem. The restriction can lifted once we have a proper solution for this problem. Otherwise, we’ll just keep seeing this kind of collisions in the package ecosystem.

Note that having private conformance is also not a solution because libraries can be made up to multiple modules. Maybe the private import feature can help there?


(Xiaodi Wu) #11

With respect to conditional conformances specifically, I'd argue that we can be more conservative. The thing to be discouraged, specifically, is conforming a type you don't own to a protocol you also don't own.

If we'd like, we can be even more conservative in forbidding this only in the specific case where the type and protocol are exported by the same module (e.g., forbidding extension T: P where T and P are both exported by the standard library or both exported by Foundation). This would permit standalone modules to vend extensions that 'glue' together disparate frameworks.


(Dale Buckley) #12

I did the work on this and any changes that were raised, but have no idea what the state of it going any further is. I don't know if it's come down to time constrains of the core team or something else :man_shrugging:t2:


(Ankit Aggarwal) #13

I think I like this approach for conditional conformance. Your second suggestion about gluing together frameworks by random module also has the same risks, I think. Imagine if a random package extends some type in a famous package.


(André Videla) #14

I think this could be solved by having names extensions which would allow a single type to conform to the same protocol multiple times. (Idris does this with names implementation of interfaces http://docs.idris-lang.org/en/latest/tutorial/interfaces.html#named-implementations)

This topic already came up in a different context than conditional conformances. It is possible that conditional conformance and conforming using types outside the control of the library owner bring additional motivation for such a feature.


(Gwynne Raskind) #15

I think this could be solved by having names extensions which would allow a single type to conform to the same protocol multiple times.

Doesn't that effectively bring us full circle to Objective-C categories and all the slings and arrows that flesh was heir to? We'd end up wanting to be able to spell "use the extension named X" or "use the extension from the module named Y" somehow, at which point the problem has just been pushed up one level in the syntax. Explicit disambiguation of which module's extension to call has already been brought up in the past as a possible solution to this problem, and I for one wouldn't care to see Swift sprout C++-like scope resolution (constructions like fooObject->TheDiamondPatternIsBad::commonVirtualBaseMethod() make me shiver). How would you reconcile multiple conformances to protocols that the compiler calls into on its own, such as Equatable, Comparable, or even more nightmarishly, Codable?


(Joe Groff) #16

That scope resolution already exists, the compiler just hides it from you. Every time you need a protocol conformance, it's really an independent parameter of the generic context being instantiated, and the compiler fills in the corresponding argument implicitly by picking the protocol conformance it has available. The conformances have names in the sense they have unique identities, it's just currently not possible to explicitly spell them in the surface syntax. The underlying implementation supports having multiple conformances of the same type to the same protocol, since they are modeled as independent entities.