Given the FunctionN variance (in parameters, out return), explain when one plain function type is a subtype of another, with an example.
answer
- in P = contravariant params, out R = covariant return
- Accept wider inputs, return narrower outputs
- (Number)->Int <: (Int)->Number
- Liskov: contravariant args, covariant results
- Arity must match exactly; variance only on types
basics
~20 sA 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 sFunctionN 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 linesfun 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 narrowgo deeper
May not know the formal rule but can grasp 'accept more, return more specific' from an example.
States that parameters are contravariant and return covariant and recognizes a valid assignment.
Derives the subtyping condition P2<:P and R<:R2 and explains why it is type-safe.
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