7

I just had a bug in my program, but i don't understand how the program was compiled at all!

I have the following variable:

gamesPerCountriesMap: MutableMap<Long, MutableMap<Long, MutableList<AllScoresGameObj>>>?

and i had the following line of code:

var gamesList = gamesPerCountriesMap?.get(countryItem.id)?.get(competitionItem)

the correct line should be:

var gamesList = gamesPerCountriesMap?.get(countryItem.id)?.get(competitionItem.id)

i have looked at the prototype of the Map class and the method is declared as following:

public inline operator fun <@kotlin.internal.OnlyInputTypes K, V> Map<out K, V>.get(key: K): V?

As we can see it can get K and it's subtype, but competitionItem which is an instacne of class CompetitionObj isn't inherit the Long class. So why the compiler didn't prevent this error? I solved the issue but i am very couries of what is didn't prevent the code from being compiled?

Gerold Broser
  • 14,080
  • 5
  • 48
  • 107
Eitanos30
  • 1,331
  • 11
  • 19
  • 3
    This is very strange indeed. I played around with it and found that if you write an extension function for a generic typed class that's invariant at the declaration site, but covariant at the function site, the compiler fails to properly enforce the type. It's the same thing going on with `Map.get`, because that's defined with `out K` at the function site. Maybe this is actually a compiler bug? https://pl.kotl.in/jmlBh5NjT – Tenfour04 Nov 20 '20 at 20:37
  • @Tenfour04, I read you code. I will never able to think of reproducing the error. Thank you. – Eitanos30 Nov 20 '20 at 21:07
  • @Tenfour04, is there any reason/advantage in declaring *out* in the extension function site? Does is do any difference if *out* is written or not in this situation? I don't need explanation on *out* here cause I'm pretty Knowledgeable in variant subject – Eitanos30 Nov 21 '20 at 11:21
  • I can't figure that out. It seems to me like it should be no different than invariance because `K` is a parameter, not a return value. And the result is that it becomes "anything goes". The compiler even lets you pass an arbitrary anonymous object. `mapOf().get(object{})` – Tenfour04 Nov 23 '20 at 20:34
  • @Tenfour04, if there was a return type, does it matter? it's ok to return subtype of K. not? – Eitanos30 Nov 23 '20 at 21:01
  • Not sure I follow exactly. I mean that internally the function doesn't call a function of the Map that outputs a key. Note that there is already a `get` function for map that takes an invariant key. And if you pass a subtype of the key type, it's still this original function that gets called because the compiler infers the subtype to be a match for the key type. So this extension function `get` seems to exist only to make `get` more lenient, but I don't see why that's needed, and it's doing it in an unexpected way. – Tenfour04 Nov 23 '20 at 21:19
  • @Tenfour04 i understand your explanation and i'm appologize that my quesion wan't clear enough? i tried to ask: If hypothetically the get will return a K (Key) does it Justifies the existence of the *out*? – Eitanos30 Nov 23 '20 at 21:24
  • I guess it would depend on what you're doing. The type of K of the function is invariant. It's the receiver Map that has a contravariant K. So it is only making it more lenient what type of Map can be used with the input parameter K. – Tenfour04 Nov 23 '20 at 21:31

2 Answers2

3

There are two get methods for Map interface.

One is defined directly in the body of the interface:

Map<K, V> { fun get(key: K): V? }

Another (which you cite in your question) - as an extention function:

fun <K, V> Map<out K, V>.get(key: K): V?

The overload resolution on call depends on whether or not you explicitly specify generic parameters, and on relationship between type of map K generic parameter & type of passed key argument:

  1. If you specify explicit generic parameter, the second version will be called (and it wouldn't have compiled in your case, if you've wrote .get()<Long, MutableList<AllScoresGameObj>.(competitionItem), although .get()<CompetitionObj, MutableList<AllScoresGameObj>.(competitionItem)) would've worked, cause there is unsafe cast inside this get overload).
  2. If you omit explicit generic parameter, passing as a key:
    1. an instance of K type (or its subtype) - the first overload will be called
    2. anything else - the second overload (cause the first overload will not compile). In this case Kotlin will try to infer omitted generic parameters, so that original map could be represented as a Map<out inferredK, V> and inferredK type parameter was a supertype of passed key argument. Eventually, it will come up with inferredK = Any. Indeed Any is a supertype of everything, and it's perfectly legal to do val x: Map<out Any, V> = mapOf<K, V>() for any K. Actually compiler realizes that this is a trivial solution and issues a compilation warning Type inference failed. The value of the type parameter K should be mentioned in input types (argument types, receiver type or expected type). Try to specify it explicitly.(I believe in your case this warning should've been too). Why this still works in runtime? Because of type erasure.

