Why it is not possible to not erase type by generi...
# language-evolution
a
Why it is not possible to not erase type by generics - because of JVM limitation? Currently I am not able to do this - as two LisenerBases collide as their types are erased at runtime.
Copy code
class Player : Listener<State> by ListenerBase<State>, Listener<Source> by ListenerBase<Source>
But why? Specified generic is a separate type to me. Why cannot I reuse one class for aggregation?
c
Yeah, pretty much just JVM limitations. In some contexts, like extension functions, you actually can have 2 functions differ only in their generic type parameter, and Kotlin won't care; the compiler resolves extension functions statically at compile-time. You will however need an
@JvmName()
annotation on one of those functions to disambiguate the two functions in bytecode. Implementing the same interfaces with different type parameters is less clear than extension functions on how it should work, though. While they are constrainted by JVM bytecode, I'd also argue that it is a good restriction in kotlin even without that; it would be clearer to have those callbacks be separate properties/functions within the class instead of being implemented by the class itself
a
Thanks for detailed answer. Personally I favour implicit composition (as using 'by' in my example) as opposed to extension functions. Functions are "torn away" and can be whenever they can be, however composition says in-place what to expect.
e
in that case I don't think you'd suggest having two different getters, would you?
a
In "my" case? If the interface which is used twice has method like this:
Copy code
interface Listener<T> {
    fun listen(value: T)
}
I expect it rolls out into separate methods:
Copy code
...
fun listen(value: State)
fun listen(value: Source)
e
I get that, but I don't think that's viable without causing issues for other cases
a
I see only the type erasure "in the way". If specific generic is "compiled" in this case there is no ambiguity (ok it can be if method signature does not contain T, but then it wont compile, yes - and thats ok)
e
if we had denotable union and intersection types, then I can imagine a future in which
Listener<in T>
➡️
Listener<State> & Listener<Source>
=
fun listen(value: State | Source)
Supplier<out T>
➡️
Supplier<State> & Supplier<Source>
=
fun get(): State & Source
but that's a way off too
creating two
fun get(): State
and
fun get(): Source
definitely can't be allowed without more invasive language changes
c
Union types get brought up from time to time, usually in the context of serialization, but I'm generally of the opinion that while they might "feel" good at the time to use, they ultimately make code harder to read and understand. it's easy enough to capture the same behavior with a simple sealed class like an
Either
monad, which gives you the same result but doesn't require any fundamental language changes, and makes it much more explicit in code the paths used for each of the two types in the union
e
whatever you call it, Listener<State> & Listener<Source> can't be implemented by a separate type Listener<Either<State, Source>> without more bridge methods going on behind the scenes
e
It goes way deeper into type system that just erasure. E.g., in Swift a class also cannot adopt the same protocol twice with different type parameters (associated types).