Protocol doesn't conform to itself?

21,318

Solution 1

EDIT: Eighteen more months of working w/ Swift, another major release (that provides a new diagnostic), and a comment from @AyBayBay makes me want to rewrite this answer. The new diagnostic is:

"Using 'P' as a concrete type conforming to protocol 'P' is not supported."

That actually makes this whole thing a lot clearer. This extension:

extension Array where Element : P {

doesn't apply when Element == P since P is not considered a concrete conformance of P. (The "put it in a box" solution below is still the most general solution.)


Old Answer:

It's yet another case of metatypes. Swift really wants you to get to a concrete type for most non-trivial things. [P] isn't a concrete type (you can't allocate a block of memory of known size for P). (I don't think that's actually true; you can absolutely create something of size P because it's done via indirection.) I don't think there's any evidence that this is a case of "shouldn't" work. This looks very much like one of their "doesn't work yet" cases. (Unfortunately it's almost impossible to get Apple to confirm the difference between those cases.) The fact that Array<P> can be a variable type (where Array cannot) indicates that they've already done some work in this direction, but Swift metatypes have lots of sharp edges and unimplemented cases. I don't think you're going to get a better "why" answer than that. "Because the compiler doesn't allow it." (Unsatisfying, I know. My whole Swift life…)

The solution is almost always to put things in a box. We build a type-eraser.

protocol P { }
struct S: P { }

struct AnyPArray {
    var array: [P]
    init(_ array:[P]) { self.array = array }
}

extension AnyPArray {
    func test<T>() -> [T] {
        return []
    }
}

let arr = AnyPArray([S()])
let result: [S] = arr.test()

When Swift allows you to do this directly (which I do expect eventually), it will likely just be by creating this box for you automatically. Recursive enums had exactly this history. You had to box them and it was incredibly annoying and restricting, and then finally the compiler added indirect to do the same thing more automatically.

Solution 2

Why don't protocols conform to themselves?

Allowing protocols to conform to themselves in the general case is unsound. The problem lies with static protocol requirements.

These include:

  • static methods and properties
  • Initialisers
  • Associated types (although these currently prevent the use of a protocol as an actual type)

We can access these requirements on a generic placeholder T where T : P – however we cannot access them on the protocol type itself, as there's no concrete conforming type to forward onto. Therefore we cannot allow T to be P.

Consider what would happen in the following example if we allowed the Array extension to be applicable to [P]:

protocol P {
  init()
}

struct S  : P {}
struct S1 : P {}

extension Array where Element : P {
  mutating func appendNew() {
    // If Element is P, we cannot possibly construct a new instance of it, as you cannot
    // construct an instance of a protocol.
    append(Element())
  }
}

var arr: [P] = [S(), S1()]

// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
arr.appendNew()

We cannot possibly call appendNew() on a [P], because P (the Element) is not a concrete type and therefore cannot be instantiated. It must be called on an array with concrete-typed elements, where that type conforms to P.

It's a similar story with static method and property requirements:

protocol P {
  static func foo()
  static var bar: Int { get }
}

struct SomeGeneric<T : P> {

  func baz() {
    // If T is P, what's the value of bar? There isn't one – because there's no
    // implementation of bar's getter defined on P itself.
    print(T.bar)

    T.foo() // If T is P, what method are we calling here?
  }
}

// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
SomeGeneric<P>().baz()

We cannot talk in terms of SomeGeneric<P>. We need concrete implementations of the static protocol requirements (notice how there are no implementations of foo() or bar defined in the above example). Although we can define implementations of these requirements in a P extension, these are defined only for the concrete types that conform to P – you still cannot call them on P itself.

Because of this, Swift just completely disallows us from using a protocol as a type that conforms to itself – because when that protocol has static requirements, it doesn't.

Instance protocol requirements aren't problematic, as you must call them on an actual instance that conforms to the protocol (and therefore must have implemented the requirements). So when calling a requirement on an instance typed as P, we can just forward that call onto the underlying concrete type's implementation of that requirement.

However making special exceptions for the rule in this case could lead to surprising inconsistencies in how protocols are treated by generic code. Although that being said, the situation isn't too dissimilar to associatedtype requirements – which (currently) prevent you from using a protocol as a type. Having a restriction that prevents you from using a protocol as a type that conforms to itself when it has static requirements could be an option for a future version of the language

Edit: And as explored below, this does look like what the Swift team are aiming for.


@objc protocols

And in fact, actually that's exactly how the language treats @objc protocols. When they don't have static requirements, they conform to themselves.

The following compiles just fine:

import Foundation

@objc protocol P {
  func foo()
}

class C : P {
  func foo() {
    print("C's foo called!")
  }
}

func baz<T : P>(_ t: T) {
  t.foo()
}

let c: P = C()
baz(c)

baz requires that T conforms to P; but we can substitute in P for T because P doesn't have static requirements. If we add a static requirement to P, the example no longer compiles:

import Foundation

@objc protocol P {
  static func bar()
  func foo()
}

class C : P {

  static func bar() {
    print("C's bar called")
  }

  func foo() {
    print("C's foo called!")
  }
}

func baz<T : P>(_ t: T) {
  t.foo()
}

let c: P = C()
baz(c) // error: Cannot invoke 'baz' with an argument list of type '(P)'

So one workaround to to this problem is to make your protocol @objc. Granted, this isn't an ideal workaround in many cases, as it forces your conforming types to be classes, as well as requiring the Obj-C runtime, therefore not making it viable on non-Apple platforms such as Linux.

But I suspect that this limitation is (one of) the primary reasons why the language already implements 'protocol without static requirements conforms to itself' for @objc protocols. Generic code written around them can be significantly simplified by the compiler.

Why? Because @objc protocol-typed values are effectively just class references whose requirements are dispatched using objc_msgSend. On the flip side, non-@objc protocol-typed values are more complicated, as they carry around both value and witness tables in order to both manage the memory of their (potentially indirectly stored) wrapped value and to determine what implementations to call for the different requirements, respectively.

Because of this simplified representation for @objc protocols, a value of such a protocol type P can share the same memory representation as a 'generic value' of type some generic placeholder T : P, presumably making it easy for the Swift team to allow the self-conformance. The same isn't true for non-@objc protocols however as such generic values don't currently carry value or protocol witness tables.

However this feature is intentional and is hopefully to be rolled out to non-@objc protocols, as confirmed by Swift team member Slava Pestov in the comments of SR-55 in response to your query about it (prompted by this question):

Matt Neuburg added a comment - 7 Sep 2017 1:33 PM

This does compile:

@objc protocol P {}
class C: P {}

func process<T: P>(item: T) -> T { return item }
func f(image: P) { let processed: P = process(item:image) }

Adding @objc makes it compile; removing it makes it not compile again. Some of us over on Stack Overflow find this surprising and would like to know whether that's deliberate or a buggy edge-case.

Slava Pestov added a comment - 7 Sep 2017 1:53 PM

It's deliberate – lifting this restriction is what this bug is about. Like I said it's tricky and we don't have any concrete plans yet.

So hopefully it's something that language will one day support for non-@objc protocols as well.

But what current solutions are there for non-@objc protocols?


Implementing extensions with protocol constraints

In Swift 3.1, if you want an extension with a constraint that a given generic placeholder or associated type must be a given protocol type (not just a concrete type that conforms to that protocol) – you can simply define this with an == constraint.

For example, we could write your array extension as:

extension Array where Element == P {
  func test<T>() -> [T] {
    return []
  }
}

let arr: [P] = [S()]
let result: [S] = arr.test()

Of course, this now prevents us from calling it on an array with concrete type elements that conform to P. We could solve this by just defining an additional extension for when Element : P, and just forward onto the == P extension:

extension Array where Element : P {
  func test<T>() -> [T] {
    return (self as [P]).test()
  }
}

let arr = [S()]
let result: [S] = arr.test()

However it's worth noting that this will perform an O(n) conversion of the array to a [P], as each element will have to be boxed in an existential container. If performance is an issue, you can simply solve this by re-implementing the extension method. This isn't an entirely satisfactory solution – hopefully a future version of the language will include a way to express a 'protocol type or conforms to protocol type' constraint.

Prior to Swift 3.1, the most general way of achieving this, as Rob shows in his answer, is to simply build a wrapper type for a [P], which you can then define your extension method(s) on.


Passing a protocol-typed instance to a constrained generic placeholder

Consider the following (contrived, but not uncommon) situation:

protocol P {
  var bar: Int { get set }
  func foo(str: String)
}

struct S : P {
  var bar: Int
  func foo(str: String) {/* ... */}
}

func takesConcreteP<T : P>(_ t: T) {/* ... */}

let p: P = S(bar: 5)

// error: Cannot invoke 'takesConcreteP' with an argument list of type '(P)'
takesConcreteP(p)

We cannot pass p to takesConcreteP(_:), as we cannot currently substitute P for a generic placeholder T : P. Let's take a look at a couple of ways in which we can solve this problem.

1. Opening existentials

Rather than attempting to substitute P for T : P, what if we could dig into the underlying concrete type that the P typed value was wrapping and substitute that instead? Unfortunately, this requires a language feature called opening existentials, which currently isn't directly available to users.

However, Swift does implicitly open existentials (protocol-typed values) when accessing members on them (i.e it digs out the runtime type and makes it accessible in the form of a generic placeholder). We can exploit this fact in a protocol extension on P:

extension P {
  func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
    takesConcreteP(self)
  }
}

Note the implicit generic Self placeholder that the extension method takes, which is used to type the implicit self parameter – this happens behind the scenes with all protocol extension members. When calling such a method on a protocol typed value P, Swift digs out the underlying concrete type, and uses this to satisfy the Self generic placeholder. This is why we're able to call takesConcreteP(_:) with self – we're satisfying T with Self.

This means that we can now say:

p.callTakesConcreteP()

And takesConcreteP(_:) gets called with its generic placeholder T being satisfied by the underlying concrete type (in this case S). Note that this isn't "protocols conforming to themselves", as we're substituting a concrete type rather than P – try adding a static requirement to the protocol and seeing what happens when you call it from within takesConcreteP(_:).

If Swift continues to disallow protocols from conforming to themselves, the next best alternative would be implicitly opening existentials when attempting to pass them as arguments to parameters of generic type – effectively doing exactly what our protocol extension trampoline did, just without the boilerplate.

However note that opening existentials isn't a general solution to the problem of protocols not conforming to themselves. It doesn't deal with heterogenous collections of protocol-typed values, which may all have different underlying concrete types. For example, consider:

struct Q : P {
  var bar: Int
  func foo(str: String) {}
}

// The placeholder `T` must be satisfied by a single type
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}

