Is there possible to support inheriting from a generic type: class classA<T: classB>: T?

Recently, I tried to write a generic class to extend UIViewController and its all subclasses, so I wrote some codes like this:

class MyBaseViewController<T: UIViewController> : T {
 ...
}

As you know, the complier reports error for it with:

inheritance from non-protocol, non-class type 'T'

I know the swift doesn't support it now, but does it possibly support it in the future? The reason that I want this feature is that I really don't want to write the duplicated codes for every subclass of UIViewController, I know extension can do something, but I want to add extra member variables not only member functions, and I also know there is a workaround to add member variables with objc_setAssociatedObject, but obviously it is not elegant. If swift supports it, I can easily write codes like:

class MyBaseViewController<T: UIViewController> : T {
   var myVar: Int
   ....
   func myFunc() {...}
   ....
}

When using, I can write some classes like:
class MyTableViewController :  MyBaseViewController<UITableViewController> {
   ...
}

class MyPageViewController : MyBaseViewController<UIPageViewController> {
   ....
}
```
Thanks!
2 Likes

There are some really tricky problems with this at the implementation level, but I think the bigger problem is the user-level semantics of initializers: there's no way to call super.init from MyBaseViewController because there's no way of requiring T to provide an initializer that can be called that way. init requirements in protocols only say that the conforming type has to provide a complete-object initializer with that signature, i.e. something that can be called with T(...), not a sub-object initializer that can be called with super.init(...). Those are semantically very different things; if we required init requirements to be callable with super.init, you wouldn't be able to satisfy them with convenience initializers or factory initializers (which already exist in the ObjC world and which we'd like to add someday to Swift). That would be a very severe restriction to enable what's really a very minor special-case feature. (It would also be both a source-compatibility and a binary-compatibility break.)

We could add a way to abstract over sub-object initializers, but that seems like a lot of complexity just to enable this fairly minor convenience.

7 Likes

My knowledge probably lacks here and there, but the pitched pattern looks like a reversed CRTP which @Slava_Pestov made possible for Swift 5. Is there really no chance to allow this in some far far away Swift version? I don‘t have a concrete use case for it, but you never know what this could allow us to express what we currently can‘t. In case of the original problem, allowing stored members in extensions is the ultimate goal of Swift, but it‘s not yet clear how we can solve all the involved issues.

Being able to express more generic code in different ways is always a plus for me. Ignoring the pitched limitation to classes, then together with automatic forwarding, with some static compiler analysis and something like 'generalized super-type constrains’ (cc @anandabits) I could imagine this reversed CRTP to fake opaque types.

// the compiler would allow this because of
// generalized super type constraint
struct Opaque<T>: T {
  let value: T
}

// the compiler must statically prove the type
// compatibility.

Opaque<String> // not okay
Opaque<Collection> // okay
Opaque<Collection<.Element == Int>> // okay
RefOpaque<UIView & P> // we‘d need an extra ref opaque type

Maybe it‘s too late as the proposal is already in review and I don‘t know what the final decision would be as there was some nagative feedback during the review, but could this be a potential alternative solution that could live in the stdlib rather than a compiler feature (in case of potential rejection of the core feature)? cc @Joe_Groff

I had the idea to pitch this some day under "Generalized Subclassing". If I could gather the motivation, that is, and somehow find an acceptable solution for all the problems I discovered. One I recall is illusive genericity: the compiler would have to statically validate each specialization for things like overriding a final method or a writable property with a read-only one. Conditional extensions, regular and conditional conformances make it all the more untamable. Initializers, well, we could require such classes to always inherit the superclass' initializers, in which case this feature becomes primarily a behavioral modification tool via overrides.

Or we could consider something currently inexplicable like:

Foo<SuperClass>.init(args1...) {
  super.init(args2...)
}

Foo<SuperClass>.init(args1...).init(args2...)

Anyway, the reason this occurred to me is this RoundedView file I have that had almost every UIView subclass further subclassed in a similar fashion. The matter settled a bit once I was able to transfer most of the implementation to a single custom layer. Now there are some complex UIView subclasses that fail to work properly due to internal implementation details, but generalized subclassing should not guarantee behavioral full coverage. It would of course be nice to have this ability, even if we fully depend on inherited initializers.

What would happen if two classes did this? Would they both be subclasses of each other?

I don't think this is sufficient justification for including what is going to be a very complex feature to implement.

7 Likes

Thanks for your detailed response. I can understand that supporting this feature would bring many impacts and some unknown risks and problems. It would be a great challenge to change something in a mature system. But I still think this feature is useful as we're seeing it in the C++ language. I don't know the underlying implementation of swift and also I have no any experiences about compiling. I just simply think:

  1. T should have a constraint with a class or protocol in this case, like my example: T must be a subclass of UIViewController or itself.
  2. When T is not unspecified, we can use its constraint to simply check MyBaseViewController implementation, e.g: its initializer, member functions, member variables. (I know the initializer checking is more complex than I thought)
  3. When T is specified, that means the whole implementation of MyBaseViewController<...> is determined, and then we can fully check if its codes and semantics are right.

Forgive my stupid thought, I just hope to see it in swift.

This feature does not exist in Java.

This sort of strategy does not work in Swift, which performs separate type checking and compilation of generic implementations, rather than full monomorphization as in C++.

2 Likes

Sorry, my mistake :sweat:, Java doesn't support it.

I just had a more specific use case for this pattern.

class G<T: AnyObject>: T {
  var property = default_value
}

As far as my understanding of this subject goes, it should be safe to use this pattern (if it was allowed) as long as the generic subclass provides default values for all stored properties it introduces. This way there no need to call super.init or am I wrong?

Furthermore I think there shouldn't be any issues with dynamic casts. You can easily upcast from G<T> to T. And you should be able to down-cast using as? just as we do it today.

My use-case was that I would like to introduce stored properties on classes that represent an object hierarchy without the need to sub-class every possible class to provide such functionality to all of them (an alternative to this pattern would be 'stored properties within protocol extensions').

cc @Slava_Pestov @John_McCall just to clarify, would such a use-case justify the introduction of this pattern OR the mentioned alternative solution? (I don't mean it in a way that it should happen immediately.)

I think an associated-objects-like "stored properties in extensions" feature would be significantly more interesting and feasible than doing a whole mess of design and implementation work around allowing generic subclassing.

11 Likes

Any chance there is already an internal compiler/runtime feature for pure Swift code? Combine provides magically a default implementation of objectWillChange stored property for conforming classes.

I have no idea how that works; they may in fact be using associated objects.

A bunch of us are curious about whether that is the case or whether it is something else. @Tony_Parker or @Philippe_Hausler are either of you able to say what technique is being used here?

Wouldn‘t that require the class to be a subclass of NSObject, their implementation seems to work on pure swift classes?! :thinking:


If there is no special compiler/runtime support from swift‘s side, then I‘m super curious what technique was used there.

The implementation of objectWillChange is a bit sneaky; there is no associated object. Instead we take advantage of knowing the metadata layout and key paths and access the existing storage from our wrapper. On the surface it seems like an associated object but in reality it is more like a metadata lens.

2 Likes

So the property is not injected into the conforming class, but how do you clean up when the model gets deallocated? I don‘t know a technique that would allow me to catch that.

so the wrapper in that case ends up being an ivar; that structure contains a reference that cleans everything up. Thankfully we don't really need any invalidation since the stream invalidation happens on cancel/terminal states. That being said there is a brief window of opportunity (with multithreaded streams) that someone could hold onto the stream and consequently cause an item to outlive it's normal lifespan I guess.

1 Like

I see. For what it's worth, this approach leaves a bit of a footgun:

class Test: ObservableObject {
   func publishChange() {
       objectWillChange.send()
   }
}

struct ContentView: View {
   @ObservedObject var test = Test()
   var body: some View {
       Button(action: { self.test.publishChange() }) {
           Text("Test")
       }.onReceive(test.objectWillChange) {
           print("received") // never gets printed when you tap the button
       }
   }
}

The above code compiles, but the publisher never sends as far as I can tell. What happens in this case?

When a Published property is added everything works as expected:

class Test: ObservableObject {
   @Published storage = true
   func publishChange() {
       objectWillChange.send()
   }
}
1 Like