You never touched Groovy, nor did you jump on the Scala train. Clojure never attracted you; and you heard about Ceylon long after the language had already died. You are one of those old-fashioned Java folks! But now, after all those years, you want to join the cool Kotlin kids. So, where to start? Let’s discover the language together by decompiling it to Java code. Today: The things we tend to forget!

Today: The things we tend to forget

Recently I was talking to a colleague about the difference of generic types between Java and Kotlin. Though I prefer Kotlin above Java code, I complained you could not add variance when using the (A) → B function syntax in Kotlin.

Quick refresher on variance, consider Java’s map method in the Stream API:

public interface Stream<T> {
  <R> Stream<R> map(Function<? super T, ? extends R> mapper);
}

It means as much as: the map method takes a function with an input parameter of T or one its super types and it returns an output of R or its subtypes. Or written more formally, the map method takes a function with a contravariant input and a covariant output as method argument. This generic behaviour gives you the option to write generic mapper functions:

Function<Number, BigDecimal> toBigDecimal = it -> new BigDecimal(it.toString());

Stream.<Integer>of(1).map(toBigDecimal);
Stream.<Long>of(1L).map(toBigDecimal);
To understand variance in Java and Kotlin even beter, read the excellent covariance and contravariance blogs by Vishal Ratna! If you are a more of a visual person, then this article may help you out. Also, see my very own Kotlin Discovered Variance blog.

To imitate the same concept in Kotlin, we can write our own incomplete implementation of a Stream object:

import java.util.function.Function

class Stream<T>(vararg val t: T) {
  fun <R> map(mapper: Function<in T, out R>): Stream<R> = TODO()
}

This work exactly the same as Java’s Streaming API; don’t forget in means contravariant and out covariant:

val toBigDecimal = Function { it: Number -> BigDecimal(it.toString()) }

Stream<Int>(1).map(toBigDecimal)
Stream<Long>(1L).map(toBigDecimal)

But then, when we change the java.util.function.Function to Kotlin’s function type syntax, it does not compile anymore:

class Stream<T>(vararg val t: T) {
  fun <R> map(mapper: (in T) -> out R): Stream<R>
}

The compiler start throwing Unsupported [modifier on parameter in function type] errors. However, it works fine if we remove the variance keywords:

class Stream<T>(vararg val t: T) {
  fun <R> map(mapper: (T) -> R): Stream<R> = TODO()
}

val toBigDecimal = { it: Number -> BigDecimal(it.toString()) }

Stream<Int>(1).map(toBigDecimal)
Stream<Long>(1L).map(toBigDecimal)

How is this possible? Shouldn’t the (T) → R function not be invariant when we define it like this? Maybe by decompiling the Stream object, we can understand it. So let’s do that:

public final class Stream {
  private final Object[] t;

  public final Stream map(@NotNull Function1 mapper) {
    Intrinsics.checkNotNullParameter(mapper, "mapper");
    throw new NotImplementedError((String)null, 1, (DefaultConstructorMarker)null);
  }

  // constructor and getter
}

Whoops, I totally forgot the (T) → R syntax is just syntactic sugar for the Function1 function. And as we have seen before, the functional interfaces of Kotlin use declaration-site variance for it generic types:

public interface Function1<in P1, out R> : kotlin.Function<R> {
  public abstract operator fun invoke(p1: P1): R
}

So in the end, I was both right and wrong at the same time. I was right when I said that you cannot add variance keywords when using the function type syntax. But I was also wrong, because those function are 'already' equipped with variance by Kotlin natively.

Well, that’s enough for one day. Stay tuned for more!

shadow-left