article banner (priority)

Covariant Nothing Object

This is a chapter from the book Advanced Kotlin. You can find it on LeanPub.

In the previous part, we already discussed variance modifiers out and in, as well as their practical use cases.

Consider that you need to define a linked list data structure. It is a type of collection constructed by two types:

  • node, which represents a linked list with at least one element and includes a reference to the first element (head) and the reference to the rest of the elements (tail).
  • empty, which represents an empty linked list.

In Kotlin, we would represent such a data structure with a sealed class.

sealed class LinkedList<T> data class Node<T>(val head: T, val tail: LinkedList<T>) : LinkedList<T>() class Empty<T> : LinkedList<T>() fun main() { val strs = Node("A", Node("B", Empty())) val ints = Node(1, Node(2, Empty())) val empty: LinkedList<Char> = Empty() }

There is one problem though - for every linked list, we need a new object to represent the empty list. Every time we use Empty(), we create a new instance. We would prefer to have only one instance that would serve wherever we need to represent an empty linked list. For that, we use object declaration in Kotlin, but object declarations cannot have type parameters.

sealed class LinkedList<T> data class Node<T>(val head: T, val tail: LinkedList<T>) : LinkedList<T>() object Empty<T> : LinkedList<T>() // Error

There is a solution to this problem. We can make the LinkedList type parameter covariant, so add the out modifier, which is perfectly fine for the type that is only returned, so for all type parameters in immutable classes. Then we should make our Empty object extend LinkedList<Nothing>. The type Nothing is the subtype of all the types, so if the LinkedList type parameter is covariant, then LinkedList<Nothing> is a subtype of all linked lists.

sealed class LinkedList<out T> data class Node<T>(val head: T, val tail: LinkedList<T>) : LinkedList<T>() object Empty : LinkedList<Nothing>() fun main() { val strs = Node("A", Node("B", Empty)) val ints = Node(1, Node(2, Empty)) val empty: LinkedList<Char> = Empty }

This pattern is used in many places, even in Kotlin Standard Library. As you already know, List is covariant because it is read-only. When you create a list using listOf or emptyList, they both return the same object, EmptyList, which implements List<Nothing>, so this way, EmptyList is a subtype of all the lists.

Every empty list created with listOf or emptyList functions from Kotlin stdlib is actually the same object.

