Very minor issue but I’m a little surprised I have to do an unchecked cast below.
I have a BigThing that contains a SmallThing and some other things.
I want BigThing to be parametrized on SmallThing otherwise, I have to do a lot of casting of BigThing.smallThing all over the place elsewhere in the code.
The problem comes when I try to use polymorphism to create a BigThing from an existing instance of SmallThing. Never mind why, but, the code for these factory functions can’t be unified.
The problem is at the factory call site, I have to do an unchecked cast that I wasn’t expecting.
Since the abstract function returns a BigThing<*>, it seems that the compiler can’t figure out the return type at the call site based on the type of the receiver.
Is this unavoidable? Is this something that might get better in future versions of Kotlin as type inference continues to improve?
Is there a better way to organize this to get the power of polymorphism without the casting?
class BigThing<out A : SmallThing>(
val smallThing: A,
val otherStuff: Any,
)
abstract class SmallThing {
// I can't make this a generic function because the implementors are specific about the type
abstract fun bigThingFactory(otherStuff: Any): BigThing<*>
}
class CategorySmallThing : SmallThing() {
override fun bigThingFactory(otherStuff: Any): BigThing<CategorySmallThing> = TODO()
}
open class RealSmallThing : SmallThing() {
override fun bigThingFactory(otherStuff: Any): BigThing<RealSmallThing> = TODO()
}
class SpecialSmallThing : RealSmallThing() {
// NOTE this is one reason the type parameter A of Thing must be covariant.
override fun bigThingFactory(otherStuff: Any): BigThing<SpecialSmallThing> = TODO()
}
class CallSite {
fun <A : SmallThing> getAThing(smallThing: A, otherStuff: Any): BigThing<A>? {
// What do I need to do so that this cast isn't needed?
// The compiler allows this cast but warns that it is unchecked.
@Suppress("UNCHECKED_CAST")
val value: BigThing<A> = smallThing.bigThingFactory(otherStuff) as BigThing<A>
// do other stuff
return value
}
}
Note that in your current implementation, a pathological subclass can result in a CCE:
open class RealSmallThing : SmallThing() {
override fun bigThingFactory(otherStuff: Any): BigThing<CategorySmallThing> = TODO()
}
What you’re looking for are self-types, which are missing from Java too, but can be easily emulated (I think you can add an out here btw):
abstract class SmallThing<Self: SmallThing<Self>> {
abstract fun bigThingFactory(otherStuff: Any): BigThing<Self>
}
Everything now typechecks without an unsafe cast!
Note that this emulation of self types is slightly bigger than self types: it admits types such as:
open class WeirdSmallThing : SmallThing<RealSmallThing>()
However, none of those weird types will typecheck with your CallSite method because the A: SmallThing<A> and then using a value of A ensures that the value you have is referring to itself.
Btw, you might consider using interfaces for nicer code here, perhaps even sealed interfaces, which would allow you to eschew self types entirely and instead define your factory as a monolithic when expression
Cool, thanks! I had tried something along those lines but not hard enough.
Note that in your current implementation, a pathological subclass can result in a CCE:
true.
Here’s what I ended up with:
class BigThing<out A : SmallThing<A>>(
val smallThing: A,
val otherStuff: Any,
)
// when I'm creating [BigThing]s, I would like to use polymorphism and just call [thingFactory] on
// an existing instance of [SmallThing].
abstract class SmallThing<out S : SmallThing<S>> {
abstract fun bigThingFactory(otherStuff: Any): BigThing<S>
}
class CategorySmallThing : SmallThing<CategorySmallThing>() {
override fun bigThingFactory(otherStuff: Any): BigThing<CategorySmallThing> = TODO()
}
open class RealSmallThing<out S: RealSmallThing<S>> : SmallThing<S>() {
override fun bigThingFactory(otherStuff: Any): BigThing<S> = TODO()
}
class SpecialSmallThing : RealSmallThing<SpecialSmallThing>() {
override fun bigThingFactory(otherStuff: Any): BigThing<SpecialSmallThing> = TODO()
}
class CallSite {
fun <A : SmallThing<A>> getAThing(smallThing: A, otherStuff: Any): BigThing<A> {
val value: BigThing<A> = smallThing.bigThingFactory(otherStuff)
// do other stuff
return value
}
}
Invalidate caches and restart usually helps. Are you using K2 mode? it’s definitely a bit buggy still.
The fact that you needed unsafe-variance suggests that maybe your code is slightly off though. it can be fine to use it occasionally, but I think you should somehow see if you even need the out here? (I think your code can work without it, nothing I’ve written so far relies on out-variance for SmallThing subclasses)