I have the following slightly complicated usecse. I declare a method in base class, which accepts functional parameter with receiver. I want to specialize this method in a subclass, basically using more specialized receiver. This actually creates a new signature, i.e. overloaded method. But compiler fails to choose the correct method, because it has no way to infer the receiver type, which is actually defined by a lambda block.
Let me demonstrate with the following example:
open class BaseModifier {
var property : String = ""
}
open class Base {
fun modify(modifier: BaseModifier.() -> Unit) {
val instance = BaseModifier()
instance.modifier()
}
}
class SubclassModifier : BaseModifier() {
var anotherProperty : String = ""
}
class Subclass: Base() {
fun modify(modifier: SubclassModifier.() -> Unit) {
val instance = SubclassModifier()
instance.modifier()
}
}
...
val instance = Subclass()
instance.modify {
property = "something" // ok
anotherProperty = "something" // problem
}
I would expect this code to work. I don’t know on which basis compiler chooses which method to call. He apparently chooses the method with BaseModifier as the receiver type, so I cannot access features of the specialized receiver implementation.
Is there a way how to resolve this ambiguity?
I can do this, but it is not very nice workaround:
val modifier: SubclassModifier.() -> Unit = {
property = "something"
anotherProperty = "something"
}
instance.modify(modifier)
Well, this would allow the client to pass literally any configuration block. Which can probably be fixed by adding lower bound to the T type. Anyway, this is not actually what I want, I don’t want the client to choose which modifier he wants, I want the modifier to be tightly coupled with the implementation, imagine there are more Subclasses each with its own modifier, that should not be interchanged.
But using generics seems to be a good idea.
This is perhaps closer to what I want:
open class BaseModifier {
var property : String = ""
}
abstract class Base<M : BaseModifier> {
abstract fun modify(modifier: M.() -> Unit)
}
class SubclassModifier : BaseModifier() {
var anotherProperty : String = ""
}
class Subclass: Base<SubclassModifier> {
override fun modify(modifier: SubclassModifier.() -> Unit) {
val instance = SubclassModifier()
instance.modifier()
}
}
now the modifier type is prescribed by the generic parameter of the whole base class. This is almost it. The problem is that the generic parameter M leaks into the Base class interface, which is unacceptable, you would need to use Base<*> every time you refer to this type. The parameter exists purely for this technical reason, clients should not be bothered with it.
The method fun modify(modifier: SubclassModifier.() -> Unit) does not override fun modify(modifier: BaseModifier.() -> Unit).
I can define the class class OtherModifier : BaseModifier() and write the code:
val otherModifier = OtherModifier()
val instance: Base = Subclass()
instance.modify(otherModifier) // error: otherModifier is not an instance of SubclassModifier
For this reason you cannot freely mix Subclass: Base<SubclassModifier> with any other subclass of Base, and for this reason the compiler force you to use Base<*>.
Requiring a specific subtype (eg: SubclassModifier) is not a detail.
And here’s the problem: SubclassModifier.() -> Unit is not more specialized. Actually, it is less specialized. This is why Kotlin prefers the function with BaseModifier.() -> Unit - when overloading the compiler always favors more specialized function.
Ok, but how BaseModifier could be more specialized than SubclassModifier? Well, it appears in the consumer/contravariant position, so subtyping is kind of reversed. Consumer of numbers could be safely used as a consumer of integers, but not the opposite.
val base: BaseModifier.() -> Unit = {}
val sub: SubclassModifier.() -> Unit = base // safe - upcasting
val base2: BaseModifier.() -> Unit = sub // compile error - downcasting
Yes, this is very counter-intuitive for humans and I can’t even find a good real-life example on why a consumer of numbers is “more specific” than consumer of integers. But let’s say only if we have BaseModifier.() -> Unit then we can call a function receiving BaseModifier.() -> Unit. if we have SubclassModifier.() -> Unit, then we can call both functions. So the first function is more picky/specific and this is why it is generally preferred when overloading.
Thanks for good insights @broot. Yes, things are a little confusing when using consumers, as well as the fact that type with receiver is actually a consumer in disguise. So the problem is not related to type with receiver after all, I could have used regular consumers. Except that with regular consumers, I can prescribe the type of the consumer in the lambda:
fun consume(consumer: (Number) -> Unit)
fun consume(consumer: (Int) -> Unit)
consume { println(it) } // calls number consumer
consume { it: Int -> println(it) } // calls int consumer
I do not agree, though, that it is obvious that the compiler should default to the number consumer. At least not on the basis of reversed subtyping of consumers you described.
Anyway, I still think that the usecase is legitimate, and can’t be easily resolved by overloaded function, so probably the best solution is simply to rename the base implementation.
interface Base {
fun modifyBase(modifier: (BaseModifier) -> Unit)
}
class Subclass: Base {
override fun modifyBase(modifier: BaseModifier.() -> Unit) = modify(modifier)
fun modify(modifier: SubclassModifier.() -> Unit) {
...
}
}
This works as indented, but you cannot collapse the two operations modifyBase and modify into the same identifier.
override fun modifyBase(modifier: BaseModifier.() -> Unit) = modify(modifier)
Because of the contravariance explained above, you can’t really do that; modify is able to consume SubclassModifier and its subclasses, and modifyBase can be called with a consumer that is able to consume a BaseModifier that is not a SubclassModifier, which will raise a runtime error if forced.
There is a way to cheat, recognizable by the unchecked cast. This cast will fail if you forget to override the modify method in one of the subclasses of Base.
interface Modifiable<out T> {
fun modify(modifier: T.() -> Unit)
}
open class BaseModifier {
var property : String = ""
}
open class Base<T: BaseModifier> protected constructor(): Modifiable<T> {
override fun modify(modifier: T.() -> Unit) {
val instance = BaseModifier()
(instance as T).modifier()
}
companion object {
operator fun invoke() = Base<BaseModifier>()
}
}
class SubclassModifier : BaseModifier() {
var anotherProperty : String = ""
}
class Subclass: Base<SubclassModifier>() {
override fun modify(modifier: SubclassModifier.() -> Unit) {
val instance = SubclassModifier()
instance.modifier()
}
}
The trickery in Base could be repeated in Subclass, if more levels of madness are needed.
Needless to say, I don’t condone this cursed abomination, I wrote it purely out of curiousity, defiance and malice.
Because of the contravariance explained above, you can’t really do that; modify is able to consume SubclassModifier and its subclasses, and modifyBase can be called with a consumer that is able to consume a BaseModifier that is not a SubclassModifier , which will raise a runtime error if forced.
I guess I can, @brunojcm , what do you mean? After all, the code example above actually compiles.
If I have a consumer that is a subclass of BaseModifier, it won’t be accepted by the modifyBase method. Even if it would be SubclassModifier.
So this is again the conterintuitive behavior of the contravariance.
It is true that “modify is able to consume SubclassModifier and its subclasses”, or it subtypes I would say. But the catch is, a subclass of BaseModifier, is not a subtype of BaseModifier, but actually its supertype.
So no, modifyBase would not accept subclass of BaseModifier.
On the contrary, the modify method, would accept (BaseModifier) -> Unit as an argument, which is correct.
And my intention anyway was to define the consumers by anonymous blocks of code (lambdas). So the whole point of this discussion is not how to dispatch consumers of known types to the right methods, but how to infer the unknown type of the consumer from the lambda, in the first place.
No, they are actually only two subclasses. Problem is that there are many uses of the Base class itself. So I really don’t want to leak any generic parameter to the API and force clients of the Base class to deal with the generic parameter, which actually does not have any value in the API.
The Base class serves not only as an extension point for the (two) subclasses, but is part of the API and is used extensively.
So yes, making Base generic solves the problem, but for the aforementioned reasons, this solution is unacceptable.
Well, now this is a nice trick. Yes, this may work, if this alias is really 100% replacement of parameter-less type. But it seems it does the trick at least in this example.