Retroactive Conformances vs. Swift-in-the-OS

We don't allow cyclic imports, so this isn't going to come up quite as you've written it. But yes, that's the behavior I expect for non-retroactive conformances in (1) and (2); having it also be true for retroactive conformances is (5).

(Note that I'm mostly ignoring conflicting retroactive conformances for now, like the one you have here. That seems like an additional level of complexity.)

For me I think the goals should be:

  1. 1 - 8 as is.
  2. For 9, a conformance added in a latter version of the OS that conflicts, I would say that should be an error.
  3. 10 as is.
  4. 11 rather than 12.
  5. 14 rather than 13.
  6. 15 yes.
  7. 16 should be an error.

And I would add an additional point, number 17, that if two retroactive conformances are in conflict; this should be an error, even if they are in different libraries/modules.

I realise that my list above isn't possible with the current linking arrangement. However I feel that changing the way Swift works so that installed apps are re-linked when sdlib etc. are changed would be a good addition to Swift.

I still need a clear definition for what you constitutes a conformance being "used" in order to evaluate your list.

(Note that I'm mostly ignoring conflicting retroactive conformances for now, like the one you have here. That seems like an additional level of complexity.)

Could you explain that? I don't see any substantive way it's "additional;" it seems to come up as soon as you allow any retroactive conformances to exist.

I very much agree that it is a desirable property. Practically speaking, though, your suggestion means that retroactively adding a CustomStringConvertible conformance would be almost useless. Is that something we're prepared to accept? I think I could live with it, FWIW.

The intention "this is only a customization point and not automatically part of the public API just because I publicly conformed to the protocol" is not at all tied to the use of dynamic checks and something that one often wants to express on a requirement-by-requirement basis. I'd totally support having a way to say that, but I'm not sure whether that's compatible with what you're suggesting.

Meanwhile, I've always said that protocols meant to be used as existentials (a prerequisite for the kind of dynamic check you're talking about) should be explicitly marked, to prevent clients from being inadvertently broken by the addition of requirements that have default implementations, which is otherwise not a binary-breaking change and only very rarely a source-breaking change.

Right, that's why I think it'd be useful to be able to warn about this. It's fundamentally the case that, with multiple conformances in a program, some of them will be useless as dynamic customization points, since the runtime will always be in the position of having to "pick one" without context.

I understand that. I was suggesting we have an annotation for protocols whose only stated purpose is dynamic customization, like the Custom* protocols in the stdlib.

And I understand that :slight_smile:. I'm just afraid that adding a specific annotation for that would leave no room for the distinct annotation I described, which IMO is more useful and general.

This is meandering from the topic at hand, but "meant to be used as an existential" would be a temporary measure, because eventually all protocols will be usable as existentials, and adding requirements wouldn't break that ability.

2 Likes

Are we proposing a change to protocols defined in all libraries? Or only resilient libraries? I've got a use case in my own library that sounds like it may be broken by some of the proposed changes here.

iOS has a notion of "extension-safe APIs", meaning those APIs which are safe to call from an app extension. If a library uses only extension-safe APIs, then it can be marked as an extension-safe library. If an extension target links against a non-extension-safe library, a warning is produced at link time.

I have a library that is used by both the main app and by extensions. I want to be able to call an extension-unsafe API in the library, but only if the current process is the main process. Currently, we are doing that like this:

// --------------- //
// In library code
// --------------- //

// Extension-unsafe APIs that the sync manager
// would like to use, if available.
protocol SyncManagerAppSupport {
    func registerForPushNotifications()
}

class SyncManager {
    func foo() {
        if let appSupport = self as? SyncManagerAppSupport {
            // This only happens if we are not in an extension
            appSupport.registerForPushNotifications()
        }
    }
}

// ----------- //
// In App code
// ----------- //

extension SyncManager: SyncManagerAppSupport {
    func registerForPushNotifications() {
        UIApplication.shared.registerForPushNotifications()
    }
}

This pattern means that the library can be marked extension-safe, and still take advantage of extension-unsafe APIs when available. But if retroactive conformances do not participate in dynamic casting, then it seems like the if let appSupport = self as? SyncManagerAppSupport line would fail.

There are other ways of achieving this same functionality, but I'm trying to understand whether the proposed behavior change would affect our code.

To not-meander, the more useful and general annotation I was referring to is not that, but the one that means "this is only a customization point and not automatically part of the public API just because I publicly conformed to the protocol."

3 Likes

OK, I agree that having that as an annotation on protocol requirements would be generally useful.

To clarify, would this annotation only allow the customization point to be called by the implementation of the protocol but not in generic contexts constrained to the protocol or on existentials do the protocol? That would be really nice to have.

What scope would define the implementation of the protocol? The module in which it is declared?

Before diving too deep down this rabbithole, it might be a good idea to start a new thread about it.

1 Like

That's this, from the end:

  • "static ways": relying on a conformance to satisfy generic requirements / associated type requirements, or to convert a value with a concrete type to a value with protocol ("existential") type

  • "dynamic cast": checking whether a type conforms to a protocol at run time, via is or as? or as! or some other feature that has yet to be designed

I guess I can clarify that the former is considered a use at the point where the compiler would check it, i.e. wherever you call the generic function, form the bound generic type, or declare conformance to the protocol with constraints on its associated type.

1 Like

Apologies for the bump, but a recent thread reminded me of this issue: Foundation.Date conforming to Strideable iOS 13.

It has also come up a few more times since this thread:

I was wondering if it might be worth pitching some kind of warning/error for retroactive conformances involving resilient libraries.

3 Likes

Why isn't one option here allowing retroactive conformances to have an access level?

// Framework A
internal extension SomeStruct: CustomStringConvertible {
  var description: String {
    return "SomeStruct, via A"
  }
}
// Framework B
internal extension SomeStruct: CustomStringConvertible {
  var description: String {
    return "SomeStruct, via B"
  }
}
// main.swift
import A
import B
print(SomeStruct()) // Compiler error: type SomeStruct does not conform to protocol CustomStringConvertible
1 Like

There's some discussion of this in the (long) threads on Scoped Conformances and An Implementation Model for Rational Protocol Conformance Behavior, but regardless we'd still need to handle all the concerns raised in the "Co-existing conformances" section:

A couple of things I'd like to take issue with in this paragraph. They are perhaps subtle, but I think they are important.

First, it's certainly possible to ā€œsupport conflicting conformancesā€ in dynamic casts; you just need to define how the conflicts are handled. Once the semantics are written down, you can consider the conflicting conformances to be supported. Of course it's possible there are no semantics we can come up with that's both usable and implementable, but I doubt that's the case.

Secondly, I think it's dangerous to view almost anything in Swift as ā€œcompile-time only.ā€ At least in my experience, every time I've headed down that alley I find myself in a dead-end where something doesn't integrate with the rest of the language, which (in its most general expression) is highly dynamic. The simple inability to monomorphize Swift programs means that the universe of types is not known (in the general case) until runtime, and that in itself means that ā€œcompile-time only conformanceā€ is not really meaningful unless it's restricted to the monomorphizable subset of programs.

3 Likes

Does this same class of problems exist for adding functions or properties to standard library types via an extension?

Let's say that today I added this initializer to String via an extension.

extension String {

  /// Creates a `String` from a `StaticString`.
  public init(_ staticString: StaticString) {
    self = staticString.withUTF8Buffer { String(decoding: $0, as: UTF8.self) }
  }
}

Then in a future version of the standard library Apple added this initializer to the standard library.

Would Swift now not know which initializer to invoke when running my old app binary (compiled when this initializer did not exist in the standard library) on a new OS version (where this initializer exists in the standard library)?

1 Like

Nope, but that's because your version of this initializer is just a function namespaced to your module (its symbol would begin with $s11YourAppName...). If the standard library adds this method, it'll be mangled differently, with a standard-library specific mangling. Your binary will have been compiled to call into your app's symbol, so the new declaration will not affect it, but when you next recompile, you'll probably have to change your code.

This specific issue arises with conformances because they are unique at runtime, and are not namespaced to one or another module, because the compiler needs to look up the single conformance to a given type in order for dynamic dispatch to be deterministic.

12 Likes

@harlanhaskins thank you for the clear explanation :slight_smile:

1 Like
Terms of Service

Privacy Policy

Cookie Policy