diff --git a/README.md b/README.md index e248c12416..9c39df8340 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,7 @@ For more information, see the [CONTRIBUTING](CONTRIBUTING.md) file. - [x] `tail` - [x] `take` / `takeRight` / `takeWhile` - [x] `updated` +- [x] `withDefault` / `withDefaultValue` ### Transformations to collections that can have a different element type diff --git a/collections/src/main/scala/strawman/collection/immutable/Map.scala b/collections/src/main/scala/strawman/collection/immutable/Map.scala index b056dfbdb6..0ab61c3893 100644 --- a/collections/src/main/scala/strawman/collection/immutable/Map.scala +++ b/collections/src/main/scala/strawman/collection/immutable/Map.scala @@ -10,7 +10,30 @@ import scala.{Any, Boolean, `inline`, Int, None, NoSuchElementException, Nothing trait Map[K, +V] extends Iterable[(K, V)] with collection.Map[K, V] - with MapOps[K, V, Map, Map[K, V]] + with MapOps[K, V, Map, Map[K, V]] { + + /** The same map with a given default function. + * Note: The default is only used for `apply`. Other methods like `get`, `contains`, `iterator`, `keys`, etc. + * are not affected by `withDefault`. + * + * Invoking transformer methods (e.g. `map`) will not preserve the default value. + * + * @param d the function mapping keys to values, used for non-present keys + * @return a wrapper of the map with a default value + */ + def withDefault[V1 >: V](d: K => V1): Map[K, V1] = new Map.WithDefault[K, V1](this, d) + + /** The same map with a given default value. + * Note: The default is only used for `apply`. Other methods like `get`, `contains`, `iterator`, `keys`, etc. + * are not affected by `withDefaultValue`. + * + * Invoking transformer methods (e.g. `map`) will not preserve the default value. + * + * @param d default value used for non-present keys + * @return a wrapper of the map with a default value + */ + def withDefaultValue[V1 >: V](d: V1): Map[K, V1] = new Map.WithDefault[K, V1](this, x => d) +} /** Base trait of immutable Maps implementations * @@ -93,6 +116,29 @@ trait MapOps[K, +V, +CC[X, +Y] <: MapOps[X, Y, CC, _], +C <: MapOps[K, V, CC, C] */ object Map extends MapFactory[Map] { + final class WithDefault[K, +V](underlying: Map[K, V], d: K => V) extends Map[K, V] { + // These factory methods will lose the default value + def iterableFactory = underlying.iterableFactory + def mapFactory: MapFactory[Map] = underlying.mapFactory + protected[this] def mapFromIterable[K2, V2](it: collection.Iterable[(K2, V2)]): Map[K2,V2] = mapFactory.from(it) + + // Specific building will keep the default but may lose the precise underlying type because our own V can be + // a supertype of the underlying collection's V so we cannot rebuild with potentially new values that are not + // valid for the underlying collection. + protected[this] def fromSpecificIterable(coll: collection.Iterable[(K, V)]): Map[K,V] = new WithDefault[K, V](mapFactory.from(coll), d) + protected[this] def newSpecificBuilder(): mutable.Builder[(K, V), Map[K,V]] = + mapFactory.newBuilder[K, V]().mapResult(new WithDefault[K, V](_, d)) + override def size = underlying.size + def get(key: K) = underlying.get(key) + def iterator() = underlying.iterator() + override def default(key: K): V = d(key) + override def empty = new WithDefault(underlying.empty, d) + override def updated[V1 >: V](key: K, value: V1): WithDefault[K, V1] = new WithDefault[K, V1](underlying.updated[V1](key, value), d) + override def remove (key: K): WithDefault[K, V] = new WithDefault(underlying - key, d) + override def withDefault[V1 >: V](d: K => V1): immutable.Map[K, V1] = new WithDefault[K, V1](underlying, d) + override def withDefaultValue[V1 >: V](d: V1): immutable.Map[K, V1] = new WithDefault[K, V1](underlying, x => d) + } + def empty[K, V]: Map[K, V] = EmptyMap.asInstanceOf[Map[K, V]] def from[K, V](it: collection.IterableOnce[(K, V)]): Map[K, V] = @@ -225,5 +271,4 @@ object Map extends MapFactory[Map] { f((key1, value1)); f((key2, value2)); f((key3, value3)); f((key4, value4)) } } - } diff --git a/collections/src/main/scala/strawman/collection/mutable/Map.scala b/collections/src/main/scala/strawman/collection/mutable/Map.scala index 167ec57fcc..51853cd3fb 100644 --- a/collections/src/main/scala/strawman/collection/mutable/Map.scala +++ b/collections/src/main/scala/strawman/collection/mutable/Map.scala @@ -22,6 +22,28 @@ trait Map[K, V] old } */ + + /** The same map with a given default function. + * Note: The default is only used for `apply`. Other methods like `get`, `contains`, `iterator`, `keys`, etc. + * are not affected by `withDefaultValue`. + * + * Invoking transformer methods (e.g. `map`) will not preserve the default value. + * + * @param d the function mapping keys to values, used for non-present keys + * @return a wrapper of the map with a default value + */ + def withDefault(d: K => V): Map[K, V] = new Map.WithDefault[K, V](this, d) + + /** The same map with a given default value. + * Note: The default is only used for `apply`. Other methods like `get`, `contains`, `iterator`, `keys`, etc. + * are not affected by `withDefaultValue`. + * + * Invoking transformer methods (e.g. `map`) will not preserve the default value. + * + * @param d default value used for non-present keys + * @return a wrapper of the map with a default value + */ + def withDefaultValue(d: V): Map[K, V] = new Map.WithDefault[K, V](this, x => d) } /** @@ -131,7 +153,6 @@ trait MapOps[K, V, +CC[X, Y] <: MapOps[X, Y, CC, _], +C <: MapOps[K, V, CC, C]] coll -= elem this } - } /** @@ -139,7 +160,36 @@ trait MapOps[K, V, +CC[X, Y] <: MapOps[X, Y, CC, _], +C <: MapOps[K, V, CC, C]] * @define coll mutable map * @define Coll `mutable.Map` */ -object Map extends MapFactory.Delegate[Map](HashMap) +object Map extends MapFactory.Delegate[Map](HashMap) { + final class WithDefault[K, V](underlying: Map[K, V], d: K => V) extends Map[K, V] { + // These factory methods will lose the default value + override def iterableFactory = underlying.iterableFactory + def mapFactory: MapFactory[Map] = underlying.mapFactory + protected[this] def mapFromIterable[K2, V2](it: collection.Iterable[(K2, V2)]): Map[K2,V2] = mapFactory.from(it) + + // Specific building will keep the default but may lose the precise underlying type because our own V can be + // a supertype of the underlying collection's V so we cannot rebuild with potentially new values that are not + // valid for the underlying collection. + protected[this] def fromSpecificIterable(coll: collection.Iterable[(K, V)]): Map[K,V] = new WithDefault[K, V](mapFactory.from(coll), d) + protected[this] def newSpecificBuilder(): mutable.Builder[(K, V), Map[K,V]] = + mapFactory.newBuilder[K, V]().mapResult(new WithDefault[K, V](_, d)) + override def size = underlying.size + def get(key: K) = underlying.get(key) + def iterator() = underlying.iterator() + override def default(key: K): V = d(key) + override def empty = new WithDefault(underlying.empty, d) + + def clear(): Unit = underlying.clear() + def add(elem: (K, V)): this.type = { underlying.add(elem); this } + def subtract(elem: K): this.type = { underlying.subtract(elem); this } + override def put(key: K, value: V): Option[V] = underlying.put(key, value) + override def update(key: K, value: V): Unit = underlying.update(key, value) + override def remove(key: K): Option[V] = underlying.remove(key) + + override def withDefault(d: K => V): Map[K, V] = new WithDefault[K, V](underlying, d) + override def withDefaultValue(d: V): Map[K, V] = new WithDefault[K, V](underlying, x => d) + } +} /** Explicit instantiation of the `Map` trait to reduce class file size in subclasses. */ abstract class AbstractMap[A, B] extends strawman.collection.AbstractMap[A, B] with Map[A, B] diff --git a/test/junit/src/test/scala/strawman/collection/immutable/HashMapTest.scala b/test/junit/src/test/scala/strawman/collection/immutable/HashMapTest.scala index 0e356bad79..b570f3f49e 100644 --- a/test/junit/src/test/scala/strawman/collection/immutable/HashMapTest.scala +++ b/test/junit/src/test/scala/strawman/collection/immutable/HashMapTest.scala @@ -1,7 +1,6 @@ package strawman.collection.immutable import strawman.collection.Hashing - import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -47,4 +46,12 @@ class HashMapTest { } assertEquals(expected, mergedWithMergeFunction) } + + @Test + def testWithDefaultValue: Unit = { + val m1 = HashMap(1 -> "a", 2 -> "b") + val m2 = m1.withDefaultValue(0) + assertEquals("a", m2(1)) + assertEquals(0, m2(3)) + } } \ No newline at end of file