开发者

How to subclass Scala immutable.Map with fixed type parameters?

开发者 https://www.devze.com 2023-03-03 10:38 出处:网络
I can\'t figure out how to deal with overriding \"+\" in an immutable map if the map can only store an invariant type for its values.

I can't figure out how to deal with overriding "+" in an immutable map if the map can only store an invariant type for its values.

Something like:

class FixedMap(val impl : Map[String, Int])
    extends immutable.Map[String, Int] with immutable.MapLike[String, Int, FixedMap] {
    // This should return FixedMap if B1 is Int, and Map[String,B1]
    // if B1 is a superclass of Int; but there's no way to do that.
    // It is possible to return FixedMap here but then you have to
    // throw at runtime if B1 is not Int
    override def +[B1 >: Int](kv : (String, B1)) : Map[String, B1] = {
        kv match {
开发者_运维技巧            case (k, v : Int) =>
                new FixedMap(impl + Pair(k, v))
            case _ =>
                impl + kv
        }
    }
    // ...
}

I'd like this to work like the methods that use CanBuildFrom and always keep the original type if possible. Is there a way? Or do Map subclasses always have to leave the value type as a type parameter?

Here's a complete compilable example:

import scala.collection.immutable

// pointless class that wraps another map and adds one method
class FixedMap(val impl : Map[String, Int])
    extends immutable.Map[String, Int] with immutable.MapLike[String, Int, FixedMap] {

    override val empty : FixedMap = FixedMap.empty

    // This should return FixedMap if B1 is Int, and Map[String,B1]
    // if B1 is a superclass of Int; but there's no way to do that.
    // It is possible to return FixedMap here but then you have to
    // throw at runtime if B1 is not Int
    override def +[B1 >: Int](kv : (String, B1)) : Map[String, B1] = {
        kv match {
            case (k, v : Int) =>
                new FixedMap(impl + Pair(k, v))
            case _ =>
                impl + kv
        }
    }

    override def -(key : String) : FixedMap = {
        new FixedMap(impl - key)
    }

    override def get(key : String) : Option[Int] = {
        impl.get(key)
    }

    override def iterator : Iterator[(String, Int)] = {
        impl.iterator
    }

    def somethingOnlyPossibleOnFixedMap() = {
        println("FixedMap says hi")
    }
}

object FixedMap {
    val empty : FixedMap = new FixedMap(Map.empty)
}

object TestIt {
    val empty = FixedMap.empty
    empty.somethingOnlyPossibleOnFixedMap()
    val one = empty + Pair("a", 1)
    // Can't do the below because one is a Map[String,Int] not a FixedMap
    // one.somethingOnlyPossibleOnFixedMap()
}


Here's what I'd try:

class FixedMap(val impl: immutable.Map[String, Int])
    extends immutable.Map[String, Int] with immutable.MapLike[String, Int, FixedMap] {

  override def +[B1 >: Int](kv: (String, B1)): immutable.Map[String, B1] = impl + kv

  def +(kv: (String, Int))(implicit d: DummyImplicit): FixedMap = new FixedMap(impl + kv)

  // ...

}

You're right: you cannot override the + that already exists, so you have to leave it there — otherwise your subclass wouldn't be able to do things that the superclasses can do, which violates the Liskov substitution principle. But you can add an additional method wich the exact arguments that you want (and you don't need a CanBuildFrom in this particular case).

The only problem is that the new method has the exact same type erasure as the one you were trying to override, but which has an incompatible signature. To solve this, you can add the DummyImplicit — defined in Predef as a class for which an implicit value is always available. Its main use is to work around type erasure in case of overloading like this.

Note that the static type of the map on which you want to call your overloaded method has to be FixedMap for this to work. If an object has a run-time type of FixedType but is statically typed to a regular Map[String, Int], the compiler won't call your new overloaded method.


It's possible to implement what you want using CanBuildFrom. The question is - do you really want/need to make it? The are several similar questions in SO, here is one of them (hope you will find answer there):

Extending Scala collections

Generally Scala gives use enough tools to avoid this (extending collections). And you really need a good reason to start with this.


It looks like it does work (best I can tell so far) if you add another + overload:

def +(kv : (String, Int))(implicit bf : CanBuildFrom[FixedMap, (String, Int), FixedMap]) : FixedMap = {
    val b = bf(empty)
    b ++= this
    b += kv
    b.result
}

The problem I was seeing doing this before was caused by returning FixedMap from the overridden +, thus preventing any upgrade to a generic map. I guess this allowed implicit conversion of the pair being +'d to work. But if you fix that overridden + method to return Map[String,B1] again, you can't use implicit conversion on the pair's value anymore. No way for the compiler to know whether to go to a superclass map i.e. Map[String,Any] or implicitly convert to Int in order to stick with FixedMap. Interesting that the return type of the method changes whether implicit conversions are used; I guess given a FixedMap return type the compiler can deduce that B1 is always just B (or something like that!).

Anyway this seems to be my bug; you just can't use implicit conversion on the pair._2 passed to + while being compatible with the Map interface, even conceptually. Now that I gave up on that, I think the overloaded + will work OK.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号