Interaction between variance markers and recursive generic types
0 reputation · 26 Oct 2020, 05:08 UTC
0 reputation · 26 Oct 2020, 05:08 UTC
Ceylon implements a static type system for the JVM that utilizes covariance and contravariance to provide more flexible generic subtyping than standard Java. This is typically managed through type constraints and variance markers on generic parameters.
A specific design challenge arises when applying these variance markers to deep recursive generic types within complex inheritance hierarchies. It is unclear how the compiler resolves type safety when a generic type refers to itself while simultaneously being marked as covariant or contravariant.
Which rules govern the interaction between variance markers and recursive generic types in these edge-case inheritance scenarios? Does the compiler prioritize the variance marker or the recursive constraint during type erasure?
33630 reputation · 26 Oct 2020, 15:10 UTC
The compiler first checks whether the variance marker (covariant out / +, contravariant in / -, or invariant) is satisfied by the positions in which the type parameter appears. A recursive use of the same generic type is just another occurrence of that parameter, so it must appear only in positions matching the variance:
out T), the recursive type may appear only in covariant positions – e.g., as a return type or in a read‑only field.in T), the recursive type may appear only in contravariant positions – e.g., as a method parameter type.Variance markers are erased at runtime; they do not influence type erasure. The erasure step treats the generic type as if the parameter were invariant, inserting the appropriate bridge methods or casts to preserve the compile‑time subtyping rules.
In Java, declaration‑site variance does not exist; use‑site wildcards (? extends T, ? super T) never change the class’s own variance, so a recursive class like class Node<T> { Node<T> next; } remains invariant regardless of wildcards.
In C# (and similar languages with declaration‑site variance), the rules above apply directly. For example:
// Valid covariant recursive interface
interface INode {
T Value { get; }
INode Next; // T appears only in covariant position (return type)
}
// Invalid – recursive use in a contravariant position
interface IBad {
void Set(T item);
IBad Next; // T appears in covariant position → error
}
To give a concrete recommendation, please specify the target language and its version (e.g., Java 17, C# 10, Scala 2.13).
Use comments to ask for clarification. Post a solution as an answer.
33,630 reputation · 26 Oct 2020, 15:56 UTC
The existing answer correctly describes the general principle, but Ceylon applies it more strictly than C# or Java wildcards. The compiler validates variance at the declaration site across the entire transitive supertype hierarchy, not just the immediate parent. When a generic type appears recursively in a satisfies clause (e.g., interface Node satisfies Container>), that recursive occurrence is treated as a usage position and must respect the declared variance.
interface List satisfies Sequence – Element appears covariantly in Sequence.interface Consumer satisfies Consumer> – the contravariant T appears in a covariant position inside the recursive Consumer argument.This check happens during declaration validation, before erasure. Erasure itself follows Java-like semantics (type parameters replaced by bounds or Object), but the variance constraints are fully enforced at compile time.