Why the usage of base protocol in conformance to the protocol type is not allowed?

Protocols do not contain implementation, so I can't call anything in the protocol init.

Protocol composition creates a union, not an intersection. The type conforming to a composite protocol conforms to the entirety of both.

2 Likes

A is a subset of A & B, because it only satisfies half of the requirement.

How you actually use the value in the method doesn't matter to the compiler, because you've defined it to enforce that any value passed to a type conforming to P in the method init(_:) MUST conform to both A & B.

The init(_ a: A) method is more restrictive than the protocol requirement, and therefor does not satisfy the requirement that you defined the compiler to enforce (both A & B) as it only enforces the A portion.


As a sidenote, usually what you want when you need to refer to multiple protocols that are related is another protocol, rather than a typealias:

protocol General: A, B { }

You can provide default implementations, depending on the use case

protocol P {
    init(_ value: General)
}

extension P {
    init(_ value: General) {
        value.b()
    }
}

Any type conforming to P will use that definition as part of its conformance unless an implementation is explicitly written.

Effectively, Sub would have the following:

init(_ a: A)
init(_ general: A & B)

You're right that your conformance should/could work, but this part is wrong, throwing people off guard. It's the other way around; Any value of type General is, by it's definition, also a value of type A, i.e., you can cast General to A.

So, if we use < as a subtype relationship, then

  • General < A,
  • Since function argument is contravariant, (A) -> Self < (General) -> Self
  • Now, as class method is covariant w.r.t. its class. It follows that Sub < P as we'd like.

The question now is, whether Swift accept the last relationship, because iirc, it doesn't allow methods with subtype signature to satisfy protocol requirement yet.

Well, the requirements form a union, but the typing forms an intersection. So you might be talking past each other here.

4 Likes

Yes you can, but the implementation of the protocol replaces, and not overrides the default implementation. So in this case the default implementation does not matter.

Yes, you're right, that was a mistake.
But I don't think that contravariant relationship is right here, because I can't think of any situation that can cause a logical error in this context.

No no, contravariant is just a relationship between type and its composite/generic. That function argument is contravariant w.r.t. its function simply means that, if Generic < A, then

(A) -> X < (Generic) -> X.

As you'd have surmised, there's nothing wrong with casting (A) -> X to (A & B) -> X because it'll just ignore the B requirement on the callee side. However, it is allowed precisely because you can cast A & B to A, not the other way around.

All Generals are As, but not all As are Generals.

Your definitions declare that a conformer to General can have any method defined by A or B invoked, but a conformer to A only promises that methods of A are available.

1 Like

The conformer just doesn't need methods of B to be involved in the implementation.

You state that P requires an init which takes a conformer to General, but your Sub type doesn't have such an init. A doesn't conform to General.

1 Like

As I wrote earlier Sub: P (could) work, because General conforms to A.

I don't know what this means. Sub: P is how the code in the original post is written. It's still the case that init(_ a: A) does not meet the requirement of P.

I explained it here: Why the usage of base protocol in conformance to the protocol type is not allowed? - #14 by Lantua. init(_: A) (could) meet the requirement of init(_: Generic) because Generic conforms to A. Whether or not A conforms to Generic is utterly irrelevant.

I had to read the post a couple times and work out with pseudocode how it would work. Thanks for expanding my understanding.

1 Like

So all-in-all, there's nothing wrong from the type perspective. I believe it fails because Swift currently requires that the type of the methods match exactly with the protocol requirement, not even its subtype.

Not yet, but you can easily make it wrong.

class Sub: P {
  init(_ a: A) {
    
  }
  
  init(_: B) {
    
  }
}

Easy solution, though.

required convenience init(_ general: General) {
  self.init(general as A)
}

I was referring to Sub.init — but actually, nothing forces you to actually use methods from B there, so I have to agree that technically, everything should be fine (that is: You can create an instance with a parameter of type General, thus fulfil what the protocol demands).

However, I'm not sure if I would like to see this restriction lifted:
There is no strict rule that signatures have to match exactly when you implement a protocol, but doing so avoids confusion; I'd rather write a small convenience initialiser and have a connection to the protocol which is visible at first sight.

It is lifted already with generics, see this post Allow protocol conformance by declaring a method with a supertype argument - #2 by xwu
So no reason to keep it with existentials.

To write the convenience init or not should be the choice of the developer and a style guide the developer is using.

That's not the same thing; there's no multiple inheritance there that would require potentially-impossible disambiguation.

You can't write a convenience init to disambiguate unless you're using existentials, because disambiguation between type cousins requires casting.

Edit: Scratch that. Opaque types opened this up.

class Sub: P {
  init<T: A>(_ a: T) {

  }

  init<T: B>(_ b: T) {

  }
  
  required convenience init<T: General>(_ general: T) {
    let a: some A = general
    self.init(a)
  }
}