Search code examples
kotlinkotlin-multiplatform

Kotlin multiplatform support for Optional


I'm working with a Java API now converted into multiplatform Kotlin. It used to use java.lang.Optional as the return type of many calls. I understand this is not the idiomatic Kotlin-way (see discussion) but this is an existing API, Optional stays (also it isn't a bad choice for the Java-facing client). My question is how?

Note: The code only needs to return Optional.of(x) or return Optional.empty() to the external API. Any internal uses will be purged.

  • How do we use expect/actual/typealias to use the real Optional class when available?
  • Is there a way to avoid re-implementing a fake Optional class on non-Java targets (i.e. work idiomatically with nullable? suffix)

Solution

  • At this point, Kotlin doesn't allow providing an actual typealias for an expected class with a companion object by using a Java class with matching static declarations. Follow this issue for updates: KT-29882.

    For now, you can workaround that by declaring the factory functions separately, outside the expected Optional class, as follows:

    expect class Optional<T : Any> {
        fun get(): T
        fun isPresent(): Boolean
        /* ... */
    }
    
    expect object Optionals {
        fun <T : Any> of(t: T): Optional<T>
        fun empty(): Optional<Nothing>
    }
    

    That should not necessarily be an object, you could just use top-level functions.

    Then, on the JVM, you would have to provide an actual typealias for the Optional class and, additionally, provide the trivial actual implementation for the Optionals object:

    actual typealias Optional<T> = java.util.Optional<T>
    
    actual object Optionals {
        actual fun <T : Any> of(t: T): Optional<T> = java.util.Optional.of(t)
        actual fun empty(): Optional<Nothing> = java.util.Optional.empty()
    }
    

    As for not providing an implementation for the non-JVM platforms, I doubt it's possible, as that would require some non-trivial compile-time transformations of the Optional usages to just the nullable type. So you would want something like this:

    actual typealias Optional<T> = T?
    

    which is now an error:

    Type alias expands to T?, which is not a class, an interface, or an object

    So you actually need a non-JVM implementation. To avoid duplicating it for every non-JVM target, you can declare a custom source set and link it with the platform-specific source sets, so they get the implementation from there:

    build.gradle.kts

    kotlin {
        /* targets declarations omitted */
    
        sourceSets {
            /* ... */
    
            val nonJvmOptional by creating {
                dependsOn(getByName("commonMain"))
            }
            configure(listOf(js(), linuxX64())) { // these are my two non-JVM targets
                compilations["main"].defaultSourceSet.dependsOn(nonJvmOptional)
            }
        }
    }
    

    Then, inside this custom source set (e.g. in src/nonJvmOptional/kotlin/OptionalImpl.kt) you can provide an actual implementation for the non-JVM targets.

    Here's a minimal project example on Github where I experimented with the above: h0tk3y/mpp-optional-demo