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

Don't know how to describe it correctly, but why this does not work?

protocol A {
    func a()
}

protocol B {
    func b()
}

typealias General = A & B

protocol P {
    init(_: General)
}

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

I can't imagine what is wrong with this construction.

General = A & B so A & B != A

A is a superset of A & B.

Subset.

Can't imagine why do you think so.

You're not wrong in that set 𝐴 is a superset of 𝐴 ∩ 𝐵, or that those conforming to A is a superset of those conforming to A & B. However, the initialiser's parameter a is at a contravariant position (← someone please correct me if I'm wrong here), so it will only take a subset of those conforming to General aka A & B.

Another way to explain it: because not all those conforming to A conform to B, you can't pass in a value that isn't "guaranteed" to conform to B to a place that asks for B-conformance.

You've defined a protocol that says it must support both definitions of requirements from protocol A and B.

You then have a new protocol P which uses this definition as a method argument.

Your attempt to conform to P fails because the method you defined only accepts a value of A which is half of the requirement of the protocol definition.

According to A & B, the type must conform to BOTH protocols in order to satisfy the type constraint, but the method declaration says it will only restrict it to A

1 Like

Because A is a superset of A & B, every call of the method of the protocol existential with the value that just confirms to A (or B) would not cause a logical error.
Is there some situation that can illustrate the potential error?

Just call a.b() in the init.
A does not guarantee that this method exists, so the compiler can't accept that line. General, on the other hand, depends on b.

2 Likes

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