avatarGabriel Shanahan

Summary

The provided content discusses the enhancements and solutions that Kotlin brings to the Java type system, focusing on the Any, Unit, and Nothing types, and how these address the limitations of Java's type system.

Abstract

The article delves into the intricacies of Kotlin's type system, emphasizing how it improves upon Java's by introducing the Any, Unit, and Nothing types. It explains that Kotlin treats all types as objects, with Any being the equivalent of Java's Object. The Unit type is introduced as a more robust alternative to Java's void, allowing for function calls to be treated as expressions. The concept of Nothing is presented as a type for expressions that do not return a value, addressing a significant gap in Java's type system. The article also touches on Kotlin's type inference, function types, and the implications of these features for nullability, covariance, and contravariance, ultimately demonstrating Kotlin's more consistent and expressive type system compared to Java.

Opinions

  • The author suggests that Kotlin's type system is superior to Java's, as it eliminates several weaknesses and inconsistencies.
  • The article implies that Kotlin's Unit and Nothing types are crucial for enabling functional programming patterns and higher-order functions.
  • The author expresses that Kotlin's explicit bottom type (Nothing) is a significant improvement over Java's handling of non-returning expressions.
  • The article conveys that Kotlin's approach to types and nullability leads to more robust and error-proof code.
  • The author seems to appreciate Kotlin's design for its logical consistency and adherence to type theory principles, particularly in the context of generic types and the empty list scenario.
  • The use of Unit as a singleton type is presented as a practical and logical design choice that simplifies the language's semantics.
  • The author encourages readers to explore additional resources to gain a deeper understanding of Kotlin's type system and its advantages over Java's.

Types

A detailed explanation of the problems int the Java type system, and how Kotlin fixes them using Any, Unit and Nothing.

— — — — — — — — — — — — — — —

THE CURRENT VERSION OF THIS ARTICLE IS PUBLISHED HERE.

— — — — — — — — — — — — — — —

Tags: #FYI #FUNDAMENTAL CONCEPT

This article is part of the Kotlin Primer, an opinionated guide to the Kotlin language, which is indented to help facilitate Kotlin adoption inside Java-centric organizations. It was originally written as an organizational learning resource for Etnetera a.s. and I would like to express my sincere gratitude for their support.

It is recommended to read the Introduction before moving on. Check out the Table of Contents for all articles.

The topic of types can be confusing to newcomers, and I don’t want to bog you down with the details when you’re just starting to learn. However, it is still important to understand what’s really going on. Therefore, I split this section into two parts.

Definitely read this

Tags: #FYI

  • Kotlin does not have primitive types — everything is an object. There is no int, only Int. This does not affect performance, as these types are automatically compiled to primitive values whenever possible.
  • The equivalent of the Java Object type is called Any in Kotlin
  • Instead of void, use Unit
  • If you ever encounter Nothing, read the next section

Kotlin has type inference — in some situations, types can be omitted:

  • when using =, i.e. when assigning a value to a variable or when defining a function as an expression
  • when a function returns Unit
  • after a type check

Type casts are performed using the keyword as. If the cast is illegal, a ClassCastException is raised. Alternatively, you can use the safe cast operator as?, which evaluates to null if it fails.

Kotlin has function types. The type (Int, String) -> Int represents a function that takes two arguments, an Int and a String, and returns an Int. A lot more will be said about function types, lambdas and higher order functions in a future lesson. Don’t worry about them for now.

If you want, skip this and come back to it when you encounter type-related behavior you don’t understand

Tags: #FUNDAMENTAL CONCEPT

The problems of Java types

The type system in Java has three large holes in it:

  1. There is no type that represents “no value returned”. Java has void, but that is not a type - you cannot write x instanceof void, void.class, etc.
  2. There is no type that represents “null value returned”. Try writing public ??? iReturnNull() { return null; } — what should you put instead of the ???? It turns out that Java will accept any non-primitive value in place of ???, i.e. any class.
  3. There is no type that represents “value not returned”. This is not the same as void! Compare the following:
  • public void iReturnNoValue() { }
  • public ??? iDontReturnAtAll() { throw new RuntimeException(); }
  • public ??? iDontReturnAtAll2() { while(true) { } }

Java will accept anything in the place of ???, including primitive types.

