This Week I Learned: the in and out of Kotlin [2022–03–06]
I’ve been learning about the in and out modifiers in Kotlin. Coming at it from a Java perspective, the docs reinforced that map-the-concept-from-Java approach and I’m not convinced that this is the best way, because I and others had a hard time wrapping our heads around it. in
and out
on a declaration like fun computeSomething(List<out T>)
are a bit like Java’s <? super T>
and <? extends T>
but they’re not exactly the same, and I think that it’s because although the Kotlin compiler does ultimately map them to the same JVM concepts that are available to Java it also does its own compile-time checks that are not exactly the same.
Instead of trying to map them to Java, and at a high level, here is what I think that this is what these modifiers mean. I’m writing this partly to better wrap my head around it, so please bear with me if I don’t hit every nuance and let me know if I get something wrong.
out
: I’m going to extract a value out of here. It’s produced by this code. This is useful for classes and functions that compute a result.
interface Producer<out T> {
fun produce(): T
}
I can assign a Producer<String>
value to a Producer<Any>
object because produce()
will extract out a value that can be assigned to an Any
object.
in
: I’m going to send in a value here. It’s consumed by this code. This is useful when you’re reading the properties of an object and computing something else, and you’re not storing or creating these objects.
interface Consumer<in T> {
fun consume(thing: T)
}
The Comparable
implementations just read in a T
and don’t make one.
Using the interfaces above:
class IntProducer: Producer<Int> {
override fun produce() = (Math.random() * 1000).toInt()
}class Printer<in T>: Consumer<T> {
override fun consume(thing: T) {
println(thing)
}
}fun inOutTest() {
val numberProducer: Producer<Number> = IntProducer()
val numberPrinter = Printer<Number>()
numberPrinter.consume(numberProducer.produce())
}
However, this code does not compile:
interface NumberProducer: Producer<Number>fun fullOfErrors() {
val numberProducer: NumberProducer = IntProducer() // wrong type
}
An interesting consequence of the way that in
and out
work is that out
tends to be on immutable collections and neither seems to work on mutable collections that also produce a result.
Check out this code:
interface ImmutableStack<out T> {
fun peek(): T
}
interface GrowingStack<in T> {
fun push(item: T)
}
interface ShrinkingStack<out T> {
fun pop(): T
}
class Stack<T>: ImmutableStack<T>, GrowingStack<T>, ShrinkingStack<T> {
private val items: MutableList<T>
constructor(items: Iterable<T>) {
this.items = items.toMutableList()
}
override fun peek() = items.last()
override fun push(item: T) {
items.add(item)
}
override fun pop(): T = items.removeLast()
}
Having separate interfaces for GrowingStack
and ShrinkingStack
looks like ridiculous overkill, but if I try to combine them then I can’t have both push()
and pop()
in the same interface
or class
until I remove both the in
and out
.
The code below will result in the error Type parameter T is declared as ‘out’ but occurs in ‘in’ position in type T
on push()
until I remove the out
, and if I replace it with in
then the inverse occurs on the other functions.
class MutableStack<out T>: ImmutableStack<T> {
private val items: MutableList<T>
constructor(items: Iterable<T>) {
this.items = items.toMutableList()
}
override fun peek(): T = items.last()
fun push(item: T) = items.add(item)
fun pop(): T = items.removeLast()
}
I guess that means that all mutable collection classes have type parameters that are neither in
nor out
. That probably limits the help that the compiler can give. Keep putting out
on those immutable collections, though, so that you can return an Int
from some kind of Collection<Int>
where a Number
is expected.