Conditional Conformance: Multiple conformances when wrapper has multiple generic elements


(Kevin Brewster) #1

Consider the following trivial example:

struct SomeWrapper<FirstWrapped, SecondWrapped> { }

protocol HasIntProtocol { }

extension SomeWrapper: HasIntProtocol where FirstWrapped == Int { }
extension SomeWrapper: HasIntProtocol where SecondWrapped == Int { } 

// Redundant conformance of 'SomeWrapper <FirstWrapped, SecondWrapped>' to protocol 'HasIntProtocol'

This fails because “[The] existing ban on multiple conformances is extended to conditional conformances, including attempts to conform to the same protocol in two different ways.”. The rationale behind this ban is because of the additional complexities introduced when you have overlapping conformances.

However, in the above example there are no overlapping conformances, so it doesn’t seem like there should be any reason to prevent multiple conformances in this way.

Am I missing something?


Explicit ordering of multiple conditional conformances
(Paul Cantrell) #2

Consider the following:

struct SomeWrapper<FirstWrapped, SecondWrapped> { }

protocol HasIntProtocol {
    func foo() -> Int
}

extension SomeWrapper: HasIntProtocol where FirstWrapped == Int {
    func foo() -> Int {
        return 0
    }
}

extension SomeWrapper: HasIntProtocol where SecondWrapped == Int  {
    func foo() -> Int {
        return 1
    }
}

SomeWrapper<Int,Int>().foo()   // ?!?

What should that last line evaluate to?


(Kevin Brewster) #3

Ah, you are correct. Thank you.


(Dimitrios Vytiniotis) #4

Sorry to bring this back again. The original example is indeed overlapping, but it does raise the question, that if the constraints were (FirstWrapped == Int) and (FirstWrapped == Bool) respectively then I don't think there's anything overlapping.

That is, is there any hope for allowing conditional conformances where simple unification over the generic arguments can detect that the instances are definitely non-overlapping, and deem everything else as potentially overlapping? That's probably a simple and predictable behavior for users (and also the design choice taken in languages with type classes). In the example of the original poster <Int,SecondWrapped> and <FirstWrapped,Int> are indeed unifiable, hence the conformances are overlapping, and would happily be ruled out as ambiguous. Is that design choice up for consideration?

Thanks


(Jordan Rose) #5

It's implementable, but doing so would make dynamic casting more complicated, as the runtime would now have to look at each possible conformance and see what matches. I'm sure there are additional implementation complications as well.

On a more fuzzy level, it also might encourage people to use ==-based conditional conformances where defining a protocol would make more sense, just because "== is more future-proof because you can add things".


(Dimitrios Vytiniotis) #6

Thanks for the quick reply. I admit little knowledge of the internals of the type system of Swift (as I am new to it), so I am particularly intrigued by "the runtime would have to look at each ...". I thought that this kind of resolution is done by the type checker/inference engine, not the runtime. Could you elaborate with an example if it's not too much burden? Many thanks!


(Jordan Rose) #7

Sure, imagine some code like this:

protocol Fooable {
  func foo() {}
}

extension Array: Fooable where Element == Int {
  func foo() { "I've got Ints!" }
}

extension Array: Fooable where Element == Double {
  func foo() { "I've got fractions, though." }
}

// ---

let array: [Int] = [1, 2, 3]
let opaque: Any = array
if let fooable = opaque as? Fooable { // !!!
  fooable.foo()
}

At the line marked !!!, the runtime has to go look through all conformances to see if the dynamic type (Array<Int>) conforms to the protocol (Fooable). Today that can be done pretty efficiently by filtering on the protocol and the generic type, then checking the <Int> part once on whatever you find. That logic would have to turn into a loop. This isn't hard to implement, but it is potentially slow if people add a lot of these.


(Dimitrios Vytiniotis) #8

Ah I see, I did not know that it was even a thing to check conformances dynamically like this. Agreed re: perf. worries, this is like a full blown vtable lookup taking the generic arguments into account too (which maybe is has optimized code for, from somewhere else in the runtime?)


(David Sweeris) #9

My inclination would be to allow the redundant conformance, but also require additional extension(s) whenever the compiler detects potentially competing extensions to clear up the matter:

// This is ok
extension SomeWrapper: HasIntProtocol where FirstWrapped == Int {
    func foo() -> Int { return 0 }
}
// When the compiler gets here, it throws a "Whoa, whoa, hold on, what 
// if they're both `Int`?" error...
extension SomeWrapper: HasIntProtocol where SecondWrapped == Int {
    func foo() -> Int { return 1 }
}
// ... which this 3rd extension resolves.
extension SomeWrapper: HasIntProtocol where FirstWrapped == Int, SecondWrapped == Int {
    func foo() -> Int { return 2 }
}
// It would be an error to _not_ have such an extension if the compiler
// detects any such "conformance collisions".

And I think the error should be thrown as soon as the potentially conflicting extension is written, not when someone first tries to instantiate a SomeWrapper<Int, Int> or tries to call the .foo() function. It'd be a nightmare for library vendors if someone was trying to instantiate the problem type and suddenly got some redundant-conformance error coming at them from out of the blue — especially if the protocol in question happens to be some internal implementation detail which isn't exported.

Admittedly, this could result in having to write a lot of extensions, but it solves the issue of what SomeWrapper<Int, Int>().foo() returns, and presumably developers wouldn't bother with all the complexity if they didn't actually need it for whatever they're trying to do.