// ...but an array of `P` could have elements of different underlying concrete types.
let array: [P] = [S(bar: 1), Q(bar: 2)]

// So there's no sensible concrete type we can substitute for `T`.
takesConcreteArrayOfP(array) 

For the same reasons, a function with multiple T parameters would also be problematic, as the parameters must take arguments of the same type – however if we have two P values, there's no way we can guarantee at compile time that they both have the same underlying concrete type.

In order to solve this problem, we can use a type eraser.

2. Build a type eraser

As Rob says, a type eraser, is the most general solution to the problem of protocols not conforming to themselves. They allow us to wrap a protocol-typed instance in a concrete type that conforms to that protocol, by forwarding the instance requirements to the underlying instance.

So, let's build a type erasing box that forwards P's instance requirements onto an underlying arbitrary instance that conforms to P:

struct AnyP : P {

  private var base: P

  init(_ base: P) {
    self.base = base
  }

  var bar: Int {
    get { return base.bar }
    set { base.bar = newValue }
  }

  func foo(str: String) { base.foo(str: str) }
}

Now we can just talk in terms of AnyP instead of P:

let p = AnyP(S(bar: 5))
takesConcreteP(p)

// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)

Now, consider for a moment just why we had to build that box. As we discussed early, Swift needs a concrete type for cases where the protocol has static requirements. Consider if P had a static requirement – we would have needed to implement that in AnyP. But what should it have been implemented as? We're dealing with arbitrary instances that conform to P here – we don't know about how their underlying concrete types implement the static requirements, therefore we cannot meaningfully express this in AnyP.