fun main() { val empty: List<Nothing> = emptyList() val strs: List<String> = empty val ints: List<Int> = empty val other: List<Char> = emptyList() println(empty === other) // true }

I could observe this pattern in many places, for instance when we define generic messages, and some of them do not need to include any parameters, we like to make them objects.

sealed interface ChangesTrackerMessage<out T> class Change<T>(val newValue: T) : ChangesTrackerMessage<T> object Reset : ChangesTrackerMessage<Nothing> object UndoChange : ChangesTrackerMessage<Nothing> sealed interface TaskSchedulerMessage<out T> class Schedule<T>(val task: Task<T>) : TaskSchedulerMessage<T> class Update<T>(val taskUpdate: TaskUpdate<T>) : TaskSchedulerMessage<T> class Delete(val taskId: String) : TaskSchedulerMessage<Nothing> object StartScheduled : TaskSchedulerMessage<Nothing> object Reset : TaskSchedulerMessage<Nothing>

Even though this pattern repeats in Kotlin projects, I couldn't find a name that would describe it. I decided to name it "Covariant Nothing Object". It is not a precise name, as a precise description would be "pattern, where object declaration implements a generic class or interface with Nothing type argument used on covariant type argument position". Nevertheless, the name needs to be short, and "Covariant Nothing Object" is a clear and catchy name.

Another example of Covariant Nothing Object comes from a library my team co-created. It was used to schedule tasks in a microservice environment. For simplicity, you could assume that each task can be modeled as follows:

class Task<T>( val id: String, val scheduleAt: Instant, val data: T, val priority: Int, val maxRetries: Int? = null )

We needed to implement a mechanism to change scheduled tasks. What is more, we would like to represent change with a class, to create it and pass it around conveniently. The old-school approach is to make a class TaskUpdate, which uses null as a marker that a specific property should not change.

class TaskUpdate<T>( val id: String? = null, val scheduleAt: Instant? = null, val data: T? = null, val priority: Int? = null, val maxRetries: Int? = null )

This approach is very limiting. Since the null value is interpreted as "do not change this property", there is no way to express that you want to set a particular value to null. Instead, we used Covariant Nothing Object to represent property change. Each property might be either kept unchanged or changed to a new value. We can represent those two options with a sealed hierarchy, and thanks to generic types, we might expect specific types of values.

class TaskUpdate<T>( val id: TaskPropertyUpdate<String> = Keep, val scheduleAt: TaskPropertyUpdate<Instant> = Keep, val data: TaskPropertyUpdate<T> = Keep, val priority: TaskPropertyUpdate<Int> = Keep, val maxRetries: TaskPropertyUpdate<Int?> = Keep ) sealed interface TaskPropertyUpdate<out T> object Keep : TaskPropertyUpdate<Nothing> class ChangeTo<T>(val newValue: T) : TaskPropertyUpdate<T> val update = TaskUpdate<String>( id = ChangeTo("456"), maxRetries = ChangeTo(null), // we can change to null data = ChangeTo(123), // COMPILATION ERROR // type mismatch, expecting String priority = ChangeTo(null), // COMPILATION ERROR // type mismatch, property is not nullable )

This way, we achieved a type-safe and expressive way to represent task changes. What is more, when we use the Covariant Nothing Object pattern, we can easily express other kinds of changes as well. For instance, if our library allows restoring a previous value or supports default values, we could add new objects to represent those property changes.

class TaskUpdate<T>( val id: TaskPropertyUpdate<String> = Keep, val scheduleAt: TaskPropertyUpdate<Instant> = Keep, val data: TaskPropertyUpdate<T> = Keep, val priority: TaskPropertyUpdate<Int> = Keep, val maxRetries: TaskPropertyUpdate<Int?> = Keep ) sealed interface TaskPropertyUpdate<out T> object Keep : TaskPropertyUpdate<Nothing> class ChangeTo<T>(val newValue: T) : TaskPropertyUpdate<T> object RestorePrevious : TaskPropertyUpdate<Nothing> object RestoreDefault : TaskPropertyUpdate<Nothing> val update = TaskUpdate<String>( data = ChangeTo("ABC"), maxRetries = RestorePrevious, priority = RestoreDefault, )

Covariant Nothing Class

There are also cases where we want a class to implement a class or interface with the Nothing type argument used for a covariant type parameter. This is a pattern I call Covariant Nothing Class. For example, consider the Either class, which can be either Left or Right. The class Either must have two type parameters to specify what data types it expects on Left and what on Right. However, Left should have only one type parameter to specify what type it expects. The same with Right. To make it work, we need to fill the missing type argument with Nothing.

sealed class Either<out L, out R> class Left<out L>(val value: L) : Either<L, Nothing>() class Right<out R>(val value: R) : Either<Nothing, R>()

With such definitions, we can create Left or Right without specifying type arguments.

val left = Left(Error()) val right = Right("ABC")

Both Left and Right can be up-casted to Left and Right with supertypes of the types of values they hold.

val leftError: Left<Error> = Left(Error()) val leftThrowable: Left<Throwable> = leftError val leftAny: Left<Any> = leftThrowable val rightInt = Right(123) val rightNumber: Right<Number> = rightInt val rightAny: Right<Any> = rightNumber

They can also be used wherever the result with appropriate Left and Right type is expected.

val leftError: Left<Error> = Left(Error()) val rightInt = Right(123) val el: Either<Error, Int> = leftError val er: Either<Error, Int> = rightInt val etnl: Either<Throwable, Number> = leftError val etnr: Either<Throwable, Number> = rightInt

This, in simplification, is how Either is implemented in the Arrow library.

That ends the second part of this series. In the next one, we will discuss variance modifier limitations.