Well, the change moving X
instances between modules that have different notions of Equatable
for X
, came long after there were “different definitions of a protocol conformance running around for a type.” So did I state the rule correctly above, or is it really don't move instances between modules (or presumably, files) that have conflicting conformances for those instances? I really wanna know.
Note that in this case:
C
added its conformance in an update, long afterD
was already importingC
. DidD
break the rules of the programming model by allowingC
to be updated?D
used code completion to finddoSomething()
onuniqueXs
, and read all the documentation that popped up when they did that. DidD
break the rules of the programming model by not tracking down the source of the symbol and noticing that it came fromC
and thatC
had added a conflicting conformance?
This is an even tighter constraint than I thought you were going for, although I guess it's one I understand mechanically. It says that the presence of any conflicting protocol conformance for
X
in a source file in moduleD
makes effectively all of the APIs that traffic inX
unusable in that source file. That's true even if the source file never directly uses one of the conflicting conformances, because you never know whether that conflicting conformance is holding up some invariant.
Yep, gloriously straightforward, right?
Also, true: as the compiler, you do never know whether a conformance is holding up some invariant. I hope we can all agree that upholding invariants is fundamental in supporting the creation of large, reliable systems. A huge part of this burden necessarily falls on humans, who must document and read API contracts. But if the language rules make it possible for a type to mean two different things and doesn't prevent me from mixing those things up, it undermines the whole effort. Note that even languages like Python, that have no static checking, don't have this issue: a type only ever means one thing at a time.
That's a very wide net to cast, because it means people are paying for (in compiler error messages) something that is very unlikely to cause a problem in practice.
You may remember, from C++, that these can be the worst kinds of problems.
I consider the collateral damage of your solution---that the mere presence of a conflicting conformance anywhere poisons a type completely---to be greater than the problem you're trying to solve.
Really, you'd take "it's going to bite you eventually“ over “you have to put some code in separate files to disambiguate?”
Static type systems do not catch all bugs, nor should they. One hard thing to balance in a statically-typed language is when to back off on the type system, because the cost of appeasing the type checker exceeds the benefits. Having an unrelated conflicting conformance make a library type unusable creates busywork and frustration for users.
You know I'm well aware of these tradeoffs, which is part of why we still don't have static constraints on which errors can be thrown in Swift. I also am painfully aware of where we've been too enthusiastic for static checking, which is why I have to sprinkle try
liberally through code where the sources of errors are irrelevant (if the effect on serialization code is annoying you should see what it does to a BGL-style breadth-first search or just about anything that uses lots of closures—so much harder to ignore the noise when it isn't all stacked uniformly at the beginning of every line). The reasons this is different are:
- Unlike error propagation, conflicting conformances are extremely rare
- When not extremely carefully managed, as you say they're “going to bite you eventually.“
- The contexts they'll bite you in are unlimited, rather than just areas where you need to be careful anyway (where invariants are being broken).
- Moving code into a separate file where the conformances are unambiguous is a relatively light burden that clarifies, rather than obfuscating.
Edit full disclosure: until I saw @mattrips' post yesterday I had forgotten that files in the same module (supposedly?) have their own sets of imports which should be hidden from one another, so one could argue that my simple rule about not mentioning X
in a context where its conformances are ambiguous is incomplete. We should consider preventing X
from being exchanged between files in the same module having conflicting conformances. I can see how that might be considered overly protective for individual module developers, but it might be necessary to support larger teams working on the same module; the implications should be thought through carefully.
Note that this situation we can create today has all the same implications as having scoped conformances (except that only scoped conformances let us reduce code bloat).