The following class uses an out T
generic parameter:
class Foo<out T> {
fun bar(f: () -> T): T {
return f()
}
}
This does not compile: "Type parameter T is declared as 'out' but occurs in 'in' position in type () -> T"
I don't understand why f: () -> T
is not legal since it appears in an out
position as a result. Can someone please explain an give an example where allowing this would break variance semantics?
The following class uses an out T
generic parameter:
class Foo<out T> {
fun bar(f: () -> T): T {
return f()
}
}
This does not compile: "Type parameter T is declared as 'out' but occurs in 'in' position in type () -> T"
I don't understand why f: () -> T
is not legal since it appears in an out
position as a result. Can someone please explain an give an example where allowing this would break variance semantics?
3 Answers
Reset to default 3In terms of "input position vs output position", fun bar(f: () -> T)
is really no different from fun bar(f: T)
. In both cases, bar
is taking a T
as input. It's just that in the former case, the input is wrapped in a function, and bar
can call it as many times as it wants.
In other words, if fun bar(f: () -> T)
is allowed, then effectively fun bar(f: T)
is also allowed, because the implementation of bar
can just call f()
, and do whatever a fun bar(f: T)
would have done with f
.
Kotlin's documentation on this is rather bad, so I will adapt the rules from the C# spec to explain the general rules. We say that an input-unsafe type cannot be used as a function parameter type, and an output-unsafe type cannot be used as a return type.
A type T
is output-unsafe if one of the following holds:
T
is a contravariant (in
) type parameter.T
is a generic typeS<A1, A2, ... An>
where for at least one of the type parametersAi
, one of the following holds:Ai
is output-unsafe and the declaration site variance of the corresponding type parameter is covariant or invariantAi
is input-unsafe and the declaration site variance of the corresponding type parameter is contravariant or invariant
A type T
is input-unsafe if one of the following holds:
T
is a covariant (out
) type parameter.T
is a generic typeS<A1, A2, ... An>
where for at least one of the type parametersAi
, one of the following holds:Ai
is input-unsafe and the declaration site variance of the corresponding type parameter is covariant or invariantAi
is output-unsafe and the declaration site variance of the corresponding type parameter is contravariant or invariant
Function types have contravariant parameters and covariant return types. We can easily show that () -> T
(aka Function0<T>
) in your code is input-unsafe. First, T
is input-unsafe because it is covariant. Then we can conclude that () -> T
is input-unsafe because T
is an input-unsafe type and the type parameter of Function0
is covariant.
T
is still in an 'in' position in bar()
when the argument type is the producer () -> T
rather than T
, as both types are a source of T
s. This is illegal as it could lead to objects being incorrectly typed.
Why it doesn't work
Say it was allowed.
Then we might write Foo.bar()
such that it stored the results of all the producer functions it received, but it returns the result of the first one it received.
class Foo<out T> {
val tStore: MutableList<T> = mutableListOf()
fun bar(f: () -> T): T {
tStore.add(f())
return tStore.first()
}
}
Then imagine we have the interfaces:
interface Animal
interface Cat : Animal {
fun purr()
}
interface Dog : Animal {
fun bark()
}
Let's create a Foo<Dog>
.
val fooDog: Foo<Dog> = Foo<Dog>()
The out
projection means we can do1:
val fooAnimal: Foo<Animal> = fooDog // since Foo<Dog> is a subtype of Foo<Animal>
fooAnimal.bar { SiameseCat() }
But then can go back to fooDog
and do:
val someDog: Dog = fooDog.foo { HoundDog() }
someDog.bark() // Runtime error, since `someDog` is actually of type `SiameseCat`
Note: example is a bit misleading
As noted by @Sweeper in the comments, this example doesn't quite work because it relies on declaring a MutableList<T>
inside Foo
, which is also illegal as a MutableList
requires its type to be invariant.
To try and summarise the issue, in order to create an error from the mistyping in the argument for bar()
, we need Foo
to produce the mistyped T
received in that function. But it seems there is no type-safe place to store that T
in Kotlin2, as all var
s or objects we might use themselves would not allow an object 'invariant' in T
to be used inside Foo
.
All in all, arguably a credit to the type-safety of Kotlin! But hopefully the answer illustrates the concept of how producing and consuming objects must in general be distinct to ensure type safety.
1 By definition, where Y : X
then Foo<out Y> : Foo<out X>
, since out
designates covariance in Kotlin
2 I suspect a Java array would work as it is not as strictly typed as a Kotlin array
It does only appear in an out position of the lambda f
, but it appears in the in position of the function bar
since it is part of its parameter.