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.