Associated types constrained by `AnyObject` can't be matched?

Hi all, I've stumbled upon a problem, which I'm not sure is caused by the intended behavior of the compiler. Consider this code:

protocol P { 
  associatedtype T: AnyObject
  func f(x: T) 
} 

protocol AO: AnyObject {}

struct S: P { 
  func f(x: AO) {} 
}

The code causes these errors:

error: repl.swift:3:8: error: type 'S' does not conform to protocol 'P'
struct S: P { func f(x: AO) {} }
       ^

repl.swift:3:20: note: inferred type 'AO' (by matching requirement 'f(x:)')
is invalid: does not conform to 'AnyObject'

This doesn't make any sense to me as AO clearly conforms to AnyObject by definition.

Could someone please clarify what's going on here?

You will have to remove the AnyObject constraint because existentials don't conform to AnyObject.

Check this similar JIRA issue:

https://bugs.swift.org/browse/SR-6109

Thanks for the link, but this also doesn't make sense to me. It clearly looks like they do, we can verify that with this code:

import Foundation

protocol AO: AnyObject {}
extension NSNumber: AO {}
let x1: AO = NSNumber(value: 42)
let x2: AnyObject = x1

We've declared AO protocol that conforms to AnyObject and we can use this protocol as an existential for x1. Then we can freely upcast AO to AnyObject for x2.

That does not proof the conformance of an existential to a protocol. Protocols do refine other protocols, but they don't conform themselves to the protocols they refine. You can think about it as if it says "every type that conforms to AO should also conform to / be AnyObject". That's why your cast succeeds with no issues, because the hidden type in x1 is known to be AnyObject inside the A0 existential. (If anyone spots wrong terminology in my wording, please feel free to correct me.)

Here is a different example. Metatypes simply describe other types in Swift, but there is a similar issue there which can lead to errors.

protocol P: AnyObject {}
class C {}

print(type(of: P.self)) // P.Protocol

func test<T: AnyObject>(_: T.Type) {}

test(C.self) // okay
test(P.self) // not okay: Cannot invoke 'test' with an argument list of type '(P.Protocol)'

In the last line P.self is not an instance of P.Type (which can theoretically exist, but is impossible to construct in Swift), but an instance of P.Protocol, a different metatype that distinguishes from P.Type in one way, it's not a sub-type of AnyObject.Type.


Let me try to explain it in some pseudocode that should represent a sub-typing relation in Swift.

  • A : B should mean that A is a subtype of B, and B is a super type of A

That creates the following relationship:

  • Any : Any // subtype of itself
  • Any.Type : Any
  • Any.Type : Any.Type
  • Any.Protocol : Any // Is not a subtype of Any.Type
  • AnyObject : Any
  • AnyObject.Type : Any
  • AnyObject.Type : AnyObject.Type
  • AnyObject.Protocol : Any
  • P : Any
  • P.Type : Any
  • P.Type : Any.Type
  • P.Type : P.Type
  • P.Protocol : Any
  • AO : Any
  • AO.Type : Any.Type
  • AO.Type : AnyObject.Type
  • AO.Type : AO.Type
  • AO.Protocol : Any
  • S : Any
  • S : P
  • S : S
  • S.Type : Any
  • S.Type : Any.Type
  • S.Type : P.Type
  • S.Type : S.Type

Maybe I missed a few relations, but you should get the idea now.

Here's another example, which I think makes it clear that even if this isn't valid code, the error message is very confusing:

protocol AO: AnyObject {}
protocol P {  
  associatedtype T: AO 
  func f(x: T)  
}
protocol AO1: AO {}

struct S: P {
  func f(x: AO1) {} 
}

does not compile with

error: type 'S' does not conform to protocol 'P'
unable to infer associated type 'T' for protocol 'P'
inferred type 'AO1' (by matching requirement 'f(x:)')
is invalid: does not conform to 'AO'

