skip to content

Explain the compiler's position check for declaration-site variance. What counts as an `out` position versus an `in` position, and what error do you get when you violate it?

level: middleimportance: should knowfreq 45%

answer

  1. out → return/val only; in → parameters only
  2. var of T is in+out → invariant only
  3. variance composes (rule of signs)
  4. private members are exempt
  5. @UnsafeVariance suppresses the check

basics

~20 s

The compiler checks, at the class declaration, that an out parameter is only used in return positions and an in parameter only in argument positions. Break the rule and it reports the parameter occurring in the wrong position.

solid answer

~50 s

Declaration-site variance is enforced by a **position check** performed once on the class/interface. The compiler classifies each occurrence of the type parameter T as an `out` position (function return types, `val` property types) or an `in` position (function value-parameter types, `var` setter parameter). For `out T`, every occurrence must be in an out position; for `in T`, every occurrence must be in an in position. Positions compose: T nested under another type parameter flips according to that parameter's variance (e.g. T inside `Comparator<T>`'s contravariant slot inverts). Private members are exempt because they aren't part of the external type, so they don't affect substitutability. Violations produce errors like *"Type parameter T is declared as 'out' but occurs in 'in' position in ..."*. The `@UnsafeVariance` annotation can suppress the check at a specific occurrence, but it removes the soundness guarantee.

code

kotlin · 9 lines
kotlin
class Producer<out T> {
    fun get(): T = TODO()    // out position: OK
    val v: T = TODO()        // out position: OK
}

// Each of these would fail the position check:
// class Bad1<out T> { fun put(x: T) {} }    // in position
// class Bad2<out T> { var x: T = TODO() }   // var = in + out
// class Bad3<in T>  { fun get(): T = TODO() } // out position

go deeper

for a junior

Knows out belongs in returns and in in parameters at a basic level.

for a middle

States the in/out position rules and recognizes that a var of T is invariant-only.

for a senior

Explains composition (rule of signs) and the private-member exemption with examples.

for a principal

Reasons about @UnsafeVariance trade-offs and when bending the check in a library API is justified.

## What the position check is When you write `class C<out T>` or `class C<in T>`, the compiler scans every place T appears in the **public/protected** signature and labels it `in` or `out`. If any label contradicts the declared variance, compilation fails. This is the single check that makes declaration-site variance sound. ## Out positions vs in positions - **out positions** (a value of T comes *out*): - function **return** types: `fun get(): T` - read-only property types: `val x: T` - **in positions** (a value of T goes *in*): - function **value parameters**: `fun set(v: T)` - the implicit setter parameter of a `var`: `var x: T` (a `var` of type T is BOTH in and out → only allowed when invariant) ```kotlin class Producer<out T> { fun get(): T = TODO() // out position — OK val value: T = TODO() // out position — OK // fun put(v: T) {} // in position — ERROR // var item: T = TODO() // var = in+out — ERROR for `out T` } ``` ## Positions compose (the tricky part) Variance multiplies through nesting. A type parameter appearing inside another generic flips according to that slot's variance: ```kotlin class Box<out T> { // Comparator<in X> — its slot is contravariant (an IN slot) // T placed in a contravariant slot is itself in an IN position → ERROR for out T // fun cmp(c: Comparator<T>) {} } ``` Two reversals cancel: T in the parameter of a function that is itself a parameter ends up back in an out position. The rule of signs: `in × in = out`, `out × out = out`, `in × out = in`. ## The exact errors - `Type parameter T is declared as 'out' but occurs in 'in' position in type T of value-parameter v` - `Type parameter T is declared as 'in' but occurs in 'out' position in type T of ...` ## Private members are exempt Private members don't form part of the type's external contract, so they cannot affect substitutability and are not checked: ```kotlin class Producer<out T>(private var cache: T) // private var with T is fine ``` ## Escape hatch: `@UnsafeVariance` `@UnsafeVariance` tells the compiler to ignore the position check at one occurrence — used in the stdlib for things like `List<out E>.contains(element: @UnsafeVariance E)`. It is genuinely unsafe in the general case and should be reserved for read-only APIs where you've reasoned about safety manually.

  • Why is a `var` property of type T disallowed for both `out T` and `in T`?
    A `var` exposes both a getter (out) and a setter (in). So T appears in both positions, which is only consistent with invariance — neither `out` nor `in` alone is sufficient.
  • How does the stdlib make `List<out E>.contains(e: E)` legal?
    `contains` takes E as a parameter (an in position), which would violate covariance, so the declaration uses `contains(element: @UnsafeVariance E)` to suppress the check, relying on manual reasoning that it only reads.

saying these in an interview costs you the question

  • Claiming the check happens at every call site instead of the declaration
  • Forgetting that a `var` is both in and out
  • Not knowing variance composes through nested generics
  • Thinking private members are subject to the check
  • Using @UnsafeVariance casually without acknowledging it breaks soundness

context