Thanks for your patience @dabrahams, and sorry it took me a few days to get back to you. I finally had a chance to write up a response!
Yeah, having had some time to think about it I think I agree. In some ways it is exactly analogous to the "not all Set<X>
s are the same idea"âif I understand correctly Set<X>
, as a distinct type consists of Set
, a generic argument (in this case X
), and an invisible bundling of X
with a particular conformance to Hashable
. It makes sense to me that inserting an X
into a P
existential also creates that "type plus conformance" bundle, such that X
in module B
(with no conformance or a different conformance) doesn't refer to the same X
contained in someP
.
Right, but unless I'm missing something, for a subclass which respects the contract of its superclass, those operations will behave according to the contract of Base
, so it's difficult for the client to do something that would break if you, say, swapped out one internal subclass for another. (For subclasses which don't behave according to the promises of the superclass, you've of course already made the choice to leak implementation details).
Heh, this is why I said I think we've been talking past each other. I thought this was what we have been discussing? I perhaps didn't clarify enough when we moved from conflicting conformances to the current line of discussion. My apologies for not being more clear!
I believe that mostly captures the semantics I expect, with one exception:
I'm assuming here that someP
has static type P
, in which case I would actually want this cast to succeed.
Yes, I agree that this is a perfectly reasonable stance for a library author to take. But I also think it's reasonable for a library vendor to make a promise that they will avoid significant semantic changes whenever possible. I think the recent discussion that Doug has raised around the changes to #file
illustrates thatâAFAIK the exact format of #file
was never documented, but there's a higher bar the change is being held to than "clients who relied on the format were buggy, so it's okay to break them".
Okay, let's do this.
As noted previously, I'm specifically interested in the someQ as? P
case (where someQ
has static type unrelated to P
, e.g., Any
).
Suppose we have a utility library Module U which provides a protocol that is like Identifiable
, but restricted to String
s, along with some utility methods.
// Module U
protocol StringIdentifiable {
var identifier: String { get }
}
extension Array where Element: StringIdentifiable {
func sortedByIdentifier() -> [Self] { ... }
}
Now, I'm writing a module, Module A. I provide a public Gadget
type, with an internal identifier that I use to implement some algorithms. I also utilize an internal conformance to StringIdentifiable
to enable some of these algorithms. A Gadget
is useful for many things, and one of the things that I use it for is to track some internal state that clients can trigger through a StateTracker
protocol:
// Module A
import U
public class Gadget {
/*internal*/ var state: Int = 0
public init() {}
public func triggerGadget() { state += 1 }
}
extension Gadget: internal StringIdentifier {
var identifier: String {
return "\(state)"
}
}
public extension Array where Element == Gadget {
/// Sort the array by the number of times the gadget has been triggered.
func sortedByTriggerCount() -> [Self] {
return sortedByIdentifier()
}
}
public protocol StateTracker {
func trackStateEvent()
}
extension Gadget: StateTracker {
func trackStateEvent() {
triggerGadget()
}
}
var globalStateTracker: StateTracker = Gadget()
A client, writing their program in Module B, doesn't like global state floating around everywhere, so they decide to create a dependency container. Most of their dependencies conform to StringIdentifiable
. In fact, globalStateTracker
is the only one that doesn't! Grr. On a lark, they think, "Hmm, StateTracker
is an existential, so of course it can't conform to a protocol, but maybe the underlying type does!". So they write:
// Module B
import U
import A
class DependencyContainer {
private var dependencies: [String: StringIdentifiable] = [:]
var globalStateTrackerIdentifier: String?
var globalStateTracker: StateTracker? {
return globalStateTrackerIdentifier.flatMap { dependencies[$0])
}
// ...
}
func initializeDependencyContainer() -> DependencyContainer {
let dependencyContainer = DependencyContainer()
// ...
if let globalStateTrackerIdentifiable as? StringIdentifiable {
dependencyContainer.add(globalStateTrackerIdentifiable)
dependencyContainer. globalStateTrackerIdentifier = globalStateTrackerIdentifiable.identifier
}
// ...
}
To their surprise, it works! This code is promptly forgotten about and never touched again.
Now, as the author of Module A, I realize I've done something terribly silly. My internal conformance to StringIdentifiable
is totally broken! Not only does the "identity" of a gadget change in a way that doesn't really make sense over the course of its lifetime, but my sortedByTriggerCount
method doesn't even sort properly. I have to fix it.
I've exposed this method publicly, and I don't want to break my clients who may have discovered the bug and implemented workarounds or changed their systems to rely on the broken ordering. So instead, I deprecate the old method and introduce a new one:
// Module A
public extension Array where Element == Gadget {
/// Sort the array by the lexicographic ordering of the string (in base 10) representing the number of times the gadget has been triggered.
@available(*, deprecated, message: "Use 'sortedByTriggerCountActually' instead")
func sortedByTriggerCount() -> [Self] {
return sorted { "\($0.state)" < "\($1.state)" }
}
func sortedByTriggerCountActually() -> [Self] {
return sorted { $0.state < $1.state }
}
}
My initial use of StringIdentifiable
was totally misguided, so I decide to remove it. This appears to be a safe change to makeâthe conformance was internal
and I've removed the only place in my program where I was depending on a conformance of Gadget
to StringIdentifiable
. The new version of the library ships, and Module A breaks.
Had I known that clients were relying on this conformance, I never would have removed it. Indeed, if it were a public conformance I would have never even considered it!
Let me know if anything here is unclear, happy to expand on any part that needs it.