Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin force nullable generic type into a non-nullable of the same generic type?

First of all I just want to point out that I'm aware of the Force a null into non-nullable type and Kotlin Generics and nullable Class type but I don't think these questions are the same as mine (correct me if I'm wrong).

Background

I'm developing a library called Awaitility which, simply put, is designed to wait until a predicate evaluates to true. The Kotlin API provides a way to write expressions like this:

// Create a simple data class example
data class Data(var value: String)
// A fake repository that returns a possibly nullable instance of Data
interface DataRepository {
    // Invoked from other thread
    fun loadData() : Data?
} 
val dataRepository = .. // Implementation of DataRepository

// Now Awaitility allows you to wait until the "value" in Data is equal to "Something"
val data : Data = await untilCallTo { dataRepository.loadData() } has {
    value == "Something"
}

This works because has returns false if dataRepository.loadData() returns null and never calls the supplied receiver function ({ value == "Something" }) if data is null. Awaitility will also throw an exception if the condition is not satisfied so we know that what's returned from the expression has type Data (and not Data?) as you can see in the example.

The has function is implemented like this:

infix fun <T> AwaitilityKtUntilFunCondition<T?>.has(pred: T.() -> Boolean) = factory.until(fn) { t: T? ->
    if (t == null) {
        false
    } else {
        pred(t)
    }
} as T

where AwaitilityKtUntilFunCondition looks like this:

data class AwaitilityKtUntilFunCondition<T> internal constructor(internal val factory: ConditionFactory, internal val fn: () -> T?)

(you can also find the ConditionFactory here if needed)

While the example above works great when the lambda passed to untilCallTo returns a nullable type (Data?) it doesn't compile if we pass it a non-nullable type (i.e. Data). For example if we simply modify the repository to look like this:

interface DataRepository {
    // Invoked from other thread
    fun loadData() : Data // Notice that loadData now returns a non-nullable type
} 

and if we then try the same Awaitility expression as in the previous example:

val data : Data = await untilCallTo { dataRepository.loadData() } has {
    value == "Something"
}

we'll get a compile-time error:

Error:(160, 20) Kotlin: Type mismatch: inferred type is AwaitilityKtUntilFunCondition<Data> but AwaitilityKtUntilFunCondition<Data?> was expected
Error:(160, 68) Kotlin: Type inference failed. Please try to specify type arguments explicitly.

Which is (of course) correct!

Question

What I want to do is to somehow modify the has method to force the return type to always be the non-nullable equivalent of the type that's passed in as argument (which can be either nullable or non-nullable). I've tried to do something like this (which doesn't work):

infix fun <T, T2> AwaitilityKtUntilFunCondition<T>.has(pred: T2.() -> Boolean): T2
        where T : Any?, // Any? is not required but added for clarity
              T2 : T!! // This doesn't compile
        = factory.until(fn) { t: T ->
                if (t == null) {
                    false
                } else {
                    pred(t as T2)
                }
} as T2

This doesn't compile due to T2 : T!! but I hope that it shows my intention. I.e. I want to somehow define T2 as:

  1. The non-nullable equivalent of type T if T is nullable
  2. To be the same as T if T is a non-nullable type

Is this possible in Kotlin?

Update:

I've created a branch in the Awaitility project called has-with-non-nullable-type where you get the compile-time error I'm talking about in the file KotlinTest. This is what I want to make compile. You can clone it using:

$ git clone https://github.com/awaitility/awaitility.git

Update 2:

I've added gist that I think demonstrates the problem without using any dependencies.

like image 978
Johan Avatar asked Jan 18 '19 08:01

Johan


2 Answers

I created a minimal example which achieves what you want:

fun <T: Any> test(t: T?): T {
    // ...
    return t as T
}

You define an upper bound Any for T so it cannot be null. For the parameter t you use the type T?. In the end you return t casted to T.

Examples:

val a: String = test("Hello")
val b: String = test(null)
like image 106
Willi Mentzel Avatar answered Oct 18 '22 01:10

Willi Mentzel


AwaitilityKtUntilFunCondition can be made contravariant (so AwaitilityKtUntilFunCondition<T> is a subtype of AwaitilityKtUntilFunCondition<T?>), and this modification of your gist seems to satisfy the requirements:

// Fake Awaitility DSL
data class AwaitilityKtUntilFunCondition<out T>(val factory: ConditionFactory, val fn: () -> T)

infix fun <T : Any> AwaitilityKtUntilFunCondition<T?>.has(pred: T.() -> Boolean): T = factory.until(fn) { t: T? ->
    if (t == null) {
        false
    } else {
        pred(t)
    }
}!!

class ConditionFactory {
    fun <T : Any?> until(supplier: () -> T, predicate: (T) -> Boolean): T {
        val result = supplier()
        return if (predicate(result)) {
            result
        } else {
            throw IllegalArgumentException("Supplied value is not matching predicate")
        }
    }
}

class Await {
    infix fun <T> untilCallTo(supplier: () -> T): AwaitilityKtUntilFunCondition<T> {
        val conditionFactory = ConditionFactory()
        return AwaitilityKtUntilFunCondition(conditionFactory, supplier)
    }
}

// Example

data class Data(var state: String)
interface DataRepository<T> {
    fun loadData(): T
}

val nullableDataRepository: DataRepository<Data?> = TODO()
val nonNullableDataRepository: DataRepository<Data> = TODO()


// Both of these compile
val data1: Data = Await() untilCallTo { nonNullableDataRepository.loadData() } has {
    state == "something"
}

val data2: Data = Await() untilCallTo { nullableDataRepository.loadData() } has {
    state == "something"
}
like image 4
Alexey Romanov Avatar answered Oct 18 '22 01:10

Alexey Romanov