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

generics

#1

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!

(John McCall) #2

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.


(Adrian Zubarev) #3

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


(Anthony Latsis) #4

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.


#5

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


(Slava Pestov) #6

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


#7

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.


(John McCall) #8

This feature does not exist in Java.


(Slava Pestov) #9

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++.


#10

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