Very confused about some compiler warnings related to protocols—bugs or intended?

Problem description:

I keep getting this compiler error when trying to use variables whose type is declared as conforming to a protocol:

Protocol type FooProtocol cannot conform to FooProtocol because only concrete types can conform to protocols.

Could someone please let me know if this is a bug, or expected behavior? If it's expected, why? I mean, what is the point of protocols if you cannot use a given instance purely on the basis of its conformance to a protocol?

Example:

Given:

protocol FooProtocol { var bar: Int { get set } }

class FooClass: FooProtocol { var bar = 42 }

struct Bar<F: FooProtocol> {
    typealias MyFoo = F
    var foo: MyFoo
    init(foo: MyFoo) { self.foo = foo }
}
struct Whee {
    var someSpecificFoo: FooProtocol
    func process() { let bar = Bar(foo: someSpecificFoo) } // error line
}
var whee = Whee(someSpecificFoo: FooClass())
print(whee.someSpecificFoo.bar)

Strangely, the compiler fails with:

Protocol type FooProtocol cannot conform to FooProtocol because only concrete types can conform to protocols.

This seems like a buggy error, because this statement is basically incorrect. The compiler ought to be able to recognize that someSpecificFoo is a concrete instance conforming to FooProtocol because that's what var someSpecificFoo: FooProtocol means.

If we change the declaration to, var someSpecificFoo: some FooProtocol, all this does is to change the error to:

Property declares an opaque return type, but has no initializer expression from which to infer an underlying type

Why does the compiler now fail to auto-synthesize an initializer expression like normal?

Even if we add an explicit initializer, it will still fail. Adding:

    init<F: FooProtocol>(foo: F) { self.foo = foo }

... just changes the error message to:

Cannot assign value of type F to type some FooProtocol

Why can't the first error tell us how to fix the problem, instead of suggesting a corrective action that (if you try it) will just change the error to something else?

More importantly, why can't F be considered some FooProtocol? F conforms to FooProtocol by definition.

And in my codebase there is only one type that conforms to FooProtocol, which means that even if the compiler just guessed, it would have a 100% chance of being correct about what the actual underlying type must be. So why do we get these errors?

Workarounds:

Just on a hunch I tried adding @objc to the protocol declaration and import Foundation to the top line, and boom, now the code works perfectly.

But why?

And are there downsides to adding @objc here? If so what are they? If not, why isn't it just like that by default?

This is not a full answer with respect to some of the why's, but the gist of the failure here is that

var someSpecificFoo: FooProtocol

declares a variable which contains some unknown type which you know to conform to FooProtocol, while

init<F: FooProtocol>(foo: F)

declares an initializer which takes a specific known type F which you know to conform to FooProtocol. Effectively, you cannot pass a value of "unknown" (dynamic) type where you must pass in a value of "known" (static) type.

There is also a sharp distinction between someSpecificFoo: FooProtocol which is that it can be assigned any value conforming to FooProtocol, while some FooProtocol means "this value conforms to FooProtocol and has a known static type, but that type is hidden behind an opaque protocol constraint".


As for the wording of the diagnostic specifically: you're right that it's not very descriptive, but this is a very difficult-to-describe area of the language that's been very difficult to boil down into a one-statement error message that's easy to digest.

This diagnostic comes up a lot and there might be merit to rephrasing it — although it is technically correct in a very compiler-y way, it's not terribly helpful. It might be good to better recognize this case and simplify the message to: "cannot pass value of dynamic protocol constraint 'FooProtocol' to function expecting generic type 'F: FooProtocol'", though this may be non-trivial.

1 Like

However, a protocol type does not conform to protocols

I am not talking about a protocol type, I'm talking about an instance of a protocol type—which does, by definition, conform to that protocol.

If an instance of a protocol type did not conform to the protocol then it would just be an instance of Any and protocols would be useless.

For protocols with static method, initializer, or associated type requirements, the implementation of these requirements cannot be accessed from the protocol type

No, but all of those can be accessed from an instance of a protocol type—to wit, the following compiles and runs just fine:

protocol FooProtocol { 
    var bar: Int { get set } 
    init(bar: Int)
    static func perform()
}

extension FooProtocol {
    func instancePerform() {
        Self.perform()
    }
}

struct Foo: FooProtocol {
    var bar = 42
    init(bar: Int) {
        self.bar = bar
    }
    static func perform() {
        print("performing...")
    }
}

struct Bar {
    let foo: FooProtocol = Foo(bar: 96)
}

