
- node, which represents a linked list with at least one element and includes a reference to the first element (head) and a reference to the rest of the elements (tail).
- empty, which represents an empty linked list.
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() }
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
LinkedList type parameter covariant (by adding the out modifier’). This is perfectly fine for a type that is only returned, i.e., for all type parameters in immutable classes. Then, we should make our Empty object extend LinkedList<Nothing>. The Nothing type is a subtype of all 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 }
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>, therefore EmptyList is a subtype of all lists.
Every empty list created with the
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 }
sealed interface ChangesTrackerMessage<out T> data class Change<T>(val newValue: T) : ChangesTrackerMessage<T> data object Reset : ChangesTrackerMessage<Nothing> data object UndoChange : ChangesTrackerMessage<Nothing> sealed interface SchedulerMessage<out T> data class Schedule<T>(val task: Task<T>) : SchedulerMessage<T> data class Delete(val taskId: String) : SchedulerMessage<Nothing> data object StartScheduled : SchedulerMessage<Nothing> data object Reset : SchedulerMessage<Nothing>
data class Task<T>( val id: String, val scheduleAt: Instant, val data: T, val priority: Int, val maxRetries: Int? = null )
TaskUpdate class which uses null as a marker which indicates that a specific property should not change.data class TaskUpdate<T>( val id: String? = null, val scheduleAt: Instant? = null, val data: T? = null, val priority: Int? = null, val maxRetries: Int? = null )
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 a Covariant Nothing Object in our project to represent a property change. Each property might be either kept unchanged or changed to a new value. We can represent these two options with a sealed hierarchy, and thanks to generic types we might expect specific types of values.data 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> data object Keep : TaskPropertyUpdate<Nothing> data 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 )
data 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> data object Keep : TaskPropertyUpdate<Nothing> data class ChangeTo<T>(val newValue: T) : TaskPropertyUpdate<T> data object RestorePrevious : TaskPropertyUpdate<Nothing> data object RestoreDefault : TaskPropertyUpdate<Nothing> val update = TaskUpdate<String>( data = ChangeTo("ABC"), maxRetries = RestorePrevious, priority = RestoreDefault, )
Nothing type argument as a covariant type parameter. This is a pattern I call the Covariant Nothing Class. For example, consider the Either class, which can be either Left or Right and must have two type parameters that specify what data types it expects on the Left and on the Right. However, both Left and Right should each have only one type parameter to specify what type they expect. To make this work, we need to fill the missing type argument with Nothing.sealed class Either<out L, out R> data class Left<out L>(val value: L) : Either<L, Nothing>() data class Right<out R>(val value: R) : Either<Nothing, R>()
Left or Right without specifying type arguments.val left = Left(Error()) val right = Right("ABC")
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
Left or 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
Either is implemented in the Arrow library.