Protocol conformance with protocol composition property

Hello
I tried to find an answer to my question using search but couldn't and this is my first one so please bear with me.
Say we have the following:

protocol A {}
protocol B {}
protocol C {
    var a: A { get }
}

struct Foo: C {
    let a: A & B
}

It does not compile saying "Type 'Foo' does not conform to protocol 'C'".

Questions are:

  • Is this expected behavior?
  • If yes then why?
  • Where can I read more on the topic? (specifically this use case)

Personally I find it uncomfortable because this looks like a perfectly valid use case for type composition, if it is an "A and B" it definitely counts as an "A", no associated types or other complications. Moreover, if we don't own Foo looks like there's no workaround without modifying protocol C.

You're likely confusing this with assignments, that is, you can of course set var a: A to an instance of type A & B.

However, var a: A as a protocol requirement means that conforming types must have a variable named exactly a of type A (not some other type that conforms to A, it has to be exactly A). Protocols are like blueprints; without generics, you won't be able to conform if you implement a requirement with a different type, even if it is somehow related to the required type. This is why the implementation of a protocol requirement (a witness) is different from an override, that are sometimes allowed to be covariant:

class  A {
   var foo: A { return A() }
}
class B: A {
  override var foo: B { return B() }
}

If you want to require a to be of some arbitrary type that conforms to A, you should use an associated type that you can implement upon conformance:

protocol C {
    associatedtype R: A
    var a: R { get }
}

3 Likes

By using concrete types that conform to the protocols, instead of using the protocols directly, you can make it work: E.g.:

protocol A {}
protocol B {}
protocol C {
    associatedtype T: A
    var a: T { get }
}

struct Foo<Bar: A & B>: C {
    let a: Bar
}


extension Int: A {}
extension Int: B {}

let foo = Foo(a: 7) // Foo<Int>

This compiles and works as expected.

1 Like

Sure there are some workaround to make this work.

But I agree with SergeRykovski, it would be more intuitive on how to manipulate protocols.

protocol A {}
protocol B {}
protocol C {
    var a: A { get } // We need a variable conforming to A (it must *at least* implement functionalities of A)
}

struct Foo: C {
    let a: A & B // Because 'a' implements A and B, it implements functionalities of A so it conforms to C)
}

That's a behavior I would naturally expect. From protocol C implementation, the fact that 'a' also implements B does not matter since it is not suppose to know implementation but only functionalities.

1 Like

Yes, in the get-only case this "ought" to work, and that's tracked by SR-522. At this point, though, it's potentially behavior-changing, so we'd have to be careful about it. (Today you might get a default implementation instead of a more specific one; see SR-5858 for a case where this happens but it's not desired.)

3 Likes

From what I understand, SR-5858 mentions a compiler bug concerning non-optional passed to an optional protocol's property.

So yes in the case of inheritance, it may break functionalities. But for protocols I think it should work because we don't care about type passed as long as it conforms to the protocol(s).
Perhaps it may look inconsistent in Swift?

In the get/set-case I don't see what's changing? :thinking:

In the get+set case the types really do have to match up:

extension C {
  mutating func change(_ newA: A) {
    self.a = newA
  }
}

But that's nothing special.

I mean that for the compiler an instance of "A & B" should be valid everywhere an "A" is expected. Using an associated type allows for what I wrote in my question. But then again, what if we cannot modify Foo?

Thanks for useful links! Looks like SR-629 is precisely what I'm asking. You commented that " We don't currently allow any covariance in protocol conformance.", has anything changed in that regard?

Not that I know of, but that was more of a statement about implementation than intent. I think people would be receptive to this change if you wanted to put it through the evolution process; the hardest part is most likely to be the implementation. (Right now many of the Apple Swift people are still focused on ABI stability.)

I'm almost sure that's true in general for the get-only case, as Jordan mentioned. Usability is an important argument, and I wouldn't be against a formal proposal on this. The coding part is quite the challenge though based on what we currently have for overrides. In these cases subtype relationships involving composition aren't supported whatsoever, so I think a proposal should address both covariant overrides and witnesses.

What we need to do is merge the code for checking overrides and checking witnesses, and in both cases use a constraint solver. This way the rules will be the consistent across the language.

1 Like

Are there any straightforward pitfalls I am missing in supporting other subtype relationships , i.e. conforming and composing?

One potential risk is that checking method overrides can now trigger conformance checking. However the request evaluator work being done by @Douglas_Gregor is meant to make this sort of cycle a lot less problematic.

One pitfall: associated type inference. Should we be able to infer associated types from covariant witnesses? It makes the currently-brittle associated type inference problem trickier, e.g.,

class Super { }
class Sub: Super { }

protocol P {
  associatedtype AT
  func foo() -> AT
  func bar() -> AT
}
 
struct X: P {
  func foo() -> Super { ... }
  func bar() -> Sub { }
  // can I infer AT here?
}

Doug

2 Likes