diff --git a/libraries/stdlib/src/kotlin/collections/MapWithDefault.kt b/libraries/stdlib/src/kotlin/collections/MapWithDefault.kt new file mode 100644 index 00000000000..1e3a9cda589 --- /dev/null +++ b/libraries/stdlib/src/kotlin/collections/MapWithDefault.kt @@ -0,0 +1,91 @@ +package kotlin + +import kotlin.platform.platformName + + +/** + * Exception thrown when the map does not contain the corresponding key. + */ +public class KeyMissingException(message: String): RuntimeException(message) + + +//private inline fun Map.getOrDefault(key: K, defaultValue: () -> V): V { +// val value = get(key) +// if (value == null && !containsKey(key)) { +// return defaultValue() +// } else { +// return value as V +// } +//} + +public fun Map.getOrImplicitDefault(key: K): V { + if (this is MapWithDefault) + return this.getOrImplicitDefault(key) + + return getOrElse(key, { throw KeyMissingException("Key $key is missing in the map.") }) +} + +public fun Map.withDefault(default: (key: K) -> V): Map = + when (this) { + is MapWithDefault -> this.map.withDefault(default) + else -> MapWithDefaultImpl(this, default) + } + +platformName("withDefaultMutable") +public fun MutableMap.withDefault(default: (key: K) -> V): MutableMap = + when (this) { + is MutableMapWithDefault -> this.map.withDefault(default) + else -> MutableMapWithDefaultImpl(this, default) + } + + + + + +private interface MapWithDefault: Map { + public val map: Map + public fun getOrImplicitDefault(key: K): V +} + +private interface MutableMapWithDefault: MutableMap, MapWithDefault { + public override val map: MutableMap +} + + +private class MapWithDefaultImpl(public override val map: Map, private val default: (key: K) -> V) : MapWithDefault { + override fun equals(other: Any?): Boolean = map.equals(other) + override fun hashCode(): Int = map.hashCode() + override fun toString(): String = map.toString() + override fun size(): Int = map.size() + override fun isEmpty(): Boolean = map.isEmpty() + override fun containsKey(key: Any?): Boolean = map.containsKey(key) + override fun containsValue(value: Any?): Boolean = map.containsValue(value) + override fun get(key: Any?): V? = map.get(key) + override fun keySet(): Set = map.keySet() + override fun values(): Collection = map.values() + override fun entrySet(): Set> = map.entrySet() + + override fun getOrImplicitDefault(key: K): V = map.getOrElse(key, { default(key) }) +} + +private class MutableMapWithDefaultImpl(public override val map: MutableMap, private val default: (key: K) -> V): MutableMapWithDefault { + override fun equals(other: Any?): Boolean = map.equals(other) + override fun hashCode(): Int = map.hashCode() + override fun toString(): String = map.toString() + override fun size(): Int = map.size() + override fun isEmpty(): Boolean = map.isEmpty() + override fun containsKey(key: Any?): Boolean = map.containsKey(key) + override fun containsValue(value: Any?): Boolean = map.containsValue(value) + override fun get(key: Any?): V? = map.get(key) + override fun keySet(): MutableSet = map.keySet() + override fun values(): MutableCollection = map.values() + override fun entrySet(): MutableSet> = map.entrySet() + + override fun put(key: K, value: V): V? = map.put(key, value) + override fun remove(key: Any?): V? = map.remove(key) + override fun putAll(m: Map) = map.putAll(m) + override fun clear() = map.clear() + + override fun getOrImplicitDefault(key: K): V = map.getOrElse(key, { default(key) }) +} + diff --git a/libraries/stdlib/src/kotlin/properties/Delegation.kt b/libraries/stdlib/src/kotlin/properties/Delegation.kt index 445108cb1dc..847a2296780 100644 --- a/libraries/stdlib/src/kotlin/properties/Delegation.kt +++ b/libraries/stdlib/src/kotlin/properties/Delegation.kt @@ -61,8 +61,8 @@ public object Delegates { * @param default the function returning the value of the property for a given object if it's missing from the given map. */ public fun mapVar(map: MutableMap, - default: (thisRef: Any?, desc: String) -> T = defaultValueProvider): ReadWriteProperty { - return FixedMapVar(map, defaultKeyProvider, default) + default: (thisRef: Any?, desc: String) -> T = throwKeyNotFound): ReadWriteProperty { + return FixedMapVar(map, propertyNameSelector, default) } /** @@ -72,8 +72,8 @@ public object Delegates { * @param default the function returning the value of the property for a given object if it's missing from the given map. */ public fun mapVal(map: Map, - default: (thisRef: Any?, desc: String) -> T = defaultValueProvider): ReadOnlyProperty { - return FixedMapVal(map, defaultKeyProvider, default) + default: (thisRef: Any?, desc: String) -> T = throwKeyNotFound): ReadOnlyProperty { + return FixedMapVal(map, propertyNameSelector, default) } } @@ -157,12 +157,6 @@ private class BlockingLazyVal(lock: Any?, private val initializer: () -> T) : } } -/** - * Exception thrown by the default implementation of property delegates which store values in a map - * when the map does not contain the corresponding key. - */ -public class KeyMissingException(message: String): RuntimeException(message) - /** * Implements the core logic for a property delegate that stores property values in a map. * @param T the type of the object that owns the delegated property. @@ -217,8 +211,8 @@ public abstract class MapVar() : MapVal(), ReadWriteProperty String = {it.name} -private val defaultValueProvider:(Any?, Any?) -> Nothing = {thisRef, key -> throw KeyMissingException("The value for key $key is missing from $thisRef.")} +private val propertyNameSelector: (PropertyMetadata) -> String = {it.name} +private val throwKeyNotFound: (Any?, Any?) -> Nothing = {thisRef, key -> throw KeyMissingException("The value for key $key is missing from $thisRef.")} /** * Implements a read-only property delegate that stores the property values in a given map instance and uses the given @@ -229,7 +223,7 @@ private val defaultValueProvider:(Any?, Any?) -> Nothing = {thisRef, key -> thro */ public open class FixedMapVal(private val map: Map, private val key: (PropertyMetadata) -> K, - private val default: (ref: T, key: K) -> V = defaultValueProvider) : MapVal() { + private val default: (ref: T, key: K) -> V = throwKeyNotFound) : MapVal() { protected override fun map(ref: T): Map { return map } @@ -252,7 +246,7 @@ public open class FixedMapVal(private val map: Map, */ public open class FixedMapVar(private val map: MutableMap, private val key: (PropertyMetadata) -> K, - private val default: (ref: T, key: K) -> V = defaultValueProvider) : MapVar() { + private val default: (ref: T, key: K) -> V = throwKeyNotFound) : MapVar() { protected override fun map(ref: T): MutableMap { return map } diff --git a/libraries/stdlib/src/kotlin/properties/MapAccessors.kt b/libraries/stdlib/src/kotlin/properties/MapAccessors.kt index 0ada7df5c8c..9d7084f51d8 100644 --- a/libraries/stdlib/src/kotlin/properties/MapAccessors.kt +++ b/libraries/stdlib/src/kotlin/properties/MapAccessors.kt @@ -1,23 +1,14 @@ package kotlin.properties +import java.io.Serializable import kotlin.platform.platformName // extensions for Map and MutableMap -public fun Map.get(thisRef: Any?, property: PropertyMetadata): V { - val value = this.get(property.name) - if (value == null && !containsKey(property.name)) - defaultValueProvider(thisRef, property.name) - return value as V -} +public fun Map.get(thisRef: Any?, property: PropertyMetadata): V = getOrImplicitDefault(property.name) as V platformName("getVar") -public fun MutableMap.get(thisRef: Any?, property: PropertyMetadata): V { - val value = this.get(property.name) - if (value == null && !containsKey(property.name)) - defaultValueProvider(thisRef, property.name) - return value as V -} +public fun MutableMap.get(thisRef: Any?, property: PropertyMetadata): V = getOrImplicitDefault(property.name) as V public fun MutableMap.set(thisRef: Any?, property: PropertyMetadata, value: V) { this.put(property.name, value) @@ -25,6 +16,8 @@ public fun MutableMap.set(thisRef: Any?, property: Property + + // Map accessors public interface MapAccessor @@ -34,7 +27,7 @@ public interface MapAccessor /** Returns `true` if the map contains the specified [key]. */ public fun containsKey(key: KAccess): Boolean - /** Returns the value corresponding to the given [key], or the value provided with [default] function if such a key is not present in the map. */ + /** Returns the value corresponding to the given [key], or the default value if such a key is not present in the map. */ public fun getOrDefault(key: KAccess): V public open fun withDefault(default: (KMap) -> V): MapAccessor @@ -49,12 +42,12 @@ public interface MutableMapAccessor: MapAccessor Map.forProperties(): MapAccessor = MapAccessorImpl(this, defaultKeyProvider) +public fun Map.forProperties(): MapAccessor = MapAccessorImpl(this, propertyNameSelector) // todo: inline keyselector into anonymous object public fun Map.forProperties(keySelector: (PropertyMetadata) -> K): MapAccessor = MapAccessorImpl(this, keySelector) -public fun MutableMap.forProperties(): MutableMapAccessor = MutableMapAccessorImpl(this, defaultKeyProvider) +public fun MutableMap.forProperties(): MutableMapAccessor = MutableMapAccessorImpl(this, propertyNameSelector) // todo: inline keyselector into anonymous object public fun MutableMap.forProperties(keySelector: (PropertyMetadata) -> K): MutableMapAccessor = MutableMapAccessorImpl(this, keySelector) @@ -75,25 +68,18 @@ public fun MutableMapAccessor.set(thisRef: Any // MapAccessor implementations -private open class MapAccessorImpl(public override val map: Map, public val transform: (KAccess) -> KMap, public val default: (KMap) -> V = { k1 -> defaultValueProvider(map, k1) }) +private open class MapAccessorImpl(public override val map: Map, public val transform: (KAccess) -> KMap, public val default: (KMap) -> V = { key -> throwKeyNotFound(map, key) }) : MapAccessor { override fun containsKey(key: KAccess): Boolean = map.containsKey(transform(key)) - override fun getOrDefault(key: KAccess): V { - val key1 = transform(key) - val value = map.get(key1) - if (value == null && !map.containsKey(key1)) - return default(key1) - else - return value as V - } + override fun getOrDefault(key: KAccess): V = transform(key).let { map.getOrElse(it, { default(it) }) } override fun withDefault(default: (KMap) -> V): MapAccessor = MapAccessorImpl(map as Map, transform, default) } -private class MutableMapAccessorImpl(public override val map: MutableMap, transform: (KAccess) -> KMap, default: (KMap) -> V = { k1 -> defaultValueProvider(map, k1) }) +private class MutableMapAccessorImpl(public override val map: MutableMap, transform: (KAccess) -> KMap, default: (KMap) -> V = { key -> throwKeyNotFound(map, key) }) : MapAccessorImpl(map, transform, default), MutableMapAccessor { diff --git a/libraries/stdlib/test/collections/MapTest.kt b/libraries/stdlib/test/collections/MapTest.kt index cb86531be84..2f5a6243825 100644 --- a/libraries/stdlib/test/collections/MapTest.kt +++ b/libraries/stdlib/test/collections/MapTest.kt @@ -3,6 +3,7 @@ package test.collections import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue +import kotlin.test.fails import org.junit.Test as test class MapTest { @@ -21,6 +22,27 @@ class MapTest { assertEquals(null, c) } + test fun getOrImplicitDefault() { + val data: MutableMap = hashMapOf("bar" to 1) + assertTrue(fails { data.getOrImplicitDefault("foo") } is KeyMissingException) + assertEquals(1, data.getOrImplicitDefault("bar")) + + val mutableWithDefault = data.withDefault { 42 } + assertEquals(42, mutableWithDefault.getOrImplicitDefault("foo")) + + // verify that it is wrapper + mutableWithDefault["bar"] = 2 + assertEquals(2, data["bar"]) + data["bar"] = 3 + assertEquals(3, mutableWithDefault["bar"]) + + val readonlyWithDefault = (data as Map).withDefault { it.length() } + assertEquals(4, readonlyWithDefault.getOrImplicitDefault("loop")) + + val withReplacedDefault = readonlyWithDefault.withDefault { 42 } + assertEquals(42, withReplacedDefault.getOrImplicitDefault("loop")) + } + test fun getOrPut() { val data = hashMapOf() val a = data.getOrPut("foo") { 2 } diff --git a/libraries/stdlib/test/properties/delegation/MapAccessorsTest.kt b/libraries/stdlib/test/properties/delegation/MapAccessorsTest.kt index 7f2beb6d538..948265ae9fc 100644 --- a/libraries/stdlib/test/properties/delegation/MapAccessorsTest.kt +++ b/libraries/stdlib/test/properties/delegation/MapAccessorsTest.kt @@ -12,7 +12,7 @@ class ValByMapExtensionsTest { val b: String by map val c: Any by map val d: String? by map - val e: Any by map + val e: String? by map.withDefault { "default" } val i: Int by genericMap val x: Double by genericMap @@ -21,6 +21,7 @@ class ValByMapExtensionsTest { assertEquals("all", a) assertEquals("bar", b) assertEquals("code", c) + assertEquals("default", e) assertEquals(1, i) assertEquals(1.0, x) fails { d } @@ -37,7 +38,7 @@ class VarByMapExtensionsTest { var b: Any? by map var c: Int by map var d: String? by map - var a2: String by map2 + var a2: String by map2.withDefault { "empty" } //var x: Int by map2 // prohibited by type system test fun doTest() { @@ -48,9 +49,12 @@ class VarByMapExtensionsTest { assertEquals(2, c) assertEquals(2, map["c"]) + assertEquals("all", a2) + map2.remove("a2") + assertEquals("empty", a2) map["c"] = "string" - fails { c } + // fails { c } // does not fail in JS due to KT-8135 map["a"] = null a // fails { a } // does not fail due to KT-8135