Superclass constraint is recursive

I'm a little confused about this behavior. The following non-generic code is allowed:

protocol Test {}

extension Test where Self: A {}

class A: Test {}

A generic version however is resulting in a compiler error:

protocol Test {
  associatedtype T
}

// error: superclass constraint 'Self' : 'A<Self.T>' is recursive
extension Test where Self: A<T> {}

class A<T>: Test {}
  • Is this intended or a bug?
  • Any idea how to workaround it?

I'm trying to create a single extension on a view type around a final generic class which has no super-class. I also want to avoid duplicating code for each possible generic parameter of that generic class. Therefore I added a base protocol which I use to lookup the generic parameter. The issue is that I also need to access some internal members, but for that I need the above constraint to work instead of adding the members directly as protocol requirements. If I did so it would expose everything to the public, which is a no-go.

cc @Slava_Pestov

1 Like

This is intended for now. Self: A<T> is shorthand for Self: A<Self.T> and you can see that the subject type (Self in this case) occurs on the right hand side of the superclass constraint.

The same restriction exists for same-type constraints. I think it would be possible to lift this restriction for superclass constraints, at least in some cases. However we haven't thought through the implications. It might also run into implementation restrictions. So for now I suggest redesigning your API instead of holding out hope for a language change here.

1 Like

FWIW, I've bumped up against this with same-type constraints a bunch of times. In every case, I the intent is to constrain the type to be a specific generic type which conforms to the protocol while letting the type arguments remain unconstrained. I wonder if there would be a way to support that use case one way or another someday...

The problem with same-type constraints is that it can cause us to construct infinite types. For example if your constraint says T == Array<T.Element> and you substitute in a concrete type for T where Element is equal to the type itself, then T must be Array<Array<Array<Array<...>>>>.

1 Like

So is there no workaround or what?

Ahh, that makes sense. This problem comes up often enough that maybe some other kind of syntax such as T: Array could address the use cases without bumping into that degenerate case.

Does it mean, that there is no way to make a protocol only applicable to a generic class, i.e.

class A<T> {}

protocol Test where Self: A<???> {}

Not with this syntax. But you can use another technique, which involves the definition of an extra helper protocol:

class A<T> {}

// Helper protocol
protocol AProtocol {
    // declare here all A methods you want to use from Test
    ...
}

// Have A conform to the helper protocol
extension A: AProtocol { }

// Now you can declare a constrained Test protocol
protocol Test where Self: AProtocol {
    ...
}
1 Like

It works, but in my case it would mean that I need to expose the protocol and some implementation details to the library user. I had to redesign a few things to avoid exposing internal API and ended up with a surface where only some internal types were exposed but not their members. That said, I still think this would be a very handy generic feature to have and I'd love to see it sorted out one day. :slight_smile:

1 Like

Thanks for the advise. The main problem for me is that a helper protocol cannot be parameterised, i.e. to apply this approach I need to extract a "non-parameterised" part of my generic class and use it in the "Test" protocol constraint, but this is unfortunately not possible. Will it be possible to use generic classes in constraints in the future versions ?

You can add associatedtype T to the protocol and then you‘ll be able to extract T in the extension.

The protocol constraint won’t compile though, which is the main issue of this thread.

Oh. I see, thanks. But yeah, I'm not sure whether it works for me, the idea is to say: to conform to "Test" you need to subclass from A, that comes from another library, using this solution it would be: to conform to "Test" your class need to conform to "AProtocol" but actually what I need is to let a user to subclass from A in order to use "Test".

Hi!
Well, I have another problem with this constraint.

Suppose, that you have parametrized class:

class A<T: UIView> {}

Now, you would like to add another parametrized class:

class B<T: UIView, A: A<T>> {}

Now, I would like to add typealias B to all A subclasses:

class A<T: UIView> {
    typealias TheB = B<T, Self>
}

class B<T: UIView, U: A<T>> {
    class func TType() -> T.Type { return T.self }
    class func UType() -> U.Type { return U.self }
}

class AA: A<UIScrollView> {}

However, in this case

class Test {
    static func test() {
        _ = AA.TheB.TType() // ScrollView
        _ = AA.TheB.UType() // Self != AA<ScrollView>
    }
}

Is there any technique which circumvent this problem?

I know this is an old thread, but I stumbled upon it while searching for a solution to the same problem. I found a neat way to do it by looking at SwiftUI’s .swiftinterface file. Kudos to the SwiftUI team for their amazing work.

I’m sharing my code here in case it helps someone else. I removed some irrelevant parts for brevity.

Solution

The Protocol

public protocol Watcher<Config> {
  associatedtype Config
}

The Class

public class FileWatcher<Config>: Watcher where Config: Codable {
///...
}

The Extension

extension Watcher {
  public static func file<Config>(
    _ url: URL
  ) -> FileWatcher<Config> where Config: Codable, Self == FileWatcher<Config> { .init(file: url) }
}

The Use Case

struct MyConfigurableStuff {
  @Watch(.file("asdfqwer"))
  var myConfig: SomeConfig?
}

This allows me to have a dynamic config that updates when the file changes. I can handle config changes at runtime. This is useful for server-side Swift development.

(note, I'm on Swift 5.9 now)

EDIT:

In case anyone's wondering if it's a typo that I create a URL from a string, no it's not. We have an oss swift package called Beton, that does this kind of stuff. Convenience APIs. I hate boilerplate code...