Generic class requires that generic type be a class type

Hello everyone. Could someone help me, please, with this piece of code?

protocol Service: AnyObject {}

final class ServiceImpl: Service {}

protocol Container<S> {
  associatedtype S: AnyObject
  
  func get() -> S
}

final class CustomContainer<S: AnyObject>: Container {
  let factory: () -> S
  weak var shared: S?
  
  init(factory: @escaping () -> S) {
    self.factory = factory
  }
  
  func get() -> S {
    if let shared {
      return shared
    } else {
      let shared = factory()
      self.shared = shared
      return shared
    }
  }
}

let container: any Container<Service> = CustomContainer { ServiceImpl() }

It seems that both Ss are constrained to be a class, and, in my opinion, the types are OK, but the compiler says "Generic class 'CustomContainer' requires that 'any Service' be a class type".

I also have tried the following variant, but the error remains the same.

let container: any Container<any Service> = CustomContainer<any Service> { ServiceImpl() }

If I delete AnyObject contraint from Container and CustomContainer, it compiles, but in this case I can't use weak var shared. As far as I understand it, seems any Service can't be constrained to a class, but maybe there is another reason or a workaround?

Will be happy to hear any advice. Thank you!

You need to provide a concrete type for <Service>, not a protocol. This compiles:

let container: any Container = CustomContainer { ServiceImpl() }

The problem is that the protocol Service itself is not a class type. protocol Service: AnyObject only means that types conforming to the protocol are class types. So Service (or any Service) doesn't fulfill the requirements of Container.S: AnyObject and CustomContainer.S: AnyObject.

Note that this also compiles (with no constraint on container2.S):

let container2: any Container = CustomContainer { ServiceImpl() }

The difference is that container2 has no information about the concrete type of container2.S, but this also allows you to assign other values to it that use a different concrete Service type. Which variant is better for you depends on your needs.

1 Like

I think the issue is that your class declaration:

class CustomContainer<S: AnyObject>

declares a generic type that requires a concrete type parameter as the replacement for S. However, you also have this:

class ServiceImpl: Service {}

Because Service is a protocol, this declares an existential type, equivalent to:

class ServiceImpl: any Service {}

The error is that you can't use an existential type as the substitution for a generic type parameter requiring a concrete type.

If I got that explanation right (:sweat:), you could argue that the error message is a bit misleading, but I think it's right that there is an error here.

1 Like

Only a handful of existential types can conform to AnyObject, namely protocol compositions that contain @objc existentials and marker protocols like Sendable.

If you have protocol P: AnyObject {}, then any P is actually represented as a pointer together with the witness table, so any P does not satisfy an AnyObject requirement.

2 Likes

Got it, thank you. Is there any workaround? Make Service a class instead of a protocol?

You might also be able to use existential opening:

func makeCustomContainer<T: Service>(_ service: T) -> any Container {
  return CustomContainer<T> { service }
}

let myService: any Service = ServiceImpl()
let container = makeCustomContainer(myService)
1 Like

Can this ability be extended for other protocols? Is there any limitations for it?
I clearly understand that existential itself is not a class. But from the user perspective it is strange when a protocol inherited form AnyObject can not be used as a class type – it is proven at compile time that existential underlying instance is a class.
After introducing existential unboxing when passing them to generic arguments it seems to be achievable.

1 Like

Sorry, probably I don't quite understand the solution.

let service: any Service = container.get() // Value of type 'AnyObject' does not conform to specified type 'Service'

This line doesn't compile with your version. How to get Service instance from this container?

Your container has generic restriction to AnyObject, returning any Container from the function as were suggested erases notion of underlying type to be Service/ServiceImpl and leaves you with just AnyObject. You need to expose either constraint or concrete type somewhere. For example, you can constrain Container.S to require its conformance to Service:

protocol Container<S> {
    associatedtype S: Service
}

While if you do not want to constraint it to Service protocols only, following @Slava_Pestov suggestion, you can make it work in the following way:

func makeContainer<T: Service>(_ s: T) -> some Container<T> {
  return CustomContainer { s }
}

let myService = ServiceImpl()
let container = makeContainer(myService)
let service: any Service = container.get()

Which on interpackage level will work roughly that way, I assume:

enum Pkg {
    private final class ServiceImpl: Service {
    }

    static func makeService() -> some Service {
        return ServiceImpl()
    }

    static func makeContainer<T: Service>(_ s: T) -> some Container<T> {
        return CustomContainer { s }
    }
}

let myService = Pkg.makeService()
let container = Pkg.makeContainer(myService)
let service: any Service = container.get()

One way or another, if your Container type isn't restricted to have S: Service, returning just any Container will lose information on that type and the only way would be to cast it with as, but that removes any point of using generics here in the first place.


Also, note that with weak reference here,

If instance returned by factory won't be hold somewhere with strong reference to it, your shared instance might be quite often nil.

1 Like

It seems all the ideas are about one approach: eliminate abstraction of Container and pass a concrete type/source (in your case, Pkg, which provides opaque Container). Or, in my variant, make Container a class, not a protocol.

Yep, that's planned to work this way.

Thank you for your solution :+1:t2:

2 Likes