Why can't Kotlin infer the type for this HashMap with a nullable value type?

Viewed 911

When I run this code, I get a Type mismatch: inferred type is kotlin.collections.HashMap<String, Int> /* = java.util.HashMap<String, Int> */ but kotlin.collections.HashMap<String, Any?> /* = java.util.HashMap<String, Any?> */ was expected error

data class Record(
        var blah: HashMap<String, Any?>
)
    
fun test2() {
        val data2 = hashMapOf("key" to 10000)
        val a = Record(data2)
        println(a.blah)
}

Run the above on Kotlin Playgrounds: https://pl.kotl.in/vL2n_Qrwo

I only get this error when the type in the data class is a HashMap, I don't get it when it's just a Map (https://pl.kotl.in/f1V3Eeyj-). Why is this? It's fixed by explicitly specifying Any? (Any with a question mark, to specify that it's nullable) in the hashMapOf type so hashMapOf<String, Any?>("key" to 10000)

The weirdest thing is if I don't create a new variable data2 to hold the hashmap, it doesn't return any error!

data class Record(
        var blah: HashMap<String, Any?>
)
    
fun test1() {
    val a = Record(hashMapOf("key" to 10000))
    println(a.blah)
}

Run this one on Kotlin Playgrounds: https://pl.kotl.in/K12q1Bd7B

As you can see, no errors. It makes no sense to me.

2 Answers

The inference engine only analyses one statement at a time.

The question is about the inference of the two type arguments to fun <K, V> hashMapOf(): HashMap<K, V>, i.e. the types of K and V.

In the first example, val data2 = hashMapOf("key" to 10000), there is no requirement given for the result of hashMapOf(), so the engine uses the parameter to determine the two types. "key" to 10000 is a pair of String to Int, so the result is that K is String and V is Int.

In the second example, val a = Record(hashMapOf("key" to 10000)), the Record constructor requires that the type of the object returned by hashMapOf() must be an object that is assignment compatible with HashMap<String, Any?>. The inference engine therefore uses K as String and V as Any, and compilation accepts that, since "key" is compatible with String and 10000 is compatible with Any?.

To answer the first part of your question, a HashMap (or Kotlin MutableMap) is defined with generic parameters K and V, whilst a read-only Map is defined with generic parameters K and out V.

The V for a Map can have the out modifier because a Map is read-only, and therefore the value is only present in the "out" parts of its function signatures -- in other words, values of type V are only returned from the Map, never provided to the Map. See Declaration-site Variance.

As noted in the answer by @Andreas, the inferred type of hashMapOf("key" to 10000) is HashMap<String, Int>.

When you define:

var blah: HashMap<String, Any?>

the compiler says: no, I can't take a HashMap<String, Int> and assign that to a variable of type HashMap<String, Any?> because someone could validly put something other than an Int in here later, which would clearly be incorrect.

You can tell the compiler "no, its ok if something other than an Int goes in here" by changing your assignment expression to explicitly specify the type, and then all is well:

val data2 = hashMapOf<String, Any?>("key" to 10000)

Correspondingly, when you define blah as a read-only Map rather than a mutable HashMap (or Kotlin MutableMap):

var blah: Map<String, Any?>

the compiler is OK with assigning a HashMap<String, Int> to it, because now Any? has out variance i.e. blah can only return Any?'s, not accept them.

Related