user2297550
user2297550

Reputation: 3356

Kotlin Problem Delegating to Map with DefaultValue - Language Bug?

In the following code, where MyMap trivially implements Map by delegation to impl:

foo@host:/tmp$ cat Foo.kt
class MyMap <K, V> (val impl : Map <K, V>) : Map<K, V> by impl {
  fun myGetValue (k: K) = impl.getValue(k)
}

fun main() {
  val my_map = MyMap(mapOf('a' to 1, 'b' to 2).withDefault { 42 })
  println(my_map.myGetValue('c'))  // OK
  println(my_map.getValue('c'))    // ERROR
}

Why do I get the following error on the second println?

foo@host:/tmp$ /path/to/kotlinc Foo.kt
foo@host:/tmp$ /path/to/kotlin FooKt
42
Exception in thread "main" java.util.NoSuchElementException: Key c is missing in the map.
        at kotlin.collections.MapsKt__MapWithDefaultKt.getOrImplicitDefaultNullable(MapWithDefault.kt:24)
        at kotlin.collections.MapsKt__MapsKt.getValue(Maps.kt:344)
        at FooKt.main(Foo.kt:8)
        at FooKt.main(Foo.kt)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.jetbrains.kotlin.runner.AbstractRunner.run(runners.kt:64)
        at org.jetbrains.kotlin.runner.Main.run(Main.kt:176)
        at org.jetbrains.kotlin.runner.Main.main(Main.kt:186)
foo@bigdev:/tmp$

Update: The compiler and runtime version outputs are:

foo@host:/tmp$ kotlinc -version
info: kotlinc-jvm 1.6.10 (JRE 17.0.1+12-LTS)
foo@host:/tmp$ kotlin -version
Kotlin version 1.6.10-release-923 (JRE 17.0.1+12-LTS)
foo@host:/tmp$ javac -version
javac 17.0.1
foo@host:/tmp$ java -version
openjdk version "17.0.1" 2021-10-19 LTS
OpenJDK Runtime Environment Corretto-17.0.1.12.1 (build 17.0.1+12-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.1.12.1 (build 17.0.1+12-LTS, mixed mode, sharing)

Upvotes: 3

Views: 911

Answers (2)

Tenfour04
Tenfour04

Reputation: 93902

This is occurring because of the slightly unexpected way in which withDefault is implemented. The wrapper that withDefault produces doesn't override getValue() as this is impossible because getValue() is an extension function. So unfortunately, what we have instead is a classic OOP anti-pattern: getValue() does an is check to see if it's being called on the internal MapWithDefault interface, and only uses the default value if that is the case. I don't see any way they could have avoided this situation without breaking the Map contract.

myGetValue calls getValue on the underlying delegate, which is a MapWithDefault, so it works fine.

getValue called on your MyMap instance will fail the internal is MapWithDefault check because MyMap is not a MapWithDefault, even though its delegate is. The delegates other types are not propagated up to the class that delegates to it, which makes sense. Like if we delegated to a MutableMap, we might want the class to be considered only a read-only Map.

Upvotes: 2

Martin Marconcini
Martin Marconcini

Reputation: 27246

Though I would have expected your code to work to be honest, this could be bug but we'd have to look at the produced bytecode.

In the documentation it says (emphasis mine):

This implicit default value is used when the original map doesn't contain a value for the key specified and a value is obtained with Map.getValue function, for example when properties are delegated to the map.

The conflict of "contracts" comes from the actual Map interface, which says:

Returns the value corresponding to the given [key], or null if such a key is not present in the map.

The maps default contract must fulfill this, so it can "only" return null when a key is non-existent.

I have found one discussion about this in the Kotlin forums.

Upvotes: 2

Related Questions