Bar().foo.instancePerform()
type(of: Bar().foo).perform()
type(of: Bar().foo).init(bar: 104)

But we are never passing an unknown type—when it gets passed in there, it will be known, because the runtime knows the type (otherwise how would it pass it in).

Right?

Or is there a situation where the runtime won't know the type?

The key here is that by design generics require static type information; dynamic type information (which is indeed provided at runtime) is insufficient.

See also "Existential Types and Generics" for some specifics on this (and in general, all of Generics.rst may be helpful, including "Implementation Model" and "Specialization").

2 Likes

I can see that there is a technical difference under the hood, regarding a difference between how existentials and generics are handled, with relation to vtable lookup.

However lets look at what this means for Swift developers:

  • cryptic, counter-intuitive, and often self-contradictory compiler warnings
  • just resort to @objc and the problem goes away (but at what cost?)
  • the only alternative seems to be kludgy type-erasure schemes
  • so people just avoid adding Equatable or Hashable like the plague since it breaks everything

Seems to me, if we're going to be forced to resort to type-erasure anyway, couldn't the compiler just auto-synthesize the type erasure code behind the scenes? Would there be a drawback to that?

I'm not sure what you mean here; only types conform to protocols, not instances.

The function type(of:) gives you the dynamic type of a value. This is distinct from the static type.

The distinction between the dynamic type and the static type is not an "under the hood" matter. It is part of the user-facing semantics of Swift.

In the general case, it is impossible to conform every existential type to its own protocol automagically. This is not a limitation of Swift.

Consider the Comparable protocol:

You can conform type A to Comparable, providing an implementation such that (a1 < a2) == true. I can conform type B to Comparable, providing an implementation such that (b1 < b2) == true. However, if the existential type Comparable conforms to itself, then that implies we could write:

let a1c = a1 as Comparable
let b1c = b1 as Comparable
print(a1c < b1c)
// If Comparable conforms to itself, then we can compare the two values.
// But what is the result?

The compiler cannot pull an implementation of such an operation out of thin air. Yes, we can arbitrarily create a rule which sorts every value of type A before every value of type B by lexical ordering of type names, but someone would need to actually make that arbitrary decision and write it out in the form of a concrete implementation of static func < for the existential type.

(Then, someone else would complain that it would be a very silly implementation, because it would imply (100 as Int) < (0 as UInt); thereby demonstrating that any reasonable implementation requires specific knowledge of the semantics of the requirement being implemented as well as of any relationships among conforming types. You can peruse the multiple revisions of AnyHashable to see how difficult it is to implement a hash value that fulfills all the required semantics.)

The same applies to static methods, initializers, and Self or associated type requirements in every protocol.

