Swift regret: retroactive conformances— Jordan Rose (@UINT_MIN) November 21, 2021
These are when you have a type Foo from module A, and a protocol Bar from module B, and you need Foo to conform to Bar. And so you say `extension Foo: Bar`.
Part of the Swift Regrets series.
There are times when you have a type Foo from dependency A, and a protocol Bar from dependency B, and you need Foo to conform to Bar. And so you say
extension Foo: Bar. This is a retroactive conformance, where you own neither the type nor the protocol, and it should not be allowed. It’s super convenient, but it suffers from the “what if two people do this?” problem. I’ve written about this before (skip down to “Retroactive Modeling”).
Swift’s run time model actually supports types conforming to protocols in multiple ways, but there are two problems with actually doing it. First, the language doesn’t have any way to name conformances, so you can have two types
Set<Foo> with different
Foo: Hashable. Second, Swift
as? can dynamically check whether a type conforms to a protocol. Which means there has to be some kind of global registry of conformances, which can’t have duplicate entries. (There have been proposals to say “this conformance is only available statically”, but…)
Even if you ignore all these problems, it’s a source compatibility hazard, a giant hole in the “library-friendly” story from last time. Because some day module A might learn about module B and provide their own conformance.
Sometimes you want this for layering reasons, like cross-import overlays. I sympathize. Maybe we could have carved out a narrow case for that. “I recognize that this other module can add conformances to my types.” But we didn’t.
I’m breaking my Swift Regrets rules a bit because I do feel bitter about this one, that I brought it up but didn’t manage to fix the problem for library evolution mode, at least. But it’s the biggest concrete regret on the list. The next language should not allow retroactive conformances, at least not in the fully general form that Swift does.
Haskell calls these the “orphan instances”. Rust has the “orphan rule” as part of “trait coherence”. Swift should never have allowed arbitrary retroactive conformances no matter how convenient it is.
P.S. What would some alternate designs look like?
- If you own the type it’s safe. (This is the only thing Java allows.)
- If you own the protocol it’s safe. (Rust also allows this.)
- The standard safe workaround is a wrapper type; perhaps Swift could have better tools for working with wrapper types.
- If you promise the compiler that you won’t update either dependency, it’s safe…as long as you’re the only one who does this. Though that doesn’t help you for OS libraries, which can always be updated out from under you.
- If you say you’re okay with your conformance being non-public (and not dynamically discoverable), it’s safe until the same conformance is added in the original module, at which point you have to allow “conformance shadowing” for that not to be source-breaking. (Alternately, you can combine this idea with the previous one.)
If you can explicitly name conformances, and retroactive conformances are always named (and not dynamically discoverable), it’s safe enough. Joe Groff is interested in this design, and Andy Gocke pointed to a related proposal for C#.