I could not find an existing topic regarding this issue but maybe my keywords were simply not the right ones to search for. Regardless, here it comes.
I would love to see default types for generics in Kotlin like Rust has it:
The most prominent example that could make use of this would be Comparable<T> by changing it to Comparable<T = this> but there are more interesting use cases.
sealead class S
class A : S()
class B : S()
open class X<T : A>
class Y : X<B>()
Constructing X is not possible without specifying the concrete type right now, hence, one has to write X<A>() everywhere. This is cumbersome and repetitive and something that was clear for X from the very beginning.
open class X<T : A = A>
class Y() : A<B>()
Now it is possible to construct X without specifying A everywhere: X()
Based on this post here I guess that this is something that will be part of 1.4 or will be implemented in one of the 1.4.x releases. I haven’t seen it talked about as part of 1.4 so probably not the first release.
class MyClass<T> {
companion object {
operator fun invoke() = MyClass<Int>()
}
}
fun main() {
val foo = MyClass<String>()
val bar = MyClass() // this is a MyClass<Int>
}
In my case I have a ViewModel Class that accepts an inputDataAdapter so that a ViewModel for a specific view can provide a TypeAdapter from String to “T”. But in some or many cases a simple (String) → String inputDataAdapter could be provided as Default Lambda.
The only way that I figured out is to “dirty” cast to T. If I could provide a Default Type for “T” the dirty cast is no more needed.
class ViewModel<T: DA = String>(
val inputDataAdapter: (String) -> DA = { input -> input },
)
// instead of
class ViewModel<T>(
val inputDataAdapter: (String) -> T = { input -> input as T },
)
I need to set a default type for var value: T. I can’t use lateinit here, what should I use except of null if I want to make the variable non-nullable?
Found a nice work-around:
public suspend fun <T> Flow<T>.first(): T {
var result: Any? = NULL
collectWhile {
result = it
false
}
if (result === NULL) throw NoSuchElementException("Expected at least one element")
return result as T
}
You can use some marker object defined above the function like so:
private object EMPTY
public suspend fun <T> Flow<T>.first(): T {
var result: Any? = EMPTY
collectWhile {
result = it
false
}
if (result === EMPTY) throw NoSuchElementException("Expected at least one element")
return result as T
}
It’s the Flow original method I found to be usable. You can’t use null as it breaks the needed conditions (you need to use value!! or likewise later everywhere). NULL here is
@JvmField
@SharedImmutable
internal val NULL = Symbol("NULL")
There is nothing wrong in using !! in cases it is really needed. In this specific case you actually wouldn’t need to use it, because if (result == null) throw automatically smart-casts to not-nullable. Kotlin is that smart!
I believe the reason what they used NULL in this function is because the flow itself could pass null values, so they needed a way to distinguish situation where we collected null or not collected anything. Alternatively, they could use a separate boolean flag, but such NULL object is often easier to use and even better for performance. Drawback is that we have to use an unchecked cast, which we wouldn’t need if not using NULL.
One advantage of default values for generic types is that it potentially reduces the amount of code changes that come as a consequence of changing the set of generic type parameters of a class (adding/removing a type parameter).
For instance, in the code base I am currently working on, a certain class A (without type parameters) is used widely throughout the code, Now it would make sense to add a generic type parameter T, leading to A<T>. But 95% of the code base does not care about T, so it would use A<*> almost everywhere (you may point out that this is an indication of poor design, and you may be right, but that is still a different discussion).
Having default values would allow to leave all those places where A would otherwise be replaced by A<*> as it is, limiting the amount of changes dramatically, focusing only on those places where the change (in parameters) is actually “relevant”.
(Of course, in other projects, instead of *, the above mentioned might hold for a certain class X that shall be used by 95% of code for parameter T.)
So, without having default values, introducing T would lead to a massive PR, almost changing the whole code base.
In my instance, I don’t think the team will accept this, arguing that the (moderate) benefit (here: type safety + minor code simplification) is not justifying the huge amount of code changes. In addition, in near future, other type parameters would likely to follow, which would again lead to a massive change and so on.