So, why this overloaded version was added to stdlib? Don't know for sure, maybe for some legal cases like:

val k : Base = Derived()
val v = mapOf<Derived, String>().get(k) // will call overloaded variant; will infer K as Base

without this overload you'd have to manually cast back:

val v = mapOf<Derived, String>().get(k as Derived) 
  • You're right with your point 2.2. I can't believe I missed that. You can cast a `Map` to a `Map` without issue. It makes the cast version impossible to `put` anything into. So it's not an error that the compiler allows you to pass any type of object as a Key for the second overload. – Tenfour04 Nov 24 '20 at 14:38
  • The remaining piece of the puzzle is why this overload should exist at all. Your example of a possible reason is a deliberate circumvention of generics. If `k` is only known to be a Base, it should be forbidden to use it to `get` unless you explicitly cast it to Derived. Otherwise, the Map interface shouldn't use a key type at all, because it's not enforcing anything about the key type. It's like Java 4, where there were no generics, so you always had to cast object that you pulled out of collections. – Tenfour04 Nov 24 '20 at 14:39
  • > If k is only known to be a `Base`, it should be forbidden to use it to get unless you explicitly cast it to `Derived` Why not? In the worst case (if you've passed as `key` something unappropriate and missed compiler warning) you'll just get `null`. And since return type of `get` is `V?`, this case should be handled anyway. This minor relaxation of type strictness is not the same as defining `Map` interface without `K` parameter at all. There are other methods in this interface, which preserve type strictness (like `put`, for instance) – Михаил Нафталь Nov 24 '20 at 20:32
  • It should be forbidden because there is no reason you should expect a map to ever return something for an invalid key, so you should never be calling it with an invalid key. It shows a warning, but it might as well be an error. Why create an overload of `get()` that serves no purpose other than to allow you to mistakenly pass an invalid key? – Tenfour04 Nov 24 '20 at 20:44
  • Unfortunately, I don't see a way of converting this particular warning into an error, but there is an option of [converting all warnings into errors](https://stackoverflow.com/a/46244803/13968673) – Михаил Нафталь Nov 28 '20 at 18:27
  • 1
    The question is why the function exists in the first place, when it seems like it shouldn't. There must be a use case the stdlib designers have in mind. – Tenfour04 Nov 28 '20 at 23:22
  • 1
    I'm no closer to understanding why the method exists, but thanks for your effort. At least I now see that the compiler treats this method as expected. – Tenfour04 Nov 30 '20 at 00:05
  • @Tenfour04 I don't know if this is a clue to your question, but I noticed that `mapOf("s" to "s2", 0 to 5)` returns a `Map`. In other words, the key type parameter is `out` on the Map type itself, not just on the `get()` function. – LarsH Jan 10 '23 at 22:45
  • Looking at this again now, I don't understand why I was confused. If you make the key covariant, it prevents you from adding to the map, but there's no danger in attempting to get items out of the map with the wrong key type. If there were, the reference would be entirely useless. – Tenfour04 Jan 11 '23 at 14:14
0

Why two get methods?

  1. Kotlin wants to inherit Java type system.
val x: kotlin.collections.List<String>= java.util.ArrayList<>()
  1. Kotlin type system is different. One of the differences is declaration-site variance

Having

open class A
class B

It is illegal in Java

List<A> x = new ArrayList<B>();

But absolutely normal in Kotlin

val x: List<A> = ArrayList<B>()

As of the Map example I would expect

Error: Type inference failed. The value of the type parameter K should be mentioned in input types (argument types, receiver type or expected type). Try to specify it explicitly

whenever type inference is broken since Kotlin 1.0.0

If it's really a problem, it would be great to see a code paste and kotlin compiler version you are using.

Sergei Rybalkin
  • 3,337
  • 1
  • 14
  • 27