I have a problem around generics and sealed class....
# announcements
p
I have a problem around generics and sealed class. Considering theses classes :
Copy code
sealed class Bird
class Chicken: Bird()
class Turkey: Bird()

class Egg<T: Bird>
I want to implement a function
fun <T: Bird> T.layEgg(): Egg<T>
. The only way I found to implement it with the compiler being ok with is like that :
Copy code
fun <T: Bird> T.layEgg(): Egg<T> {
  return when(this as Bird) {
    is Chicken -> Egg<Chicken>()
    is Turkey -> Egg<Turkey>()
  } as Egg<T>
}

fun main() {
  val egg: Egg<Chicken> = Chicken().layEgg()
}
But IDEA tells me that the first
as Bird
in the when is useless, but if I remove it, the compiler complains that the when is not exhaustive. And the final
as Egg<T>
is marked as unsafe but I need it because returning an Egg<Chicken> is not and Egg<T>. Is there a way to have this implemented a better wat or is this simply impossible with generics ? Thanks
s
I think this is maybe what you really want
Copy code
fun <T : Bird> Bird.layEgg(): Egg<out T> {
  return when(this) {
    is Chicken -> Egg<Chicken>()
    is Turkey -> Egg<Turkey>()
  } 
}

fun main() {
  val egg: Egg<Chicken> = Chicken().layEgg()
}
edit: turns out this doesn’t actually compile, playground lied to me 👺
though doing this as an extension function is a bit weird from the beginning, I feel like. egg-laying sounds potentially like a bird-specific implementation process, perhaps it makes more sense to make it an abstract method in the Bird definition?
p
I tried :
Copy code
sealed class Bird {
  abstract fun <T: Bird> layEgg(): Egg<T>
}
But with this, every subclass has the same implementation of the layEgg method and when calling
layEgg()
on a subclass, I have to specify the type of Bird
Copy code
class Chicken: Bird() {
  override fun <T : Bird> layEgg(): Egg<T> = Egg()
}

class Turkey: Bird() {
  override fun <T : Bird> layEgg(): Egg<T> = Egg()
}

class Egg<out T: Bird>

fun main() {
  val egg = Chicken().layEgg<Chicken>()
}
b
Kotlin's type mechanisms are smarter than what you might think:
Copy code
sealed class Bird
data class Chicken(val x: String): Bird()
data class Turkey(val y: String): Bird()

class Egg<T: Bird>

fun <T: Bird> T.layEgg(): Egg<T> = Egg()

fun main(args: Array<String>): Unit {
    val egg = Chicken("x").layEgg()
    println(egg)
}
egg
is of type
Egg<Chicken>
in
main()
However, if each
Egg
requires a different constructor then you're back to
when
and an apparent failure at exhaustiveness checking
I did get a little closer with this. It still requires an "unchecked" cast to
Egg<T>
but IJ correctly determines the
when
is exhaustive:
Copy code
sealed class Bird
data class Chicken(val x: String): Bird()
data class Turkey(val y: Double): Bird()

class Egg<T: Bird>

fun layEggImpl(bird: Bird): Egg<*> = when(bird) {
    is Chicken -> Egg<Chicken>()
    is Turkey -> Egg<Turkey>()
}

fun <T: Bird> T.layEgg() = layEggImpl(this) as Egg<T>

fun main(args: Array<String>): Unit {
    val egg= Chicken("foo").layEgg()
    println(egg)
}
egg
in
main
is correctly inferred to be
Egg<Chicken>