There are many consequences of these problems:

  • Type checking is much weaker, which makes the code error prone (e.g. NPE’s)
  • Co/contra-variance don’t work properly. For example, there is no reasonable way to define the type of an empty list. Think about it — a list of Ints is List<Int> and a list of Strings is List<String>, and you can never interchange the two except when the lists are empty — an empty list is an empty list, it does not matter of what, there’s nothing in it. So whatever it is, it has to be useable in place of any List<T>, which means being a subtype of List<T> for any T, which is impossible in Java.
  • You cannot write Function<T, void> or Function<void, T>. Lambdas which return no value (or accept no value) need completely separate types (Supplier, Consumer)
  • Combining the previous with the horror of checked exceptions, it is impossible to have reasonable functional types in Java, which in turn makes working with higher order functions (map, flatMap, filter etc.) very unwieldy

The Kotlin type system is designed to remedy these problems. It’s type hierarchy is structurally very similar to the Java type hierarchy, with four notable differences:

  • The Any type
  • The Unit type/object
  • The Nothing type
  • Nullable types (discussed previously)

Thanks to these, in Kotlin, every expression always has a single, well-defined type. This is not true in Java.

The Any type

The Any type in Kotlin is the same as Object in Java - everything inherits from it. That's all.

The Unit type

The Unit type is to void what a Real Boy is to Pinocchio - it's what it always wanted to be. Like void, Unit represents "no value", or to be more precise, "empty value". Unlike void, it is an actual type. Unit inherits from Any.

// Java
class Class {
  public void iReturnVoid() {
    System.out.println("Hello World!");
  }  
}
//Kotlin
fun iReturnUnit(): Unit {
    println("Hello World!")
}

In Java, a call to iReturnVoid() cannot be assigned:

??? result = iReturnVoid();

This is precisely because void is not a type - we cannot write void in place of ???.

In Kotlin, a call to iReturnUnit() can be assigned:

val result: Unit = iReturnUnit()

A natural question to ask is: what value does result contain? To answer this, you need one last piece of information: Unit is actually a singleton - a type which has exactly one instance associated with it.

In Java, you would have something like this:

public final class Unit {
  public static final Unit INSTANCE;
  private Unit() {}
  static {
    INSTANCE = new Unit();
  }
}

You would write Unit to refer to the type, and write Unit.INSTANCE to refer to the value.

However, there is no added benefit of writing different things for the type and the value, because types can never appear in the same places in code as values. Therefore, in Kotlin, both the type and the value are referred to by the same text, Unit, because the location is enough to specify if we are talking about the type or the value.

Woa woa woa, hold your horses, I hear you saying. Didn’t we start this whole thing by saying that Unit, like void, means "no value"? But we just spent two friggin' paragraphs talking about the value of Unit!

Think of it this way. A list is a collection of objects. It most definitely is a “thing”, a value. What about the empty list? It contains nothing, but it is a value — the special, empty value.

You can think of Unit in a very similar fashion — it is the “thing” that represents “no thing”, the value that means “empty value”. Among “collection of values”, we have the empty list. Among “values”, we have Unit. It plays the same role. It means “this function returns, but the return value contains no information” — it is “empty”.

Why is this useful? Because, among other things, it allows us to treat all function calls as expressions — all functions can be assigned, lambdas returning Unit can be treated like any other lambda and so on. No more special behavior for void. No more problems when we try to use generics with functions. Everything just works.

I highly recommend reading this article as well, which gives a nice cross-language overview of this issue and goes a little deeper into the theoretical aspects at the end.

The Nothing type

Jeeeeezus! Nothing, no thing, Unit, empty, psdjgolg, ARGH!

I know, I know. Just hold on, we’re almost there.

While we’ve already talked about returning “a thing” (a value) and returning “empty thing” (Unit), there is one last situation we haven't talked about, which is not returning at all.

Take a look at the following:

class Example {
  public ??? iDontReturnAtAll() {
    return iDontReturnAtAll();
  }

  public ??? iDontReturnAtAll2() {
    throw new Exception();
  }   
}

Java will let you write anything in the place of the first ??? except for void, and anything including void in the place of the second ???. Excluding cases when you use void, the compiler will allow you to assign the result of both functions. That's not really a problem - the function will never return anything, because it will never actually return at all, but we can probably agree that it's still a little strange.

What’s even stranger is that this will compile:

class Example2 {
  public static <A> A test() {
    throw new IllegalStateException();
  }
  
  public static main(String[] args) {
    int x = test();
    String y = test();
    Object z = test();
  }
}

This is code that promises to produce a value of type A for every type A. It compiles fine, even though a method cannot possibly return an instance of any type A without accepting an A as well.