Sure, we can (and eventually should, IMO) relax the rules around which protocols can be used as existential types, change the spelling of existential types from P to any P, and even allow users to implement self-conformance by writing extension any P: P { ... }, but the implementation itself (the part that's elided here with ...) will have to be manually written. There is no "auto-synthesis" here in the general case.

8 Likes

To expand on what @xwu said below: this is very much not an under-the-hood difference, but a semantic difference, and even more so, a feature difference. Existential types and generic types in Swift are two separate but very important language features that give you different tools for your toolset. Even though they overlap in some significant aspects, there are important use-cases that the other cannot resolve:

  • Existential types allow you to represent a dynamic range of types which are unknown and cannot be known at compile time
  • Generic types give you access to associated types, specialization, (occasionally) static dispatch, different optimizations, and more

So, a bit of a frame shift: given two separate feature sets that happen to have some overlap, instead of trying to work around where they are disjoint, is it possible for your use-case to lean into one or the other?

My instinct for your (likely simplified) code example would not be to work around the issue but to lean into generics and make Whee generic on the specific type F: FooProtocol that you want to store (and eventually create a Bar from). If this is not possible, consider why not; for instance, if Whee.someSpecificFoo needs to by dynamically assignable to any FooProtocol instance, then you're using the core feature that existential types exist for; in that case, is it possible for Bar.init(foo:) to take an existential instead of a generic type? (If not, then you have more thinking to do, because clearly in some cases you need explicitly dynamic type instances, and in others you need explicitly static type instances. There are ways to solve this, but it will be specific to your use-case.)

Can you share a little bit more about your concrete use-case? There should definitely be ways to resolve this without workarounds.

1 Like

I definitely take @1oo7's point that the error message and underlying failure here are quite confusing—a quick search of StackOverflow turns up countless results for the same issue. It's also true that in the 'easy' cases, such as FooProtocol, it's conceptually sound for a protocol to conform to itself. After all, the static type FooProtocol obviously trivially satisfies the requirements of FooProtocol.

But as soon as the protocols start getting more complex (such as in the ways @xwu mentioned), there's a lot of tricky questions to be answered, and the language currently doesn't have the expressive power to cover those cases. Adding a one-off rule of "'simple' protocols may self-conform" without the additional features needed to generalize the rule may leave the language in a worse resting place than we have currently (or, maybe not! I'm unsure whether there's been extensive discussion around allowing self-conformance for a very limited subset of protocols).

To your point about being able to access static or init requirements from an instance, @1oo7, it's worth pointing out that Swift's generics model doesn't require that a generic parameter be attached to an instance at all. Consider:

protocol FooProtocol { 
  var bar: Int { get set } 
  init(bar: Int)
  static func perform()
}

struct FooCreator<T: FooProtocol> {
  func giveMeAFoo() -> T { T.init(bar: 0) }
}

Right now, there's simply no way for a FooCreator<FooProtocol> to function. Yes, if we had some foo: FooProtocol we could do type(of: foo).init(bar: 0), but that's not going to be possible generally since generic functions and types can be parameterized by just the type.

What you are looking for is "opening existential type". This was discussed in Improving the UI of generics, but not yet implemented.

With the hypothetical syntax from the the discussion it would look like this:

func process() {
   let <X: FooProtocol> openedFoo = someSpecificFoo 
   let bar = Bar<X>(foo: openedFoo)
}

Current workaround for that is to move code depending on the opened type to the protocol extension, like you did with instancePerform().

On master, the diagnostic is slightly improved:

error: protocol 'FooProtocol' as a type cannot conform to the protocol itself; only concrete types such as structs, enums and classes can conform to protocols
    func process() { let bar = Bar(foo: someSpecificFoo) } // error line
                               ^
note: required by generic struct 'Bar' where 'F' = 'FooProtocol'
struct Bar<F: FooProtocol> {
       ^
3 Likes

After reading this thread I have some trouble with my understanding of the definition of existential type.

Quoting from this link that was posted in the first comment:

https://github.com/apple/swift/blob/master/userdocs/diagnostics/protocol-type-non-conformance.md

However, a protocol type does not conform to protocols - not even the protocol itself. Allowing existential types to conform to protocols is unsound. For protocols with static method, initializer, or associated type requirements, the implementation of these requirements cannot be accessed from the protocol type - accessing these kinds of requirements must be done using a concrete type.

Please correct where I'm wrong:

  1. In my understanding an existential type in Swift is a type that can – at runtime – contain different concrete types that all conform to the same protocol.
  2. The existential type implementation is implicitly generated by the compiler and uses some form of dynamic dispatch.
  3. For protocols that have associated type requirements it's not possible to create an existential type.

This would imply that existential types always conform to their protocol, otherwise they couldn't be created in the first place.

Example from this thread:

let a1c = a1 as Comparable
let b1c = b1 as Comparable
print(a1c < b1c)
// If Comparable conforms to itself, then we can compare the two values.
// But what is the result?

I think this already fails before the comparison, because Comparable cannot be used to create an existential:

let a1c = a1 as Comparable
// Protocol 'Comparable' can only be used as a generic constraint
// because it has Self or associated type requirements

Coming back to @1oo7's initial question, if existential types always conform to their protocol this would imply that allowing them to be used to specialize generic parameters shouldn't create any soundness issues.

Any type-level requirements (initializers, static methods) also become problematic as @xwu mentioned and as I illustrated above. You won’t always have access to a dynamic type via an instance, so these requirements need to be invokable via the static type, which isn’t compatible with the way protocols/existentials work (currently).

Ah, thanks, missed that on the first read.

Adding a one-off rule of "'simple' protocols may self-conform" without the additional features needed to generalize the rule may leave the language in a worse resting place than we have currently (or, maybe not! I'm unsure whether there's been extensive discussion around allowing self-conformance for a very limited subset of protocols).

I think it would be a good idea to allow "simple" protocols to self-conform, it's not an uncommon case. Maybe with a slightly adapted error message:

error: protocol 'FooProtocol' as a type cannot conform to the protocol itself
 because it has static method or Self requirements;
 only concrete types such as structs, enums and classes can conform to 'FooProtocol'

For the past month or so, several people here have requested to have existential types for protocols with associated types when all of said associated types are fully specified. This still won't help with protocols with Self requirements, though.

Terms of Service

Privacy Policy

Cookie Policy