2
  • Java : 1.8.0_102
  • Kotlin: 1.0.4

I'm trying to create a map where you can do something like map["key"] += 5 similar to javascript.

Kotlin already has withDefault that solves one part of this, but map's get function still returns a nullable value, so i proceeded to make my own implementation of this inspired by withDefault

interface NonNullableMutableMap<K,V> : MutableMap<K,V> {
  override fun put(key: K, value: V): V
  override fun get(key: K): V
}

fun <K,V> MutableMap<K,V>.withoutNullValues(default: () -> V): NonNullableMutableMap<K, V> {
  return NonNullableMapWrapper(this, default)
}

class NonNullableMapWrapper<K,V>(val map: MutableMap<K,V>, val default: () -> V) : NonNullableMutableMap<K,V> {
  override fun put(key: K, value: V): V = map.put(key, value) ?: default()

  override fun get(key: K): V {
      val value = map.getOrPut(key, default)
      return value
  }
  override val size: Int get() = map.size

  override fun containsKey(key: K): Boolean = map.containsKey(key)

  override fun containsValue(value: V): Boolean = map.containsValue(value)

  override fun isEmpty(): Boolean = map.isEmpty()

  override val entries: MutableSet<MutableMap.MutableEntry<K, V>> get() = map.entries
  override val keys: MutableSet<K> get() = map.keys
  override val values: MutableCollection<V> get() = map.values

  override fun clear() {
      map.clear()
  }

  override fun putAll(from: Map<out K, V>) {
    map.putAll(from)
  }

  override fun remove(key: K): V {
    return map.remove(key) ?: default()
  }
}

I created the following unit test to test it

class NonNullableMapTest {

  @Test
  fun notNullableTest() {
      val map = HashMap<String, Long>().withoutNullValues { 0 }
      map["first"] += 10L
      map["second"] -= 10L
      assertThat(map["first"]).isEqualTo(10L)
      assertThat(map["second"]).isEqualTo(-10L)
      assertThat(map["third"]).isEqualTo(0L)
  }
}

But i'm getting the following error when i run the test:

tried to access method kotlin.collections.MapsKt__MapsJVMKt.set(Ljava/util/Map;Ljava/lang/Object;Ljava/lang/Object;)V from class foo.bar.NonNullableMapTest
java.lang.IllegalAccessError: tried to access method kotlin.collections.MapsKt__MapsJVMKt.set(Ljava/util/Map;Ljava/lang/Object;Ljava/lang/Object;)V from class foo.bar.NonNullableMapTest

Any idea how to resolve this issue?

martin treurnicht
  • 1,223
  • 1
  • 10
  • 17
  • Currently `withDefault` is intended to be used only in the property delegation scenario, it doesn't affect the return value of `get` function. See [KT-11851](https://youtrack.jetbrains.com/issue/KT-11851) – Ilya Oct 12 '16 at 21:45

2 Answers2

2

This looks like a bug to me. I recommend reporting it at Kotlin (KT) | YouTrack.

One way to workaround it is by explicitly defining set on your NonNullableMutableMap interface. e.g.:

interface NonNullableMutableMap<K, V> : MutableMap<K, V> {
    override fun put(key: K, value: V): V
    override fun get(key: K): V
    operator fun set(key: K, value: V) {
        put(key, value)
    }
}
mfulton26
  • 29,956
  • 6
  • 64
  • 88
  • Thanks! this solves the issue i was having, i'll report it to the Kotlin Team – martin treurnicht Oct 12 '16 at 22:38
  • I've simplified this to the basics, here is the github gist for anyone interested in using it https://gist.github.com/martintreurnicht/2906b2e1fbfc04d488fbf06219b0746e – martin treurnicht Oct 12 '16 at 22:48
  • 2
    Good solution in the gist. I would only argue against `getOrPut` because it has an unexpected side-effect: It writes back to the map just, thus making `get` non-idempotent. To put it differently, if you call `containsKey` before and after `getOrPut` you'll get different results. Same goes for `size()` and a big set of other methods. And this is expected. But it's not expected with `get` which I think should act idempotently. In my implementation I use `getOrDefault`. – Kostas Filios Aug 15 '17 at 19:53
2

Regarding to the runtime error you get, there is currently a bug in how += operator gets compiled for inline MutableMap.set extension function: https://youtrack.jetbrains.com/issue/KT-14227

The workaround is not to use +=:

map["first"] = map["first"] + 10L
Ilya
  • 21,871
  • 8
  • 73
  • 92