Now, naturally, we know that this code is fine, precisely because those functions never will return. So it’s not a problem that the compiler doesn’t report an error, it’s just that we would expect the type of functions (and expression) that don’t return to be a little more rigorous than the WhateverFloatsYourBoat type.

In Java, we can just leave it at that, because excluding the weird examples above, these things never appear in situations where types are necessary — throw is not an expression, it's a statement, so you can't assign it, if is also a statement, not an expression, same with switch, there is no when, you cannot throw in a ternary expression and so on.

However, in Kotlin, you can do this:

Since if's are expressions, they can be assigned. The variable test must have a precisely defined type, which means that, in turn, the types of both branches must be precisely defined - the one with the throw can't just be random. The same goes for when expressions, or defining functions with expression bodies.

But we can see that Java’s type system is lacking in this — it has no type that denotes “does not return at all”. Kotlin fixes with the Nothing type.

Let’s look at a couple of properties of the Nothing type.

Nothing cannot be instantiated and no value can have type Nothing

This is a logical consequence of the fact that Nothing denotes things that don't return. Therefore, we can never instantiate it, because by definition, the instantiation never finishes.

Nothing is a subtype of every type

When the return type of a function is X, we can either return X or any subtype of X. However, a function can always throw and as we saw above, we can use throw in an expression. Since we must be able to use throw in any expression, it must be assignable to any type, which by definition makes it a subtype of every type. It is no coincidence that Nothing is often called the bottom type in type theory.

That last example actually hints at a nice consequence of having an explicit bottom type (the technical term for Nothing) in a language with generics. Let’s use lists as an example.

Sometimes, when we are working with a List<Supertype>, we want to be able to pass in a List<Subtype> as well - for example, if we have an algorithm that calculates the average of a List<Number>, we should be able to apply it to List<Float>, List<Int> and List<Double> with no problems. The technical term for this is that List is covariant in T, but if you ever say that out loud people will just run off screaming, so instead we're going to say that List has a unicorn-lollipop-rainbow relationship with T.

Just to make things clear, let’s state this one more time: if we say that Container<T> has a unicorn-lollipop-rainbow relationship with T, that means that when SUBTYPE is a subtype of SUPERTYPE, then Container<SUBTYPE> is a subtype of Container<SUPERTYPE>. In our example, List<T> has a unicorn-lollipop-rainbow relationship with T and Int is a subtype of Number, so List<Int> is a subtype of List<Number>.

Okay, that’s dope and all, but why are we even talking about this? I hear you say, exhaustedly.

Here’s why: what about the empty list? If we have an empty list of Ints or Floats, there's no problem, we can just pass it in — it might cause a runtime error depending on the way the algorithm is implemented, but it will certainly compile. But what about an empty list of Strings? Well, we can't do that, you say. The types don't match.

But we should be able to do it. There is no difference between an empty list of Ints, an empty list of Floats or an empty list of any other type. It's all just an empty list — an empty list of strings, floats, numbers, whatever, should always have the same type.

To achieve that, the empty list has to have a type List<X> such that List<X> is a subtype of every List<T>. Since List has a unicorn-lollipop-rainbow relationship with its type parameter, this means that X must be a subtype of every T, which means that, you guessed it, X must be Nothing. And, lo and behold, the type of emptyList()is indeed List<Nothing>. Notice that if you read it out loud, it kinda has a nice ring to it — a List<Int> is a list containing Ints, and a List<Nothing> is a list containing nothing.

This all come together rather nicely when you consider the generic function fun <T> firstElementOf(list: List<T>): T.

This function returns the first element of the List<T>, which is a value of type T — in other words, it always has to return whichever type is between the < and >. Does this still hold when we pass in an empty list?

Well, what happens when we pass in an empty list? The function throws — it has to, because the return type is not nullable, so there is nothing it could possibly return.

Pause for a second and think about what just happened — we deduced an implementation detail just by looking at the type signature. This is another demonstration of how useful the Kotlin type system is.

And what is the type of the empty list? List<Nothing>. What is the type of a throw? Nothing! It all just works. No surprises, no special treatment for edge cases, just a single set of consistent rules. Awesome!

I highly recommend reading this excellent article, which gives a nice overview of all this as well as mentioning other interesting theoretical aspects of this issue.

Let’s close with an interesting exercise. What is the type of x in the following code snippet?

val x = null

Make a guess, and then use the shortcut mentioned in the previous article to check your answer.

Go back to Nullability, jump to the Table of Contents, or continue to Literals.

Join me in Etnetera
Kotlin
Java
Programming
Types
Type Systems
Recommended from ReadMedium