Why is an explicit `AnyObject` annotation required here?

Consider this Swift 5.4.2 code:

class Foo<Thing : AnyObject> { }
class Bar<Thing> : Foo<Thing> { }

Foo constrains Thing to be AnyObject. Bar<Thing> inherits from Foo<Thing>, so the only logical outcome is that Bar.Thing is also constrained to AnyObject.

But no — this code won't compile. Swift raises the error 'Foo' requires that 'Thing' be a class type.

The error can be fixed by adding an extra AnyObject annotation like so:

class Foo<Thing : AnyObject> { }
class Bar<Thing : AnyObject> : Foo<Thing> { }

But if you try to do this:

class Foo<Thing : AnyObject> { }
class Bar<Thing : Int> : Foo<Thing> { }

Swift will complain that 'Foo' requires that 'Thing' be a class type.

I find this strange. Swift apparently understands that my definition of Foo implies that Bar.Thing must necessarily be a class type. But it won’t compile my code unless I add an explicit annotation saying so.

Is there a cleaner way to notate this kind of “transitive” constraint between generic classes?

not necessarily, the Thing can be of any type so long it is a class type:

class Bar<Thing: OtherClass> : Foo<Thing> { }

That’s exactly right. In fact, you can throw some really complex sets of constraints at the compiler and it can figure out some non-obvious facts about them, almost like solving a complex set of equations.

The primary audience for the declaration of Bar isn’t the compiler, but the reader of that code who will be planning to use it, which might be a large audience or might just be future you. Swift requires some constraints to be made explicit so that readers don’t have to deduce them after consulting multiple other declarations and solving systems of equations.

1 Like

For better or for worse, we do do this sort of transitive constraint inference for generic function signatures:

struct S<T: AnyObject> {}

func f<T>(t: T, s: S<T> = .init()) {} // OK!

f(t: 0) // Error: global function 'f(t:s:)' requires that 'Int' be a class type
1 Like

Or, equivalently, we can view this error as having arisen because the caller attempts to create an instance of type S<Int>, no transitive constraint inference required.

1 Like

Sure—the point though is that the definition of f succeeds just fine. Even naming S<T> should be invalid unless T: AnyObject, regardless of whether we're creating an instance:

let any: Any = ()

if any is S<Int> { // Error: 'S' requires that 'Int' be a class type
    print("S<Int>")
}

So if T weren't transitively inferred to be AnyObject-constrained, we'd expect an error on the definition of f, "'S' requires that 'T' be a class type."

Equivalently, if we did allow Bar<Thing>: Foo<Thing> {}, you would of course expect an error from:

let _ = Bar<Int>()

which you could view as either an implicit (transitive) constraint on Bar's generic parameter, or a failure from an attempt to create an instance of (a subclass of) Foo<Int>, violating the constraint on Foo's generic parameter.

1 Like

A simpler example is dictionary:

func dict<T>(_ value: [T: String]) {} // implicitly T: Hashable
2 Likes

I wonder if @Slava_Pestov’s Requirement Machine would diagnose the missing AnyObject constraint.

IIRC (though I can't track down the thread where I previously asked about this), the transitive constraint inference is a feature of the current generic signature builder, rather than an undesired emergent behavior/bug.

1 Like

Requirement inference from application of bound generic types appearing in a function's signature is indeed a feature. Regardless of merit, I don't think we can remove it at this point without breaking a large amount of code.

The inferred requirements do appear in the "minimal canonical signature" that is used for mangling and generic signature queries and so on; this inference is a surface-level syntax sugar that sits "above" the requirement machine and the rest of the type checker implementation.

7 Likes