- About Scala
- Documentation
- Code Examples
- Software
- Scala Developers
Maps and withDefault
Tue, 2010-10-19, 08:11
I had tried before to generalize withDefault and withDefaultValue on
mutable and immutable maps, a couple times even, so when I saw that
patch go by I thought "how'd he make it look so easy..." Let me
deconstruct some of the issues which stymied me last time and which
haunt the present implementation. You don't need to pick it up again as
I have a patch to address all these in reasonable ways, but there are
subtle and dangerous issues in here which all should be aware of.
* Issue #1: overloading across subtype boundaries.
This is the biggest. I'm convinced it should never be done, or at least
never when generics are involved. I would like for the compiler to warn
sternly about it.
Here is a signature in collection.Map:
def withDefault[B1 >: B](d: A => B1): Map[A, B1] = new Map.WithDefault[A, B1](this, d)
Here is a signature in collection.mutable.Map. Notice no override.
def withDefault(d: A => B): Map[A, B] = new Map.WithDefault[A, B](this, d)
Of course you can't override, because mutable.Map is invariant in the
value and you can't tighten the signature. We can't abstract over
variance so there's not much to be done at this point. But you don't
want to overload, because then you get this:
scala> val m = scala.collection.mutable.Map(1 -> 2)
m: scala.collection.mutable.Map[Int,Int] = Map((1,2))
scala> m.withDefault((x: Int) => x + 2)
res0: scala.collection.mutable.Map[Int,Int] = Map((1,2))
scala> m.withDefault((x: Int) => x + "2")
res1: scala.collection.Map[Int,Any] = Map((1,2))
Working with an invariant collection, you make what should be a type
error and instead you win a new, wrongly typed collection.
* Issue #2: the wrapper trap
Wrapper types are always full of opportunities for the wrapping to get
off-kilter and leak abstraction all over your shoes.
scala> val m = scala.collection.mutable.Map(1 -> 2)
m: scala.collection.mutable.Map[Int,Int] = Map((1,2))
scala> val m1 = m withDefaultValue 42
m1: scala.collection.mutable.Map[Int,Int] = Map((1,2))
scala> m1(10)
res0: Int = 42
scala> val m2 = m1 withDefaultValue -5000
m2: scala.collection.mutable.Map[Int,Int] = Map((1,2))
scala> m2(50)
res1: Int = 42
* Issue #3: inadequate return type
scala> val m = scala.collection.immutable.Map(1 -> 2)
m: scala.collection.immutable.Map[Int,Int] = Map((1,2))
scala> val m1 = m withDefaultValue 42
m1: scala.collection.Map[Int,Int] = Map((1,2))
It was how to deal with #1 that stymied me before, but now I know what
to do: abandon collection.Map and put distinct method implementations in
mutable and immutable Map. Once in a while we have to suck it up and
admit a little code duplication. (This is a looong ways from the worst
example of that in trunk anyway.) I'll check it in tomorrow. Let's all
keep our tomatoes ready: I too will have to dodge the bugaboos...
> scala> val m = scala.collection.mutable.Map(1 -> 2)
> m: scala.collection.mutable.Map[Int,Int] = Map((1,2))
>
> scala> m.withDefault((x: Int) => x + 2)
> res0: scala.collection.mutable.Map[Int,Int] = Map((1,2))
>
> scala> m.withDefault((x: Int) => x + "2")
> res1: scala.collection.Map[Int,Any] = Map((1,2))
>
> Working with an invariant collection, you make what should be a type
> error and instead you win a new, wrongly typed collection.
>
>
The issue #1 below is probably a very good reason to have these methods
named differently for mutable and immutable maps.
> * Issue #2: the wrapper trap
>
> Wrapper types are always full of opportunities for the wrapping to get
> off-kilter and leak abstraction all over your shoes.
>
> scala> val m = scala.collection.mutable.Map(1 -> 2)
> m: scala.collection.mutable.Map[Int,Int] = Map((1,2))
>
> scala> val m1 = m withDefaultValue 42
> m1: scala.collection.mutable.Map[Int,Int] = Map((1,2))
>
> scala> m1(10)
> res0: Int = 42
>
> scala> val m2 = m1 withDefaultValue -5000
> m2: scala.collection.mutable.Map[Int,Int] = Map((1,2))
>
> scala> m2(50)
> res1: Int = 42
>
>
True. As far as `m2` is concerned, `50` now exists in the underlying
map. `WithDefault` should be overriden to pattern match against the type
of the `underlying`.
This appears to have been happening originally in the maps as well:
scala> val m = scala.collection.immutable.Map(1 -> 2)
m: scala.collection.immutable.Map[Int,Int] = Map((1,2))
scala> val m1 = m withDefaultValue 42
m1: scala.collection.Map[Int,Int] = Map((1,2))
scala> m1(10)
res4: Int = 42
scala> val m2 = m1 withDefaultValue 5000
m2: scala.collection.Map[Int,Int] = Map((1,2))
scala> m2(50)
res5: Int = 42
> * Issue #3: inadequate return type
>
> scala> val m = scala.collection.immutable.Map(1 -> 2)
> m: scala.collection.immutable.Map[Int,Int] = Map((1,2))
>
> scala> val m1 = m withDefaultValue 42
> m1: scala.collection.Map[Int,Int] = Map((1,2))
>
>
It's possible to go around this by redefining the withDefaultValue in
immutable.Map to a more specific return type.
> It was how to deal with #1 that stymied me before, but now I know what
> to do: abandon collection.Map and put distinct method implementations in
> mutable and immutable Map. Once in a while we have to suck it up and
> admit a little code duplication. (This is a looong ways from the worst
> example of that in trunk anyway.) I'll check it in tomorrow. Let's all
> keep our tomatoes ready: I too will have to dodge the bugaboos...
>
>