Variance annotations
Variance is the rule for when a generic built from one type is assignable to the same generic built from a related type. CruftScript defaults every unmarked type parameter to invariance and lets you mark one out (covariant) or in (contravariant) to opt into a looser rule. Today the markers parse but no variance check runs, so everything is treated invariantly.
Variance is the rule for when a generic type built from one type is assignable to the same generic built from a related type. In CruftScript you can mark a type parameter out or in to opt into a relaxed rule; without a marker, the default is invariance.
interface Producer<out T> { get(): T } // covariant
interface Sink<in T> { put(x: T): void } // contravariant
If Cat is assignable to Animal, variance answers a question like: is a Producer<Cat> assignable to a Producer<Animal>? The answer depends on how the parameter is used, and the three possible answers each have a name.
The three kinds, in plain terms
Covariance follows the subtype direction. A Producer<Cat> is a Producer<Animal>, because everything it hands you with get() is a Cat, and a Cat is an acceptable Animal. A parameter that only ever comes out of a type, through return values, is safe to treat this way. You mark it out.
Contravariance reverses the direction. A Sink<Animal> is a Sink<Cat>, because a sink that accepts any Animal certainly accepts a Cat. A parameter that only ever goes into a type, through arguments, is safe to reverse this way. You mark it in.
Invariance allows neither direction. A parameter used both to take values in and to hand values out is only interchangeable with itself: Box<Cat> is a Box<Animal> only when the parameters are the same type. This is the default here.
The default is invariance
CruftScript defaults every unmarked type parameter to invariance. This differs from TypeScript, whose method parameters default to bivariance: it accepts assignment in both directions at once, which is convenient and unsound, because it lets a value flow somewhere its type does not actually fit.
By defaulting to invariance, CruftScript starts from the safe rule and makes you ask for anything looser. The out and in markers are intended as those requests: opt-in relaxations, not hints. In the intended model, marking a parameter out but using it in an input position — where covariance would be unsound — is a variance error rather than an accepted suggestion. That enforcement is not in effect yet: the markers parse but no variance check runs off them today, so every type parameter is currently treated invariantly regardless of an out/in marker (see Limitations).
Why it works this way
Variance is one of the quiet places a type system can be unsound. If a Producer<Cat> and a Producer<Animal> are treated as interchangeable when they are not, a value ends up somewhere its type does not fit, and the failure surfaces far from the assignment that caused it. Bivariant-by-default trades that safety for convenience. CruftScript makes the opposite trade: invariant by default, so nothing is assumed compatible until you have said which direction is safe and the checker has confirmed the parameter is only used that way. You write a marker in the cases where a looser rule genuinely holds, and you get an error in the cases where it does not.
Limitations
- Variance markers parse, but variance is not enforced yet. The parser accepts
out Tandin Ton a type parameter, but the marker is tolerated rather than acted on: no covariant or contravariant check runs off it. The default-invariance model and theout/inchecks described here are the intended behavior, not something you can lean on now. - Invariance is the only rule in effect. Every type parameter is treated invariantly regardless of an
out/inmarker. There is no way today to grant the covariant or contravariant relaxation.