Overlapping Conformance

Given the direction Swift is taking with existentials, it may be worth revisiting overlapping conformances.

Swift has deliberately banned having generic types conform to the same protocol twice even with different bounds, because there may be an overlap. For instance,

protocol Monoid {
   static var neutral : Self {get}
   func append(_ next: Self) -> Self
}

struct Func<S, T> {
   let body : (S) -> T
}

extension Func : Monoid where S == T {
   static var neutral : Self {Func{$0}}
   func append(_ next: Self) -> Self {
      Func {next.closure(closure($0))
   }
}

extension Func : Monoid where T : Monoid {
   static var neutral : Self {Func {_ in .neutral}}
   func append(_ next: Self) -> Self {
      Func {closure($0).append(next.closure($0))}
   }
}
 

The problem being: which overload does Swift use if S==T and T : Monoid?

Right now, the solution is to just ban multiple conformance. But we cloud do something similar to what we're now doing with existential boxes: we could allow multiple conformance and throw a compile error if we're attempting to create instances where the conformance actually overlaps. This could even be combined with the rejected considered alternative in the linked proposal where you provide "tie breaker" overloads.

Basically, the idea was that you provide an explicit overload for any possible overlapping conformance, e.g.:

extension Func : Monoid where S == T, T : Monoid {
   /// do what you consider appropriate
}

The authors of the linked proposal found this unsatisfying because you get a quickly growing number of overlapping conformances with each additional conformance. But if we only considered it an error if you created instances where no "tie breaker" is declared, that would mitigate the issue.

What do y'all think?

1 Like

I haven’t thought this through all the way, but another possibility would be to allow strictly non-overlapping conditional conformances. That means for any two conformances of T to P, the constraints must explicitly not overlap.

To achieve this, we could introduce “anti-constraints”, specifying that some relationship must not hold. These could be written T != U and T !: P, which lets your example become:

extension Func: Monoid where S == T { ... }

extension Func: Monoid where T: Monoid, S != T { ... }

This essentially “bakes in” the precedence of the extensions, in cases where overlap would otherwise occur, and it does not cause combinatorial explosion.

Alternatively, conformances could be given explicit precedences, though I don’t know how that would look and work in practice.

5 Likes

Don't forget that conformances can be found at run time with as?. If you allow multiple conformances, you need to decide what the behavior is there as well. (Not that today's behavior is especially solid; you can have two separate modules define the same conformance and as? will just pick one. What else is it supposed to do?)

1 Like

I guess if we can ban calling protocol witnesses or protocol extension methods in case of ambiguous conformance, we can ban casts to the protocol in those cases as well. Problems might arise if you do type erasure in two steps, e.g.

myFunc as? Monoid //ambiguous, therefore banned
myFunc as? Any as? Monoid //uh oh