Here’s a strange case that I just ran into. Even ...
# language-proposals
m
Here’s a strange case that I just ran into. Even though the language forbids overriding inline methods, it’s actually possible here. The method signatures differ by the input lambda type, which is erased at runtime, and by the return type.
The strange thing is, it’s impossible to call the overridden method in the subclass when using an implicit lambda. The only way to do it is to extract the lambda into a variable so that its type is explicitly specified.
So, in this example the
f
that gets called is the one in
A
, which is counterintuitive, I would expect the one in
B
.
Admittedly, this is kind of an edge-case, and perhaps it would make more sense for the language to forbid it.
m
I don't think they override each other (there's no
override
modifier). They're just overloads. I'm guessing the
f
in
B
gets name-mangled or something during compilation so it works after type erasure.
👆 1
m
Indeed there’s no
override
, the method binding happens at compile-time, and it’s the compiler which chooses to bind to the method in
A
rather than the one in
B
. It’s just strange that it chooses to do so.
i
@mikhail.zarechenskiy
d
This is because
A.f
is more specific than
B.f
, because they differ only in parameter of accepted lambda, which is a contravariant position. In other words,
A.f
accepts only lambdas which operate on
A
, while
B.f
accepts lambdas which operate both on
A
and
B
. This similar to how we choose
f(Int)
over
f(Any)
in
f(42)
, but lambda makes it much more confusing (and, honestly, I also thought that something is wrong here at first 😅 )
m
Thanks for the explanation! But wouldn’t this mean that the following snippet would print
class A
twice? In practice, it doesn’t, it prints
class A
then
class B
.
Copy code
open class A
class B: A()

fun f(block: (A) -> Unit) = A()
fun f(block: (B) -> Unit) = B()

fun main() {
    val blockA: (A) -> Unit = {}
    val blockB: (B) -> Unit = {}
    println(f(blockA)::class)
    println(f(blockB)::class)
}
d
No, because
f(block: (A) -> Unit)
can not accept
blockB
— again, theoretically speaking, this is because parameters are contravariant. And speaking more practically, body of
blockB
treats parameter as
B
, while
f(block: (A) -> Unit)
may pass here something which is not
B
(e.g., instance of
A
)
m
Ok so in the first example, the idea is therefore that the lambda is interpreted in its most specific form, if I understand correctly. Which, when using a receiver, can be the opposite of what’s desired, because it’s more useful to have the sub-type which has the extended behaviour. Makes sense though. A workaround is to use extension functions, because then then the chosen overload is the one with the most specific first parameter, i.e. the receiver, therefore the sub-type.
Thanks for the explanation 👍