Therefore, the solution in this case is only really useful in the case of instance protocol requirements. In the general case, we still cannot treat P as a concrete type that conforms to P.

Solution 3

If you extend CollectionType protocol instead of Array and constraint by protocol as a concrete type, you can rewrite the previous code as follows.

protocol P { }
struct S: P { }

let arr:[P] = [ S() ]

extension CollectionType where Generator.Element == P {
    func test<T>() -> [T] {
        return []
    }
}

let result : [S] = arr.test()
Share:
21,318
matt
Author by

matt

Author most recently of iOS 15 Programming Fundamentals with Swift and of Programming iOS 14. Code examples from the books available at my github repository. A much earlier edition, Programming iOS 6, is available to read for free at http://www.apeth.com/iOSBook/. And the Swift language chapters of iOS 10 Programming Fundamentals With Swift are available at http://www.apeth.com/swiftBook/.

Updated on July 13, 2022

Comments

  • matt
    matt almost 2 years

    Why doesn't this Swift code compile?

    protocol P { }
    struct S: P { }
    
    let arr:[P] = [ S() ]
    
    extension Array where Element : P {
        func test<T>() -> [T] {
            return []
        }
    }
    
    let result : [S] = arr.test()
    

    The compiler says: "Type P does not conform to protocol P" (or, in later versions of Swift, "Using 'P' as a concrete type conforming to protocol 'P' is not supported.").

    Why not? This feels like a hole in the language, somehow. I realize that the problem stems from declaring the array arr as an array of a protocol type, but is that an unreasonable thing to do? I thought protocols were there exactly to help supply structs with something like a type hierarchy?

    • vadian
      vadian over 8 years
      When you remove the type annotation in the let arr line, the compiler infers the type to [S] and the code compiles. It looks like that a protocol type can't be used in the same way as a class - super class relationship.
    • matt
      matt over 8 years
      @vadian Correct, that is what I was referring to in my question when I said "I realize that the problem stems from declaring the array arr as an array of a protocol type". But, as I go on to say in my question, the whole point of protocols is usually that they can be used in the same way as a class - superclass relationship! They are intended to provide a sort of hierarchical structure to the world of structs. And they usually do. The question is, why shouldn't that work here?
    • Martin R
      Martin R over 8 years
      Still does not work in Xcode 7.1, but the error message is now "using 'P' as a concrete type conforming to protocol 'P' is not supported".
    • matt
      matt over 8 years
      @MartinR It's a better error message. But it still feels to me like a hole in the language.
    • Martin R
      Martin R over 8 years
      Sure! Even with protocol P : Q { }, P does not conform to Q.
    • matt
      matt over 8 years
      @MartinR That's the first thing I tried :)
    • Hamish
      Hamish almost 6 years
      Oh wow, thanks for the bounty @MartinR! :)
    • matt
      matt about 5 years
      Should see also stackoverflow.com/questions/42561685/… for more about array-of-protocol.
  • jsadler
    jsadler about 8 years
    Lots of useful information in this answer, but the actual solution in Tomohiro's answer is better than the boxing solution presented here.
  • matt
    matt about 8 years
    @jsadler The question wasn't how to work around the limitation, but why the limitation exists. Indeed, as far as explanation goes, Tomohiro's workaround raises more questions than it answers. If we use == in my Array example, we get an error, Same-type requirement makes generic parameter 'Element' non-generic." Why doesn't Tomohiro's use of == generate the same error?
  • AyBayBay
    AyBayBay over 7 years
    @Rob Napier I am still perplexed by your response. How does Swift see any more concreteness in your solution vs the original? You seemed to have just wrapped things in a struct... Idk maybe I am struggling to understand the swift type system but this all seems like magic voodoo
  • Rob Napier
    Rob Napier about 7 years
    @AyBayBay Updated answer.
  • AyBayBay
    AyBayBay about 7 years
    Thank you very much @RobNapier I am always amazed with the speed of your replies and quite frankly how you find the time to help people as much as you do. Nevertheless your new edits definitely put it into perspective. One more thing I'd like to point out, understanding type erasure also helped me. This article in particular did a fantastic job: krakendev.io/blog/generic-protocols-and-their-shortcomings TBH Idk how I feel about some of this stuff. It seems like we are accounting for holes in the language but Idk how apple would build some of this in.
  • matt
    matt about 7 years
    Maybe I'm just being dense, but I don't understand why the static case is special. We (the compiler) know just as much or as little about a prototol's static property at compile time as we know about a protocol's instance property, namely that the adopter will implement it. So what's the difference?
  • Hamish
    Hamish about 7 years
    @matt A protocol-typed instance (i.e concrete-typed instance wrapped in existential P) is fine because we can just forward calls to the instance requirements to the underlying instance. However, for a protocol type itself (i.e a P.Protocol, literally just the type that describes a protocol) – there is no adopter, therefore there's nothing to call the static requirements on, which is why in the above example we can't have SomeGeneric<P> (It's different for a P.Type (existential metatype), which describes a concrete metatype of something that conforms to P – but that's another story)
  • matt
    matt about 7 years
    The question I ask at the top of the this page is why a protocol-type adopter is fine and a protocol type itself is not. I understand that for a protocol type itself there is no adopter. — What I don't understand is why it is any harder to forward static calls to the adopting type than it is to forward instance calls to the adopting type. You are making an argument that the reason there is a difficulty here is because of the nature of static requirements in particular, but I don't see how static requirements are more difficult than instance requirements.
  • Hamish
    Hamish about 7 years
    @matt It's not that static requirements are "harder" than instance requirements – the compiler can handle both fine through existentials for instances (i.e instance typed as P) and a existential metatypes (i.e P.Type metatypes). The problem is that for generics – we're not really comparing like for like. When T is P, there's no underyling concrete (meta)type to forward static requirements to (T is a P.Protocol, not a P.Type)....
  • Hamish
    Hamish about 7 years
    ... it's only when we receive an instance of type P that we can forward calls to instance (and static) requirements. But with only the metatype of P – we just have the type that describes the protocol itself – there's no adopting type. However, the point I was making with static requirements is that if there's no static requirements to call in the first place, there's no problem – because we cannot even attempt to call them on the protocol type itself (although of course it could be argued that allowing exceptions to the rule would be inconsistent – but that's another discussion :) ).
  • Hamish
    Hamish about 7 years
    (and of course it's fine if we receive a metatype, i.e a T.Type method parameter – we can call static requirements on that just fine).
  • matt
    matt about 7 years
    Well, the protocol in my example has no static requirements. (In fact it has no requirements at all.) But it still isn't acceptable to the compiler.
  • Hamish
    Hamish about 7 years
    @matt Sure, Swift doesn't distinguish between these different cases at the moment, but might well do (hopefully) in a future version. There is an argument to be made for making the behaviour consistent – as adding a single static requirement could result in a whole lot of generic code suddenly not compiling (quite unintuitively) – even though they may not rely on static requirements (although that being said, this isn't too dissimilar to the situation with protocol with associated types – adding a single associatedtype or Self requirement can rain down errors).
  • Hamish
    Hamish about 7 years
    @matt This is the exact reason that Jordan Rose gives in this bug report for the behaviour btw: "This is correct in the general case: not all protocols conform to themselves. The simplest case is a protocol with initializer requirements: which concrete type would you construct?"
  • matt
    matt about 7 years
    I think focusing on the initializer is the best way to help people see this.
  • Hamish
    Hamish about 7 years
    @matt I agree – although I still think it's worth talking about other static requirements. I've edited the answer with (hopefully) a good compromise.
  • Hamish
    Hamish about 7 years
    It's worth noting that there are cases where this "shouldn't work", but they're around static requirements (including initialisers), as said in my answer below. But yes, in this particular case, it should be safe for P to conform to itself – although an argument could be made that making an exception to the rule could lead to surprising inconsistencies (although this isn't too different to the current situation with associatedtype requirements).
  • Jonathan.
    Jonathan. over 5 years
    I really don't care about soundness etc, I just want to write apps, and if it feels like it should work it just should. The language should just be a tool, not a product itself. If there's some cases that it really wouldn't work for then fine disallow it in those cases but let everyone else use the cases it works for and let them get on with writing apps.
  • J. Doe
    J. Doe over 4 years
    Hey Hamish, I read your answer about the existential. I have asked a question (stackoverflow.com/q/58962468/7715250) and Rob pointed me in a good direction about those wrapper types. The edge cases you describe when using a protocol type directly in a method, instead of using a generic type, is because of extensions on arrays and generic struct. However, in my case, I can not see any reason is can fail. I asked Rob in a comment if the type in the existential container is erased. If it isn't, I can not see a reason Swift doesn't compile, expect...
  • J. Doe
    J. Doe over 4 years
    ...for the fact of compiler limitations as you pointed out. Can you see a reason why it shouldn't compile for a simple stand-alone method not inside a generic struct or extension? Swift should be able to know the type of type protocol, even in the existential wrapper I believe.
  • imre
    imre over 4 years
    I don't think Collection vs Array is relevant here, the important change is using == P vs : P. With == the original example works too. And a potential problem (depending on context) with == is that it excludes sub-protocols: if I create a protocol SubP: P, and then define arr as [SubP] then arr.test() won't work anymore (error: SubP and P must be equivalent).