Since it sees the argument as type A, shouldn't it ignore the extensions altogether and fail at compile time? Otherwise it'd be fair to ask the compiler to never fail on "unknown" methods and properties but always perform a dynamic dispatch (as is mostly done in Objective-C).
The compiler's not allowed to see whether the body of doSomething(_:) uses Mixin, because maybe it does in this version of the MixinUser library, but doesn't in next year's release. Or vice versa. (When library evolution is enabled, changing the body of a function should not be an ABI-breaking changeâmaintaining ABI compatibility is already trickier in Swift than in C!)
I also agree with @Lantua that if you can't see a requirement in the declaration of a function, it's weird for the compiler to complain about it. Then again, there are all sorts of function preconditions in Swift that get deferred to runtime ("the upper bound of a Range cannot be less than the lower bound"), and it would actually be very nice if the compiler checked them, so I can't say that this is out of consideration.
Using different conformances in different contexts is a self-consistent answer to this problem, but a somewhat tricky one. Let's make this more concrete using Hashable. (I'm not explicitly separating things into separate modules this time, but imagine I did to make the problem harder.)
protocol A: Equatable {
var a: Int
}
protocol B: Equatable {
var b: Int
}
extension A: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(a)
}
}
extension B: Hashable { // typo fixed, thanks Lantua
func hash(into hasher: inout Hasher) {
hasher.combine(b)
}
}
struct Concrete: Equatable, A, B {
var a, b: Int
}
func makeSet<Element: A>(_ element: Element) -> Set<Element> {
var result = Set<Element>()
result.insert(element)
return result
}
func checkIf<Element: B>(_ element: Element, in set: Set<Element>) -> Bool {
return set.contains(element)
}
let value = Concrete(a: 1, b: 2)
let set = makeSet(value) // if this uses A: Hashable
let isPresent = checkIf(value, in: set) // and this uses B: Hashable
print(isPresent) // then this might print 'false'
I'm oversimplifying here; the Swift runtime, at least, would not consider these Sets to have the same type because they use different conformances. But you get the idea.
One answer to this problem is actually what you said:
Probably, to not spawn a crazy amount of modules if such a case has arisen, it is better to use namespacing, so a programmer can choose required conformance within a certain scope within a single module.
...but today the way to spell "choose a particular (or alternate) conformance" in Swift is "use a wrapper type". I do think it should be easier to define and use such types, but Swift isn't unique here; I don't know of any languages with generics that don't use wrapper types for this besides ML. (ML's approach says you always have to specify which conformance you wantâthere's no defaultâand so it's as if all your generic types used a Strategy or delegation pattern.)
We've been ignoring dynamic lookup, and that's because it's really worse than everything else. In the same example as before, what happens for (Concrete() as Any) as? Mixin? (Keep in mind that the conversion to Any might happen in a binary library we can't see.) Without any other context, there are two, equally valid options, and we'd have to decide carefully what happens, or whether such "mixin conformances" can only be used statically or something.
(Objective-C's answer, at the method level, is to pick one based on library load order, which is pretty inscrutable.)
At compile-time, it absolutely does (ignoring optimizations when the compiler happens to be able to see all the code). At run-time, it gets a little tricky: you can do dynamic checks (that's what as? is) but the context in which you perform the check is separate from where the type was defined, or where it was coerced into an Any. This can be a problem if there's more than one conformance available (something that only happens today with retroactive conformances, but which would apply to mixin conformances as well).
In my case, though, there isn't a witness record (conformance descriptor) for Concrete: Mixin, only Concrete: A, Concrete: B, A: Mixin, and B: Mixin. Should dynamic casts do an unbounded amount of work to figure out whether Concrete: Mixin can be formed?
(The answer could be yes! Since it can easily be cached, at least. I'm not sure whether that would be my answer, since it makes the cost of as? go up, but this particular issue has a solution that doesn't immediately contradict a design goal. as? may already have to scan through all loaded libraries if it's the first time a particular dynamic check is performed.)
Well, yes, Swift shouldn't be a copy of one other language, but it is absolutely a synthesis of good ideas from other languages (struct semantics are very much like copyable types in Rust, enums are almost directly sum ADTs from several other languages, classes are a lot like Objective-C's but with a C++-style dispatch, protocols behave a lot like ML conformances but associating them with a particular type like Java/C#). If there's one thing that Swift has really innovated on, it's having a stable ABI with non-uniformly-sized values and a non-trivial generics system. No other major language does that as far as I know.
If there's a new idea that's really good, we should definitely consider adding it to Swift. But a new idea is harder to evaluate since we don't know its trade-offs yet.
@Kentzo's comparison to Scala implicits is spot-on, and I'm sorry I didn't concur with that sooner. I think such a feature can be reasonably designedâ"if the compiler can't see any other way that this type conforms to this protocol, use this implementation" or even "use this implementation instead of how the type normally conforms to the protocol"âbut the trade-off we get is that the conformance might behave differently than one used elsewhere in the program, and that's not currently something that happens in Swift (except, again, in the retroactive conformance case).
What if we split it up into two proposals: a way to use alternate conformances explicitly, and then after that a way to do that implicitly? I should warn that the second one is likely to get a lot more resistance; besides overloading, there aren't that many features in Swift that kick in implicitly. But the first step is something the runtime already (mostly) supports, and you could see types that look something like this (not really proposing this syntax, just for understanding):
Set<Int where Element: Hashable = CustomHashable> // replacing a specific conformance requirement
Set<Int in IntWrapper> // replacing a type, along with all its conformances
Set<@IntWrapper Int> // using property wrapper syntax
It's less obvious how to apply this to generic functions, but :-/
Perhaps one way to address it is to extend Swift to have final extension for retroactive protocol conformances in the translation unit where the protocol itself is defined. The assumption is that partial or full redefinition of a (retroactive) protocol conformance implies a weighted judgement by the user and should only be disallowed when library's author has reasons to disallow it.
Doesn't "closest namespace then closest match" suffice?
Alright, at least to name the problem is better to do nothing. Do you know where are templates for proposals are located? I failed to find the thing on github. : \