Swift should report conflicting requirement in child protocol as early as possible

Hi, Swift currently can't catch issue like this when user defines child protocol.

protocol ParentP {
    var x: Int { get }
}

protocol ChildP: ParentP {
    var x: String { get }
}

print("OK")

But it's actually not OK, and Swift will report errror when user tries to define a struct to conform to ChildP, because it's impossible to do it.

There were a lot of discussions on the net about two protocols having same method signatures. One example is this. But I think my above example is different, because it's an error by protocol definition. The compiler doesn't need further information to determine it.

The reason why I think compiler should report issue like this as early as possible is because Swift promotes protocol-oriented programming. When we do protocol-oriented programming, we define protocols in early design phase and define concrete types later. It's bad if the compiler doesn't catch protocol definition issue as early as possible. While issue in the above example is quite obvious, it can be subtle and confusing in some case. For example, does the following code work? I knew it didn't work. But when I saw the compiler didn't report error, I was confused and thought it was probably a feature I didn't know.

protocol ParentP {
    var x: Type1 { get }
}

protocol ChildP: ParentP {
    var x: Type2 { get } // Type2 is subclass of Type1
}

So I'd think catching issue like this improves usability a lot.

1 Like

There is an unofficial, unsupported feature that lets this work:

struct S: ChildP {
  @_implements(ParentP, x)
  var a: Int
  
  @_implements(ChildP, x)
  var b: String
}

It’s not an official part of the language (yet?) but it does exist and can be used.

5 Likes

Thanks @Nevin. It's interesting to know this feature. But that's probably not what I tried to achieve. What I tried to achieve is two inheritance hierarchy: one for members, one for containers. I believe the canonical way to do it is to use associated type. See code below.

protocol MemberProto1 {
    var x: Int { get }
}

protocol MemberProto2: MemberProto1 {
    var y: Int { get }
}

protocol ContainerProto {
    associatedtype MemberProto: MemberProto1
    var member: MemberProto { get }
}

// The above code suffice. But I find that I can go further and define the following.
protocol ContainerProto2: ContainerProto {
    var member: MemberProto2 { get }
}

The above code worked fine. But I was curious if I could find an alternative way without using associated type. So I tried the following and found the compiler didn't give error.

protocol MemberProto1 {
    var x: Int { get }
}

protocol MemberProto2: MemberProto1 {
    var y: Int { get }
}

protocol ContainerProto1 {
    var member: MemberProto1 { get }
}

protocol ContainerProto2: ContainerProto1 {
    var member: MemberProto2 { get }
}

That's where I got confused and mistook it worked, but it didn't.

Ah, did you mean the examples I gave in my original post could work in some scenarios and hence Swift shouldn't report error? That seems too tricky to me.

1 Like

It is indeed tricky, but if it does make sense in some scenario, you also need to be able to express "Yes, this is what I want, please silent the warning." I couldn't think of such syntax from that.

FWIW, you can also do it with current Swift via default implementations:

struct Member1: MemberProto1 { var x = 0 }
extension ContainerProto1 {
    var member: MemberProto1 { Member1() }
}
struct Member2: MemberProto2 { var x = 1, y = 0 }
extension ContainerProto2 {
    var member: MemberProto2 { Member2() }
}

struct Container: ContainerProto1, ContainerProto2 {} // OK
2 Likes

@Lantua Thanks for the code. I'm afraid you probably misunderstood my purpose. What I wanted to achieve is to set up a inheritance hierarchy between ContainerProto1 and ContainerProto2 so that I am able to access the structs conforming to those protocols using a common interface (that is, ContainerProto1).

I understand that you stumble upon this structure trying to achieve something else and that this particular code does not have the effect you want. However, I also wanted to point out that this could have been the effect someone else wants, and the compiler wouldn't know, nor could they tell the compiler as such (none that I can immediately see).

2 Likes

Got it. In my opinion, these counterintuitive examples suggest that there isn't a simple theory unifying how protocol, class, and inheritance should work together. As a result, the actual behavior is implementation-specific and hard to predict.

I've found Nevin's suggestion vital. Isn't this what you wanted?

protocol MemberProto1 {
    var x: Int { get }
}

protocol MemberProto2: MemberProto1 {
    var y: Int { get }
}

protocol ContainerProto1 {
    var member: any MemberProto1 { get }
}

protocol ContainerProto2: ContainerProto1 {
    var member: any MemberProto2 { get }
}

extension ContainerProto2 {
    @_implements(ContainerProto1, member)
    var __superMember: any MemberProto1 { member }
}