Retroactive Conformances vs. Swift-in-the-OS

If you can only pick one conformance for the entire process, the non-retroactive one is the only uniquely-identifiable one, and it's the one that matches the intent of the type or protocol owner. If you allow multiple conformances, then yeah, we could do something more like "closest". But we'd have to be very careful to define what that means, especially when it comes to dynamic casts. I don't see good things down that direction.

6 Likes

That's the intended runtime model, but Jordan is right that the compiler doesn't always carry around the right conformance in all circumstances, and to @Jon_Shier's point, there's also no syntax for explicitly referring to a specific conformance or its members with qualified names.

1 Like

If I'm understanding this correctly, this seems like exactly the same problem as orphan instances in Haskell. Afaik, the consensus there is that they're usually best avoided although this is not enforced. However, GHC (the most common Haskell compiler) will raise a warning in such a case.

2 Likes

I don’t know what we should do about the scenario described in the original post, but I think it needs to be part of a larger discussion about resilient modules and our story for library evolution.

It would be highly unfortunate to prohibit retroactively conforming system-framework types to system-framework protocols. Especially since we would be preventing people from making a useful conformance today, on the grounds that in the future that conformance might be recognized as *so* useful that it would actually become available automatically.

2 Likes

Seems like a good rule for resilient libraries and obj-c frameworks.

For compiled against code I have often wanted some simple way of specifying which one (I have run into this).

I know we've had huge problems with this in the past at Apple with people adding, say, NSCoding conformance to a type, and then getting in trouble when a later Apple OS implements NSCoding in a different way. (See "Why you can’t make someone else's class Decodable".) If this restriction is the "stick", the "carrot" might be the thing that gets talked about every now and then: forwarding a protocol to a particular property, to make it easier to make wrapper types (the "newtype" solution in Haskell, as @yxckjhasdkjh pointed out).

To @Jon_Shier's points, I actually do think we need a way to disambiguate members from different modules, but that doesn't fix the conformance issue. For the rest of it, the sticking point is usually the behavior of dynamic casts. If we didn't have to worry about those, this would all be a lot simpler (see the "appendix" section in the original post).

3 Likes

Would it make sense (as another option) to split the difference here and say: without introducing any new attribute, retroactive conformance of a third-party or resilient type to a third-party or resilient protocol is always a "fallback"?

1 Like

I personally really want the fallback behavior to be explicit, since it means writing your code in such a way that it still works if the implementation changes out from under you, or even behaves differently in concrete and generic contexts. But you're right, that is an option.

1 Like

Would it be at all feasible to have an @available(beforeButNotIncluding: iOS 16) or similar? There's likely numerous problems with this (e.g. requires updating code if an upstream library introduces an @available(...) conformance), but it seems like this may retain some of the valuable flexibility of retroactive conformances.

Incidentally, this is a feature of Swift I've wanted for a while. There are circumstances where I'm interested in adding convenience conformances to types that my module does not own (e.g. conform this abstract C data structure from a system library to Collection so I can work with it more easily). However, I usually come to my senses about halfway through writing the conformance and remember that I'm just begging to have either my code or someone else's break.

This is not a critical limitation, because I can always declare a wrapper data type to allow me to do this. But it would help address part of the issue.

This is also an example of a good general pattern for conforming other people's types: if you want to avoid ambiguity, a good way to do so is to generate a wrapper type that you do own, and conform that type instead.

2 Likes

I guess this is related to this: Best way to implement new feature for old SDKs?
Ideally, solving the problem @jrose describes would also yield a solution to my question/problem.

¯\ _(ツ)_/¯

I've long wanted a way to declare a "weak" conformance in my framework. That is, the compiler recognizes the conformance, but will automatically defer to any other conformances if there is a conflict.

Ultimately, though, we need a way to explicitly choose a winner in cases where the compiler might get it wrong.

1 Like

This is good to break a lot of code without a clear migration path.

If we do go forward with this breakage, we definitely need some way to help people achieve similar functionality. @jrose What is this solution you are talking about? Can you go into more detail? Haskell's newtype just looks like typealias.

Appendix: Co-existing conformances

A reference to prior art for this: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2098.pdf

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.

A reasonable answer to the dynamic casting problem, IMO, is that the cast fails unless one of the following holds:

  • there is only one such conformance in the system or
  • there is only one such conformance visible at the point where the concrete type is converted to the existential protocol type.

Haskell uses newtype to create simple, zero-cost wrapper types. E.g.

newtype Id = Id String

is kind of the same as writing

struct Id {
  let value: String
}

in Swift, only that Haskell will optimize the wrapper type away at runtime; it's purely used for type-safety purposes. There is a somewhat similar library for Swift: GitHub - pointfreeco/swift-tagged: 🏷 A wrapper type for safer, expressive code.

So, in Haskell, people (afaik) say "retroactive conformances" (they have a different name in Haskell, but that's beside the point) are best avoided, so your best bet if you don't own the type and you don't own the protocol is to create a wrapper type that you own and add the conformance for that wrapper type.

4 Likes

FWIW Swift optimizes away the wrapper type too, at least in terms of manipulating values of the type. A struct with one stored property is layout-equivalent to a value of that type.

10 Likes

Oh, didn't know that. Thanks. :)

The former doesn't account for conformances being newly introduced in OS releases, and the latter doesn't handle the String(describing:) case, which deliberately does the check/conversion far away from the definition of the type.

That seems like a principled version of the "fallback" annotation, yeah, and we already have something like it for @available(swift, …) (a static check rather than a dynamic check). I'm a little worried about non-iOS platforms, though. Normal @available has that * in it very deliberately, to say that the declaration is available by default on all other platforms. This one might have to be hidden by default.

IMO this has always struck me as an inadequate workaround. I still want to be able to work with a Set<MyIntWithCustomHashing> as if it were a Collection of Int, not of my wrapper type, but in order to do so, a wrapper type imposes a bunch of mapping in and out of the wrapper. And although Collection nicely provides pre-fab map operations to do that for you, other abstractions may not have a map operation at all. Swift's generics model was designed to accommodate "incoherent" and "orphan" protocol conformances, and in the fullness of time I'd like to see the language design embrace it.

That said, I agree with Jordan's assessment that his proposed rule seems prudent given that we have a lot of compiler implementation and language design work still to do to make such conformances practical to work with, not only theoretically possible to.

1 Like