Even if we assume that "existentials don't conform to AnyObject" (I'm still unsure what that means, why that restriction exists and what problems does that restriction solve), the error message clearly contradicts the written code. It says AO1 does not conform to AO, while it clearly does conform to that protocol right there in the example code about which the compiler didn't complain at all: protocol AO1: AO {}.

Sorry, but this won't work in my broader example, where I have to have the AnyObject constraint to avoid reference cycles with weak variables:

protocol GenericAlgorithm { 
  associatedtype T: AnyObject
  func f(x: T) 
} 

struct Weak<W: AnyObject> {
  weak var value: W?
}

struct WeakStorage<A: GenericAlgorithm> {
  // If `AnyObject` constraint is removed, I can't store `T` 
  // in `weakReferences`
  var weakReferences = [Weak<A.T>]()
}

And there are intended "users" of these types in a completely separate module:

protocol Things: AnyObject {
}

struct Algorithm: GenericAlgorithm {
  func f(x: Things) {}
}

struct AlgorithmWithStorage {
  let storage = WeakStorage<Things>()
  let algorithm = Algorithm()

  // more code here that binds `storage` with `algorithm`
}

The names of types are a bit contrived, but I hope this makes sense. Maybe there's a good way to work around the AnyObject restriction for associatedtype, while still being able to store an array of weak references?

The constraint you're looking for is not expressible in todays Swift, I've been there myself and what you've written had compiled previously but crashed the compiler (see the linked issue above).

To understand existential types better here is a good explanation:

In your case you must provide a concrete type that is not a protocol or it won't compile. To workaround the issue you could try box your protocol.

final class ThingsBox: Things { ... }

struct Algorithm: GenericAlgorithm {
  func f(x: ThingsBox) {}
}

Here is a great example how one can write a box type:


Protocols do not conform to themselves, so do not existentials. That is a limitation in current Swift we have to live with. However starting with Swift 5 we have the first protocol that doesn't have that restriction, the Error protocol. It's a great goal of Swift to lift this restriction one day as much as possible.

Thank you, I know what an existential type is. In fact, I do have a tree of boxes already. The main problem is that the base box class itself is generic. To avoid the generic leaking out I had to create the existential protocol, which erased the generic type and all was fine until I needed weak references to improve memory usage. A single addition of weak adds AnyObject constraint and breaks everything.

Maybe I'll be able to create a "base base" non-generic class instead of the existential protocol I had previously...

The protocol AO1 refines the protocol AO, which means that anything that conforms to AO1 also conforms to AO.

However, (the existential type) AO1 does not conform to (the protocol) AO1. Protocols do not conform to themselves--this is not a restriction related to AnyObject. The restriction exists because, in the general case, a protocol cannot conform to itself. For further explanation of this point you may see any of the previous multiple threads about protocols conforming to themselves.

Note that as a special case, Error, which has no requirements, now conforms to itself as of Swift 5.

3 Likes

Thank you @DevAndArtist! Replacing the existential with a base class worked around the compilation errors.

I still think that the compiler error text could be significantly improved to avoid the confusion and to give Swift developers more clear direction on how their code could be fixed.

@xwu, thank you for the explanation, this made it crystal clear:

Do you folks think that separating simple existentials from protocols syntactically would bring more clarity to the language and would help avoiding this sort of confusion? Thus we would stop conflating the protocol and the existential type created with the protocol? Do I understand correctly that an existential type based on a protocol is roughly equivalent to Any where Self: P and could be actually written that way if we had generalized existentials working with this kind of syntax?

This has already popped up a few times I think, like here:

1 Like

I'm still trying to understand existentials, so maybe a dumber-level view can show the problem.

For S to conform to P, S.f has to have its parameter to be of a specific type (to match T). You didn't specify a single type; you specified a protocol which could have an arbitrary number of types that could match. Protocols and types aren't (always) interchangeable.

Thank you, that's a great way to put it. Also, in my current understanding in the type system those aren't interchangeable at all, but this fact is hidden by the syntax which automatically generates existentials for you.

Do I understand it correctly that the following two functions f1 and f2 are equivalent and on a lower level are compiled to very similar (if not completely the same) LLVM commands? And the way to write f1 is a "syntax sugar" to actually get the signature of f2?

protocol P {}
func f1(x: P) {}
func f2<T: P>(x: T) {}

No, they are not. f2 will use the static knowledge of T to be compiled in a more efficient way, while f1 will have to be stuck with the existential. They're also different at the language level, as if you happen to have a let foo: P = ... you cannot call f2 with it.

It's similar to my last response. f2 is a function that takes an exactly specified type, and there are separate instantiations for each T used. There is only one f1 that all types that conform to P share.

1 Like

Hm, isn't the number of instantiations for a generic function an implementation detail for a user of Swift? Here the doc in the compiler repository describes the implementation of generics, which makes it look like f1 and f2 do look the same on some level unless @_specialize is used or some optimizations are applied:

Because generics are constrained, a well-typed generic function or type can be translated into object code that uses dynamic dispatch to perform each of its operations on type parameters. This is in stark contrast to the instantiation model of C++ templates, where each new set of template arguments requires the generic function or type to be compiled again. This model is important for scalability of builds, so that the time to perform type-checking and code generation scales with the amount of code written rather than the amount of code instantiated. Moreover, it can lead to smaller binaries and a more flexible language (generic functions can be "virtual").

protocol P {}
func f1(x: P) {}
func f2<T: P>(x: T) {}

The difference between f1 and f2 here is how the information about the conformance to P is packaged up. With the existential value (the Any where Self: P; that's a good way of putting it), the value of x is packaged up with the conformance to P (the conformance of x's dynamic type to P). In the generic case, the conformance info is passed separately as a sort of hidden argument. There are a few more implementation-level differences, but you're still pretty correct that a client can't really tell the difference and probably doesn't care.

Things start getting more relevant for either more uses of the same generic type:

func g1            (x: P, y: P) {}
func g2<T: P>      (x: T, y: T) {}
func g3<T: P, U: P>(x: T, y: U) {}

or for uses in contexts other than functions:

struct S {
  var value: P
}
var s: S = …
s.value = P1()
s.value = P2()
struct S<T: P> {
  var value: T
}
var s: S<P1> = …
s.value = P1() // okay
s.value = P2() // error

So, getting back to your original question: why don't class-constrained protocols fit in an AnyObject-constrained value? Because being an AnyObject means being a single retainable pointer, but existentials have extra information packed in about how their value conforms to the protocol.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy