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:
T
if T
is nullableT
if T
is a non-nullable typeIs 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.
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)
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"
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With