Same-type Requirement Error and Implicit Existential Opening

Disclaimer: I'm still learning about the generics system. The following text is likely to be very confused and possibly has a simple solution :slight_smile:

Howdy,

Given a code that looks roughly like this

protocol EntityState {}

protocol Entity {
  associatedtype State: EntityState
  var state: State { get set }
}

I would like to instantiate and configure a generic object using an any Entity. e.g.

extension Entity {
   static let all: [any Entity] = [...]
}

final class EntityViewController<EntityType: Entity>: UIViewController {
   ...
}

// I would like to be able to do something like this
let targetEntity = Entity.all.first!
let vc = EntityViewController(targetEntity) // EntityViewController<...>

I've noticed through experimentation that opening seems to fail with a compiler error in the following two cases:

  1. When the function that is to do the opening uses the type's generic directly ("Type 'any Enitity' does not conform to 'Enity'").
  2. When the function that is to do the opening returns a concrete type (nonzero exit code?)

If these are even real limitations, then they eliminate the following possibilities for a function that "instantiate(s) and configure a generic object using an any Entity".

final class EntityViewController<EntityType: Entity>: UIViewController {

   // .init(targetEntity) Fails because 1 & 2
   init(_ entity: EntityType) {...}

   // .init(targetEntity) Fails because of 2. and even if it worked, it gives a scary warning that I'll get back to later.
   init<InitEnityType>(_ entity: InitEntityType) where EntityType == InitEntityType {...} 

   // .makeAndConfigure(basedOn: targetEntity) Fails because 1 & 2
   static func makeAndConfigure(basedOn entity: EntityType) -> Self {...}

   //  .makeAndConfigure(basedOn: targetEntity) Fails because of 1
   static func makeAndConfigure(basedOn entity: EntityType) -> UIViewController {...}

   //  .makeAndConfigure(basedOn: targetEntity) Fails because of 2 (also gives scary warning)
   static func makeAndConfigure<InitEnityType>(basedOn entity: InitEnityType) -> Self where EntityType == InitEntityType {...} 

}

I believe this leaves me with one remaining option for creating an initializer-like function that can open an any Entity (while remaining in EntityViewController's namespace and maintaining type inference).

// Swap 'Self' on the last example for 'UIViewController'
final class EntityViewController<EntityType: Entity>: UIViewController {
   static func makeAndConfigure<InitEnityType>(basedOn entity: InitEnityType) -> UIViewController where EntityType == InitEntityType {...} 
}

Good news! This works.

let targetEntity = Entity.all.first!
✅ let vc = EntityViewController.makeAndConfigure(basedOn: targetEntity) // EntityViewController<...>

Bad news! I get something like this in the console.
Same-type requirement makes generic parameters 'InitEntityType' and 'EntityType' equivalent; this is an error in Swift 6

Which I guess makes sense, but its not good!

If possible I would like to implement an interface with a similar shape that won't break. Anyone got any advice?

Thanks,
smkuehnhold

If two generic parameters are intentionally made equivalent by a same-type requirement, then you may as well delete the second generic parameter entirely, since it no longer serves a distinct purpose. But since this is not usually the intended outcome, the warning is there to tell you that potentially have made a mistake in a where clause.

Now, consider this code:

protocol P {}
struct S: P {}

class C<T: P> {
  init(_: T) {}
}

let p: any P = S()
C.init(p)

(You can also write C instead of C.init). The reason this is rejected is that the type of the initializer C.init is <T> (T) -> C<T>. While it is valid to open an any P and bind it to T for the parameter, the return type of the initializer allows the opened type to "leak out" inside C<T>. It is not correct to replace T with any P there because C<any P> is not a subtype (or supertype) of any other C<T>.

The trick with the UIViewController works because the static function returns a base class and not the generic class itself (you've erased the type parameter T).

In fact I would consider it a bug that this works:

class C<T: P>: Base {
  init(_: T) {}

  static func f<U: P>(_ t: U) -> Base where T == U { return C(t) }
}

But this doesn't:

class C<T: P>: Base {
  init(_: T) {}

  static func f(_ t: T) -> Base { return C(t) }
}

The call C.f(p) where p: any P should be type-safe. @Douglas_Gregor, what do you think?

In the mean-time, can you use a top-level function instead of a static method?

protocol P {}
struct S: P {}

class C<T: P>: Base {
  init(_: T) {}
}

static func f<T: P>(_ t: T) -> Base { return C(t) }

let p: any P = S()
f(p)
5 Likes

Thanks for the reply @Slava_Pestov !

I totally can use a top-level function! (It is just not preferred)

Also one question if you don't mind :slight_smile:.

I don't know if I fully get the point you are making here yet, but let me check my understanding. What is the reason that the compiler tries to return a C<any P> when calling C.init(pExistential) rather than a C<T> where T is the opened up type of pExistential? Is it because the return type has to be statically known? Are there other reasons?

Even with existential opening, any P still does not conform to P, so C<any P> is not a valid type because C requires that T conforms to P. When you call C.init, the type parameter T is bound to the "opened type" stored inside the any P. However there's no way to spell this type out or refer to it--indeed, the opened type depends on the specific runtime value of type any P that it was derived from. If there was a way to denote this, then C.init could return C<typeOf(p)> or whatever, and this would be a different type than C<typeOf(q)> even if q also had type any P.

1 Like