How to provide cross-module conformances without running afoul of “no retroactive conformances”?

i keep running into the situation where i have some library type such as

Sources/UUID/UUID.swift

struct UUID
{
    ...
}

and some library protocol such as

Sources/BSON/BSONDecodable.swift

protocol BSONDecodable
{
    ...
}

and i want to conform UUID.UUID to BSON.BSONDecodable. but there only two ways to do this without running afoul of “no retroactive conformances”, that both involve forcing UUID and BSON to depend on each other:

  1. make UUID depend on BSON:
import BSONDecodable

extension UUID:BSONDecodable
{
    ...
}
  1. make BSON depend on UUID:
import UUID

extension UUID:BSONDecodable
{
    ...
}

but this doesn’t seem justified to me, because you should be able to use BSON without depending on UUID, and vice versa.

“cross import overlays” sounded like something that would have solved this problem, but the idea seems to have died in committee years ago.

what is the recommended approach now-a-days?

2 Likes

Usually these sorts of things go in an optional module you vend, like NIO's NIOFoundationCompat module, which can be imported separately by any who needs them. In this case, if BSON is a third party library, it would be best to get such extensions into a module there rather than your own framework. If it's your code then there's no problem.

Personally, I don't think a "no retroactive conformances" rule is generally useful, though that doesn't seem to be your primary issue here. At most a "don't conform types you don't own to protocols you don't own" rule may be useful, but conforming types you don't own to your own protocols doesn't seem like an issue.

5 Likes

If you own either the type or the protocol, the conformance isn’t “retroactive”.

5 Likes

Doesn't the compiler already diagnose redundant conformances?

I think retroactive conformances are really only a problem when SDK types are involved, because they may end up having conformances at runtime which weren't present in the interface you had available when building.

i was going off of the definition used in SE-0364 which considers all external-to-external conformances “retroactive”:

This warning will appear only if all of the following conditions are met, with a few exceptions.

  • The type being extended was declared in a different module from the extension.
  • The protocol for which the extension introduces the conformance is declared in a different module from the extension.

regardless of who wrote the code.

i think the arguments against external-to-external conformances, at least the ones presented in SE-0364, have more to do with broken compilations than broken runtime behavior.

i think this is the route i am going to take because i think the problems that come from external-to-external conformances mostly have to do with conflicting conformances, so having a standard implementation could mitigate that issue.

Ah, yeah, if it’s “separate module but I own one of the upstream modules” you don’t have to worry, good point.

As a fun sidebar, support for cross-import overlays in SwiftPM would be a useful solution to this problem.

Unfortunately, while they're a fun syntactic solution to this problem, they don't solve the big issue which is trying to deal with the dependencies they introduce.

2 Likes

Would canImport work here?

struct UUID { ... }

#if canImport(BSONDecodable)
import BSONDecodable

extension UUID: BSONDecodable { ... }
#endif

I'm not super familiar with how canImport works with SPM packages, so this might not actually work.

The short answer here is "no".