Let's start with the following definition...
Retroactive conformance: extending a class, struct, or enum from another module to conform to a protocol from another module. The protocol and the conforming type may be from the same module or from different modules; the important note is that neither is in the same module as the extension.
Hopefully uncontroversial. Retroactive conformances are a useful feature, mainly because (as Dave A has said in the past) they allow you to take types from one library and protocols from another library and make them work nicely together.
However, retroactive conformances suffer from the "What if two people did this?" problem:
// Framework A
extension SomeStruct: CustomStringConvertible {
var description: String {
return "SomeStruct, via A"
}
}
// Framework B
extension SomeStruct: CustomStringConvertible {
var description: String {
return "SomeStruct, via B"
}
}
// main.swift
import A
import B
print(SomeStruct()) // ???
If framework A imported framework B or vice versa, the compiler would be able to complain that there might be a conflict, but as is there's no way to do so in advance. And there's not really anything that the actual app can do about it either in this case.
Today, we just allow this to happen, with the Swift runtime deterministically picking one framework's conformance to "win". I believe this is based on the order the dynamic libraries get loaded or the static libraries get linked together, i.e. not something you really want to be depending on!
So, retroactive conformances do have a flaw. The fully safe rule would be to disallow them completely, or only allow them in the app target (and cross our fingers about any possible dynamically-loaded plugins). But they're a little too useful for that---you wouldn't be able to make a Swift package that combines two libraries together, for example.
However...the situation is even worse with libraries in the OS. Imagine this scenario:
// CoreKit overlay in iOS 15
public struct SomeStruct { … }
// main.swift
import CoreKit
extension SomeStruct: CustomStringConvertible {
var description: String {
return "SomeStruct, via my app"
}
}
print(SomeStruct())
Everything's fine, right? Until the OS update.
// CoreKit overlay in iOS 16
public struct SomeStruct { … }
// A non-retroactive conformance! But one with availability.
// We don't actually support this syntax yet, but we're going
// to need something like it.
extension SomeStruct:
@available(iOS 16, *) CustomStringConvertible {
var description: String {
return "SomeStruct, via CoreKit itself"
}
}
Funny new syntax aside, we have a problem. The already-compiled version of my app is going to run on iOS 16 and expect to use its own implementation of CustomStringConvertible. But the one in the OS will win, because it's non-retroactive. Worse, if I recompile my app to support iOS 16 and remove my own implementation of CustomStringConvertible, it won't behave correctly on iOS 15!
Unless and until we come up with a better answer here, I propose the following rule:
It is an error in Swift 5 (warning in Swift 4 mode) to declare a retroactive conformance if both the protocol and the conforming type are from "resilient" libraries or system frameworks.
That "resilient" refers to "libraries that may be swapped out without recompiling clients". In Swift, such libraries are compiled with extra indirection in their ABI in order to handle future changes. We haven't formalized this feature yet, so for now you can read "resilient libraries" as "the standard library and SDK overlays". The "system frameworks" part is thrown in to account for Objective-C code, which has the same problem but no formal distinction between "libraries that may be swapped out without recompiling clients" and "libraries whose exact version is known by the client".
What do people think? It kind of stinks, but it definitely sidesteps these problems.
P.S. Once we've worked this out, there's a bonus problem involving class inheritance. I'll bring that up later, though.
Appendix: Co-existing conformances
A few other Apple people have pointed out that it would actually be possible to support conflicting conformances, except for in dynamic casts and features that use them (like print
). This is because when a conformance is used at compile time, the compiler knows exactly where to find it, and it can be sure to continue using that implementation even if another one appears at run time in another module. However, it does complicate the language and runtime a little to support these "compile-time-only" conformances, and it still doesn't solve the problem when you do want to make the conformance available for dynamic casting, like the CustomStringConvertible example above.
Another option would be to come up with an attribute that indicates that a conformance is a "fallback". This would slightly slow down regular code that uses the conformance, as it would start off with a dynamic lookup for a non-fallback implementation, but that result could be cached. Again, though, that's making the language more complicated when we're not yet sure we have a need to.
Possibly interested Apple people: @Joe_Groff, @Douglas_Gregor, @dabrahams, @Slava_Pestov