skip to content

Given the FunctionN variance (in parameters, out return), explain when one plain function type is a subtype of another, with an example.

level: principalimportance: nice to knowfreq 22%

answer

  1. in P = contravariant params, out R = covariant return
  2. Accept wider inputs, return narrower outputs
  3. (Number)->Int <: (Int)->Number
  4. Liskov: contravariant args, covariant results
  5. Arity must match exactly; variance only on types

basics

~20 s

A function can stand in for another if it accepts at least as wide a range of inputs and returns at least as specific an output. So you can use a function that takes a broader parameter type and returns a narrower result type.

solid answer

~40 s

FunctionN declares parameters as in (contravariant) and the return as out (covariant): interface Function1<in P, out R>. So (P) -> R is a subtype of (P2) -> R2 when P2 <: P (parameters are contravariant — accept wider) and R <: R2 (return is covariant — produce narrower). Concretely, (Number) -> Int is a subtype of (Int) -> Number: it accepts any Number (so certainly an Int) and returns an Int (which is a Number). This mirrors the Liskov rule for function subtyping: contravariant in arguments, covariant in results. It lets you pass a more general function where a more specific one is required, e.g. supply a (Any) -> String to a parameter typed (String) -> CharSequence. Arity must match exactly; variance only relaxes the individual parameter and return types, not the count.

code

kotlin · 9 lines
kotlin
fun apply(transform: (Int) -> Number, x: Int): Number = transform(x)

val general: (Number) -> Int = { it.toInt() }  // accepts any Number, returns Int

// general is a subtype of (Int) -> Number, so this is allowed:
println(apply(general, 7))   // 7

// The opposite direction would NOT compile:
// val r: (Number) -> Int = { i: Int -> i }   // error: param too narrow

go deeper

for a junior

May not know the formal rule but can grasp 'accept more, return more specific' from an example.

for a middle

States that parameters are contravariant and return covariant and recognizes a valid assignment.

for a senior

Derives the subtyping condition P2<:P and R<:R2 and explains why it is type-safe.

for a principal

Applies it to API design (accepting general callbacks), connects to Liskov and FunctionN's in/out declarations, and notes arity invariance.

## The variance declarations The standard-library function interfaces are declared with **variance annotations**: ```kotlin public interface Function1<in P1, out R> { public operator fun invoke(p1: P1): R } ``` - `in P1` means **contravariant** in the parameter: a `Function1` of a *wider* parameter type is a subtype. - `out R` means **covariant** in the return: a `Function1` of a *narrower* return type is a subtype. ## The subtyping rule `(P) -> R` <: `(P2) -> R2` exactly when: - **Parameters (contravariant):** `P2 <: P` — the subtype must accept *at least* every input the supertype accepts (it may accept more). - **Return (covariant):** `R <: R2` — the subtype must produce a result that is *at least as specific* (it may be more specific). This is the classic Liskov substitution rule for functions: **contravariant in arguments, covariant in results**. ## Worked example ```kotlin val wide: (Number) -> Int = { it.toInt() } val narrow: (Int) -> Number = wide // OK: subtyping holds ``` Why does `wide` qualify as an `(Int) -> Number`? - Parameter: caller passes an `Int`; `wide` accepts any `Number`, so an `Int` is fine (`Int <: Number`). - Return: `wide` returns an `Int`; the caller expects a `Number`, and `Int <: Number`, so the result fits. The reverse assignment fails: ```kotlin val bad: (Number) -> Int = narrow // ERROR: narrow only accepts Int, not all Numbers ``` ## Why it is safe If code holds an `(Int) -> Number` reference, it will only ever pass `Int`s and only rely on a `Number` back. A `(Number) -> Int` honors both: it handles the `Int` (and more) and returns an `Int` (which is a `Number`). No call site can be surprised. ## Boundaries of the rule - **Arity is invariant.** `(Int) -> R` is never a subtype of `(Int, Int) -> R`; the parameter *count* must match before variance applies. - Variance applies position-by-position: every parameter is contravariant, the single return is covariant. - This underlies why higher-order APIs can accept more general callbacks than they strictly need, improving reuse. ## Practical payoff Designing a parameter as `(SpecificInput) -> GeneralOutput` lets callers supply broader, more reusable functions — e.g. a logging API taking `(LogEvent) -> Unit` can accept a `(Any) -> Unit` general logger.

  • Is (Any) -> Unit a subtype of (String) -> Unit?
    Yes. Parameters are contravariant, so accepting the wider Any qualifies it wherever a String is passed; the Unit return matches.
  • Does variance let (Int) -> R be used where (Int, Int) -> R is needed?
    No. Arity is invariant; the parameter count must be identical before contravariance/covariance can relax the individual types.

A subtype function is an over-qualified employee: it accepts a broader range of tasks and delivers a more precise result than the job strictly demands.

saying these in an interview costs you the question

  • Saying parameters are covariant (it is the opposite)
  • Thinking you can substitute across different arities
  • Claiming (Int)->Number is a subtype of (Number)->Int
  • Confusing covariant return with covariant parameters

context