Skip to content

Instantly share code, notes, and snippets.

@vlsi
Last active October 2, 2024 10:17
Show Gist options
  • Save vlsi/3e7c6a0aa3f13ec0cfc94c0f7447ceb7 to your computer and use it in GitHub Desktop.
Save vlsi/3e7c6a0aa3f13ec0cfc94c0f7447ceb7 to your computer and use it in GitHub Desktop.
I would definitely recommend Kotlin generics to a Java fellow, here's why

Generics in Java are hard (sad, but true), however, Kotlin makes them way more accessible 🤩.

Suppose you are doing property-based testing. Just like I suggest, I was creating a jqwik test, and I ran into an issue with generic signatures.

Here's a minimal example, and it already has two issues.

Take a second and find both of them 😉

import java.util.function.Predicate;

interface Arbitrary<T> {
    /** Generate a sample object */
    T sample();

    /**
     * Create a new arbitrary of the same type but only allows
     * values that are accepted by the {@code filterPredicate}
     */
    Arbitrary<T> filter(Predicate<T> filterPredicate);

Issue 1: filter does not accept a more generic Predicate.

For instance, if you have Arbitrary<String> names, you can't filter it with Predicate<Object> hashCodeIsZero filter. Even though hashCodeIsZero is applicable to any object, the signature of filter(Predicate<T> just does not allow to pass Predicate<Object> there.

The Java solution is to add ? super T wildcard (Producer Extends Consumer Super):

    Arbitrary<T> filter(Predicate<? super T> filterPredicate);

Now it works.

However, there's a more severe issue there 🤔.

Issue 2: you can easily miss the wildcard, and the compiler won't help you there.

Java compiler does not know the intention of the code. It does not know Arbitrary<T> is supposed to produce T, and it does not know Predicate<T> is supposed to consume T.

Here's a case when Optional<T> was replaced with Optional<? extends T> for better API: SpongePowered/SpongeAPI#369

Is it that obvious that you need to declare returning values as Optional<? extends T>? I doubt so. The worse is that in Java you have to duplicate ? extends T again and again within the same class.

For instance, if you want to generate a list out of an arbitrary generator, the proper signature is Arbitrary<? extends T>. You never know that unless someone comes to you and suggests that something does not compile with subclasses.

interface Arbitrary<T> {
   // Note how "? extends T" is duplicated, and it is easy to forget :-/
   <T> ListArbitrary<T> list(Arbitrary<? extends T> elementArbitrary)
   <T> SetArbitrary<T> set(Arbitrary<? extends T> elementArbitrary)

Does Kotlin make it any better?

In Kotlin, the variance is specified at declaration site. That is a game-changer.

interface Arbitrary<out T> {
    sample(): T;
    filter(filterPredicate: Predicate<T>): Arbitrary<T>
  1. The code is easier to read. The interface declaration means "this interface produces T".

  2. The code fails to compile 🎉

     Type parameter T is declared as 'out' but occurs in 'invariant' position in type Predicate<T>
    

    What it says is that "ok, you said T was supposed for producing values, however, we don't know if Predicate<T> produces or consumes"

    With Java you have to remember that you need specify wildcards in cases like filter(Predicate<? super T> predicate), and Kotlin either infers the variance from the type variable declaration or it stops with a compilation error.

It is sad there's no way to teach Kotlin compiler that java.util.function.Predicate<T> means java.util.function.Predicate<in T>, however, it is really helpful that Kotlin detects issues before you even start using the API.

The fix would be:

interface Arbitrary<out T> {
    sample(): T;
    // Explicit `Predicate<in T>` is needed since Java's Predicate does not declare variance
    filter(filterPredicate: Predicate<in T>): Arbitrary<T>

The case with list is easier with Kotlin:

interface Arbitrary<out T> {

   // It just works, and it generates `Arbitrary<? extends T> elementArbitrary` in the bytecode
   <T> list(elementArbitrary: Arbitrary<T>): ListArbitrary<T>
   <T> set(elementArbitrary: Arbitrary<T>): SetArbitrary<T>

There must be edge cases even with Kotlin

Unfortunately, Kotlin does not capture all the issues when using Java classes.

For instance, if you use java.util.function.Function, most likeky you would need to specify out variance.

interface Arbitrary<out T> {
    // Fails to compile since T has out variance in Arbitrary and invariant in Function
    fun <U> map(mapper: Function<T, U>): Arbitrary<U>
    
    // Compiles, however it does not allow passing functions that return a subtype
    fun <U> map(mapper: Function<in T, U>): Arbitrary<U>

    // Perfect declaration, `out` needs to be added manually :-/
    fun <U> map(mapper: Function<in T, out U>): Arbitrary<U>

If you use Kotlin types (e.g. (T) -> R), then you get the variance for free since Kotlin functions declare it: interface Function1<in P1, out R>.

Can I keep code in Java and keep Kotlin consumers happy?

Unfortunately, Java does not allow to specify generic variance at the declaration site.

There's an open Kotlin compiler issue: KT-41062 Allow Java classes to specify declaration-site variance, and the takeaway there is

  1. You could migrate the interfaces to Kotlin
  2. You could attach the metadata to your Java classes. kotlinx.metadata project might be helpful for generating the metadata if you absolutely want to avoid Kotlin in dependencies.
  3. You could vote for @In and @Out annotations in JSpecify

Conclusions

All-in-all, I would definitely recommend using Kotlin generics to a friend:

  1. Code with generics in Kotlin is easier to read and write
  2. Kotlin compiler captures more bugs, and it captures programmer's intention better than Java

If you want to see how it fits in a bigger picture, here's a draft PR for jqwik that migrates core API to Kotlin: jqwik-team/jqwik#302

PS If you like testing, consider proposing a talk via https://heisenbug.ru/en/callforpapers/ PS If you have a cool JVM story, consider proposing a talk via https://jpoint.ru/en/callforpapers/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment