Uses of Nothing

On the surface, Nothing may seem useless. And, in day-to-day development, it is unlikely that you will use Nothing directly… but it is very likely that you are using Nothing without realizing it.

As a Return Type

A function can return Nothing as a type. This might seem impossible, since there are no instances of Nothing. From a compiler standpoint, though, a function returning Nothing means the function is required to never return. The primary scenario of this is if the function is guaranteed to throw an exception.

That may sound strange… until you consider TODO().

TODO() is declared to return Nothing:

public inline fun TODO(reason: String): Nothing =
  throw NotImplementedError("An operation is not implemented: $reason")

TODO() is marked as being an inline function, so the compiler will “bake” the exception right into wherever the TODO() appears. However, the Nothing return type indicates to the compiler that since TODO() can never return, there is no sense in worrying about anything else in the function after that point, including the missing return.

For example, suppose we created our own similar function, called NOTDONE():

public inline fun NOTDONE(reason: String) {
  throw NotImplementedError("$reason")
}

fun heyThisIsNotDoneYet(): Int {
  NOTDONE("wut")
}

fun main() {
  heyThisIsNotDoneYet()
}

This fails compilation with A 'return' expression required in a function with a block body ('{...}') for our heyThisIsNotDoneYet() function. Even though NOTDONE() is inline, and so heyThisIsNotDoneYet() will always throw an exception, the compiler is not quite smart enough to figure that out on its own. Adding Nothing as the return type to NOTDONE() clears that up:

public inline fun NOTDONE(reason: String): Nothing {
  throw NotImplementedError("$reason")
}

fun heyThisIsNotDoneYet(): Int {
  NOTDONE("wut")
}

fun main() {
  heyThisIsNotDoneYet()
}

On the Right Side of Elvis

TODO() is not the only function that returns Nothing. error() does as well:

public inline fun error(message: Any): Nothing = throw IllegalStateException(message.toString())

error() forms nice shorthand for throwing an exception based on a failed null check using the Elvis operator:

fun main() {
  val something: String? = "foo"
  val somethingNotNull = something ?: error("hey, that was null!")
  
  println(somethingNotNull)
}

The reason why this compiles is that Nothing is a sub-type of all types, including String. Hence, from a type-safety standpoint:

For Covariant Generics

The out keyword signifies covariance in a generic type: we can accept the type or any sub-type. List, for example, uses out:

interface List<out E> : Collection<E>

As a result, we can use a List<Nothing> in place of any other typed List: List<String>, List<Axolotl>, etc.

The same holds true for any covariant generic type.

For Generic Singletons

That previous section might seem esoteric. After all, if there are no instances of Nothing, we certainly cannot have a List of such instances. However, just because a List<Nothing> is impossible does not mean that a List<Nothing> has no uses.

For example, suppose we need an empty list of something. One way to do that is to use listOf() with no contents:

val things: List<String> = listOf()

An alternative is to use emptyList():

val things: List<String> = emptyList()

Those look nearly identical, but they actually have significantly different implementations. listOf() will instantiate an empty List, allocating a bit of memory along the way. emptyList() does not… because emptyList() returns a singleton:

public fun <T> emptyList(): List<T> = EmptyList

…and EmptyList is a List<Nothing>:

internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
    private const val serialVersionUID: Long = -7390468764508069838L

    override fun equals(other: Any?): Boolean = other is List<*> && other.isEmpty()
    override fun hashCode(): Int = 1
    override fun toString(): String = "[]"

    override val size: Int get() = 0
    override fun isEmpty(): Boolean = true
    override fun contains(element: Nothing): Boolean = false
    override fun containsAll(elements: Collection<Nothing>): Boolean = elements.isEmpty()

    override fun get(index: Int): Nothing =
      throw IndexOutOfBoundsException("Empty list doesn't contain element at index $index.")
    override fun indexOf(element: Nothing): Int = -1
    override fun lastIndexOf(element: Nothing): Int = -1

    override fun iterator(): Iterator<Nothing> = EmptyIterator
    override fun listIterator(): ListIterator<Nothing> = EmptyIterator
    override fun listIterator(index: Int): ListIterator<Nothing> {
        if (index != 0) throw IndexOutOfBoundsException("Index: $index")
        return EmptyIterator
    }

    override fun subList(fromIndex: Int, toIndex: Int): List<Nothing> {
        if (fromIndex == 0 && toIndex == 0) return this
        throw IndexOutOfBoundsException("fromIndex: $fromIndex, toIndex: $toIndex")
    }

    private fun readResolve(): Any = EmptyList
}

From a practical standpoint, both listOf() and emptyList() fill the role of an empty list, but because emptyList() returns a singleton, emptyList() does not allocate memory.

From a compilation standpoint, emptyList() can be applied to any type, because Nothing is a sub-type of any type.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.