Kotlin’s function type variance is an important concept that helps developers understand how types behave when dealing with function inputs and outputs. If you’ve ever wondered why function parameters and return types follow different variance rules, this post will clear things up..!
Understanding Variance in Kotlin
Variance determines how subtyping relationships work in a type hierarchy. There are two key types of variance in Kotlin:
- Covariance (Out-projected types): A subtype can replace a supertype when reading values.
- Contravariance (In-projected types): A supertype can replace a subtype when writing values.
Function Types and Variance
Kotlin functions can be represented as types, for example:
val func: (String) -> Int = { it.length }
Here, func
is a function type that takes a String
as input and returns an Int
. Function types in Kotlin have two parts:
- Parameter type(s) — What the function takes in.
- Return type — What the function returns.
The variance in function types applies differently to parameters and return types.
- Return types are covariant (
out
). - Parameter types are contravariant (
in
).
Now, let’s explore how these apply to Kotlin’s function type variance.
Covariance and Contravariance in Kotlin’s Function Types
In Kotlin, a class or interface can be covariant on one type parameter and contravariant on another. One of the classic examples of this is the Function
interface. Let’s take a look at the declaration of the Function1
interface, which represents a one-parameter function:
interface Function1<in P, out R> {
operator fun invoke(p: P): R
}
To make the notation more readable, Kotlin provides an alternative syntax (P) -> R
to represent Function1<P, R>
. In this syntax, you’ll notice that P
(the parameter type) is used only in the in
position and is marked with the in
keyword, while R
(the return type) is used only in the out
position and is marked with the out
keyword.
This means that the subtyping relationship for the function type is reversed for the first type argument (P
) and preserved for the second type argument (R
).
For example, let’s say you have a higher-order function called enumerateCats
that accepts a lambda function taking a Cat
parameter and returning a Number
:
fun enumerateCats(f: (Cat) -> Number) { ... }
Now, suppose you have a function called getIndex
defined in the Animal
class that returns an Int
. You can pass Animal::getIndex
as an argument to enumerateCats
:
fun Animal.getIndex(): Int = ...
enumerateCats(Animal::getIndex) // This code is legal in Kotlin. Animal is a supertype of Cat, and Int is a subtype of Number
In this case, the Animal::getIndex
function is accepted because Animal
is a supertype of Cat
, and Int is a subtype of Number, the function type’s subtyping relationship allows it.

This illustration demonstrates how subtyping works for function types. The arrows indicate the subtyping relationship.
Function Type Variance in Kotlin’s Type System
As you now know, when working with function types in Kotlin, variance is explicitly defined:
interface Function1<in P, out R> {
fun invoke(param: P): R
}
P
(parameter) is contravariant (in
)R
(return type) is covariant (out
)
This aligns with the natural logic of function type variance.
Conclusion
So in summary,
- Covariance (
out
) applies to return types – A function returning a subtype can be assigned to a function returning a supertype. - Contravariance (
in
) applies to parameter types – A function taking a supertype can be assigned to a function taking a subtype. - Kotlin enforces these rules to ensure type safety and prevent runtime errors.
Understanding Kotlin’s Function Type Variance helps write flexible, reusable, and type-safe code, especially when designing APIs, lambda functions, and higher-order functions.