Type inference and higher order functions


#1

Apologies if this is mis-categorised.

Please consider the following trivial code snippet

import kotlinx.coroutines.experimental.Deferred
import java.time.Instant 
import java.util.concurrent.ConcurrentHashMap

data class TypedContainer<T>(
    var item: Deferred<T>? = null,
    var importantTime: Instant? = null
)

class TypeInference {
    companion object {
        @JvmStatic fun main(args: Array<String>) {
            val registry: MutableMap<String, TypedContainer<String>> = ConcurrentHashMap()

            // compiles
            registry.computeIfAbsent("testKey", { TypedContainer() })

            // does NOT compile
            registry.computeIfAbsent("testKey2", { TypedContainer().also { it.importantTime = Instant.now() } })

            // compiles
            registry.computeIfAbsent("testKey3", { TypedContainer<String>().also { it.importantTime = Instant.now() } })

        }
    }
}

Is it reasonable to expect that type inference could be successful even with the use of “also { }”? I’m assuming there is an issue here with the way in which defaulted constructor parameters are handled. Any insight would be appreciated.


#2

The reason why it works for testKey is that the result of TypeContainer() is (indirectly) used for the argument of computeIfAbsent(...). So the compiler can easily infer that in this case TypeContainer<String> is expected.

It does not work for testKey2 because the instance is used to invoke also(...). At that point the compiler needs to know what T must be, but it has no way of knowing unless it evaluates more of the code. In this case the extra code is small, but in more complex cases the amount of code to evaluate can grow quickly. As code evaluation is expensive, the compiler only infers types at the actual spot where it is needed.


#3

Thank you for you answer…your explanation is clear and enlightening; but what continues to “challenge me” is determining at what point the kotlin compiler forces itself to resolve type versus combine forms to evaluate on some additional pass. One additional form that works is this:

        // does not compile
        registry.computeIfAbsent("testKey4", { TypedContainer( importantTime = Instant.now() ) })

I had assumed this was the functional equivalent to the use of also (maybe even a compilation pre-step), yet this, also, will compile. So, I am still intrigued but what level of “context” the compiler is able to leverage.


#4

I am not an expert on how type inference works, but the basic rule is: At the point of use the compiler must know all types in the expression being used.

Inferring the types of expressions can easily be done by the compiler, simply because the compiler has knowledge about the exact type transformations of the operators. But for functions (which may or may not have lambda parameters), the compiler has to analyze the whole function. This can become costly, so the compiler stops inferring at functions.

Cases testKey2 and testKey4 are very different from the perspective of the compiler. testKey2 has a function call after construction of TypedContainer, and testKey4 does not. testKey4 is similar to testKey.