Generic type inference

Hello fellow coders!

I used Kotlin for at least 4 years with production applications involving fullstack Kotlin-jvm/js.

I developed a small rpc (remote procedure call) library using kotlinx serialization.
There is a thing with it that I was never able to understand: the following does compile and I would actually expect an error! See the full code for this poc (the full library is in the same repo).

addHandler { request: SumRequest -> Any() } // it does compile, why!?

This does not compile:

// it doesn't compile: good! As expected! 
addHandler<SumRequest, SumResponse> { request: SumRequest -> Any() } 

These are the definitions required to make the complete picture for the code above:

inline fun <reified Req : Request<Res>, reified Res : Any> 
  addHandler(noinline handler: (Req) -> Res): Unit = TODO()

interface Request<in Res : Any>
@Serializable data class SumRequest(val a: Int, val b: Int) : Request<SumResponse>
@Serializable data class SumResponse(val sum: Int)

The following are some different type of invocation (they both compile)

// as I usually use it
addHandler { request: SumRequest -> SumResponse(request.a + request.b) }

// ok, no news here
addHandler<SumRequest, SumResponse> { request: SumRequest -> SumResponse(request.a + request.b) }

If someone can shed some light on this I would be very grateful!

2 Likes

I’m wondering whether the addHandler call which you expect not to compile causes any heap pollution. To answer this, I was wondering what addHandler could do with the passed function (what you’re hiding from us via the TODO()). I might miss something here, but it it seems it cannot do anything with it, because it cannot create instances of Req.

If my suspicion is correct, you might think of it as cleverness by the compiler. It knows that it doesn’t matter what you put into it because it can’t be used, so it doesn’t complain. In reality, I guess, it falls naturally from the chosen implementation. Something like iterating over a list which simply does nothing when it is empty, instead of checking for emptyness and then throwing some error.

Hi thumannw, thank you for your reply.

Sorry if I was not clear: the library is used in production and it is doing actually something.
Notice that I’m using a reified type, so it is possible to get the serializer and we can create an instance of Req.

You can find a copy of the code implementation here; I’m linking the file for the handler registration.

By the way, the ‘missing’ compiler error happens both with the mock code above and the real production code.

1 Like

This does seem to be a bug I believe. If you try to show the inferred type parameters in IDEA it expands to <Any, Any>. Furthermore, if you make the trivial implementation:

nline fun <reified Req : Request<Res>, reified Res : Any> 
  addHandler(noinline handler: (Req) -> Res, req: Req): Res = handler(req)

This should be equivalent in how the types are viewed (because functions have a sort of implied in and out parameters, which is out of scope for this issue)
Then attempting to call it with a SumRequest fails by stating that it expects a Request<Any>

Thank you kyay10 for the feedback and the useful trivial implementation.
I agree with you: it looks like a bug.

I played with your trivial implementation and below I wrote a new minimal reproducible example.
Luckily I seldomly botch the response type :slight_smile:

import kotlinx.serialization.Serializable

fun main() {
    addHandler(AddRequest(10, 32)) { request: AddRequest ->
        AddResponse(request.a + request.b)
    }
    addHandler(AddRequest(10, 32)) { request: AddRequest ->
        Any() // <--- compiler error, good! Required ApiResponse found Any
    }
    addHandler2 { request: AddRequest ->
        AddResponse(request.a + request.b)
    }
    addHandler2 { request: AddRequest ->
        Any() // <--- no compiler error! one should be expected
    }
}

interface Request<Res : Any>

inline fun <reified Req : Request<Res>, reified Res : Any>
        addHandler(req: Req, noinline handler: (Req) -> Res): Res = TODO()

inline fun <reified Req : Request<Res>, reified Res : Any>
        addHandler2(noinline handler: (Req) -> Res): Res = TODO()

@Serializable class AddRequest(val a: Int, val b: Int) : Request<AddResponse>
@Serializable class AddResponse(val result: Int)
1 Like

I agree that it is a bug. The compiler (1.6.20) inlines <Request<SumResponse>, Any> here which should also not be allowed. The closest type parameters which make sense are <Request<Any>, Any> but then the passed lambda cannot accept SumRequest.

// compiles, ok
addHandler<Request<Any>, Any> { request: Request<SumResponse> ->
    Any()
}

Seems like the variance is mistakenly turned around during type inference here.

2 Likes

Hi thumannw, thank you for your time.

Anybody knows if I can report this kind of issue somewhere?

1 Like

https://youtrack.jetbrains.com/issues/KT

2 Likes

I did open an issue on the tracker

By the way, latest IDEA gives warning:
“Type argument for a type parameter Req can’t be inferred because it has incompatible upper bounds: AddRequest, Request. This will become an error in Kotlin 1.9”