Scala:使用具体类型实现Map

时间:2011-08-10 00:18:37

标签: scala types

我在Scala类型系统中遇到某种怪癖,让我有点难过。我正在尝试创建一个扩展Map [String,String]的类,我无法弄清楚如何以编译器接受它的方式实现+方法。

这是我现在的代码:

class ParamMap(val pairs:List[(String,String)] = Nil) extends Map[String,String] {

  lazy val keyLookup = Map() ++ pairs

  override def get(key: String): Option[String] = keyLookup.get(key)
  override def iterator: Iterator[(String, String)] = pairs.reverseIterator

  /**
   * Add a key/value pair to the map
   */
  override def + [B1 >: String](kv: (String, B1)) = new ParamMap(kv :: pairs)

  /**
   * Remove all values for the given key from the map
   */
  override def -(key: String): ParamMap  = new ParamMap(pairs.filterNot(_._1 == key))

  /**
   * Remove a specific pair from the map
   */
  def -(kv: (String, String)) : ParamMap = new ParamMap(pairs - kv)
}

斯卡拉告诉我:

type mismatch;  found: (String, B1)  required: (String, String)

我相信这是因为允许B1是String的子类型,但我的构造函数只需要一个String(?)。我最初的尝试是:

override def +(kv: (String, String)) = new ParamMap(kv :: pairs)

但这引起了抱怨,因为类型签名与特征不匹配:

class ParamMap needs to be abstract, since method + in trait Map of type [B1 >: String](kv: (String, B1))scala.collection.immutable.Map[String,B1] is not defined
method + overrides nothing

我是Scala的新手,我想我已经在类型系统的工作方式上超越了我的头脑。也许我会尝试搞乱演员,但我觉得可能有一种“更好的方式”,如果我知道的话,将来会给我带来很多麻烦。

有什么想法吗?

4 个答案:

答案 0 :(得分:7)

关于Scala类型系统的一些背景知识。

  • 语法B1 >: String表示B1String超类型。因此B1不太具体,无法转换为String。相反,B1 <: String将是子类型关系。

  • Map特征的定义为Map [A, +B],其中A表示密钥的类型,B表示值的类型。 +B符号表示密钥类型中Map协变,这意味着T <: S隐含Map[A, T] <: Map[A, S]

  • Map.+方法的完整类型为+ [B1 >: B] (kv: (A, B1)): Map[A, B1]B类型的协方差强制使用B1 >: B。以下是其工作原理的示例:给定地图m: Map[String, String]添加具有不太具体的类型kv : (String, Any)的键值对将导致不太具体的地图(m + kv): Map[String, Any]

最后一点说明了ParamMap定义的问题。根据{{​​1}}界面,应该能够将Map类型的密钥添加到Any类型的地图中,然后返回ParamMap <: Map[String, String]。但您尝试定义Map[String, Any]始终返回ParamMap.+,这与ParamMap[String, String]不兼容。

解决问题的一种方法是给Map.+一个显式类型参数,例如(警告未经测试),

ParamMap

但这可能不是你想要的。我认为没有办法将值类型修复为class ParamMap[B](val pairs:List[(String,String)] = Nil) extends Map[String, B] { ... override def + [B1 >: B](kv: (String, B1)) = new ParamMap[B1](kv :: pairs) } 并实现String接口。


考虑到以上所述,为什么你的答案中的代码会编译?您实际上发现了Scala模式匹配的限制(不健全),它可能导致运行时崩溃。这是一个简化的例子:

Map[String, String]

虽然这可以编译,但它没有做任何有用的事情。实际上,它总是会因def foo [B1 >: String](x: B1): Int = { val (s1: Int, s2: Int) = (x, x) s1 }

而崩溃
MatchError

在你的回答中,你基本上告诉编译器将scala> foo("hello") scala.MatchError: (hello,hello) (of class scala.Tuple2) at .foo(<console>:9) at .<init>(<console>:10) at .<clinit>(<console>) ... 实例转换为B1,如果转换不起作用,你将会遇到运行时崩溃。这相当于一个不安全的演员,

String

答案 1 :(得分:4)

你的构造函数需要类型List[String, String]的值是正确的,但问题不在于B1可能是String的子类,而是它可能是超类 - 这是B1 :> String符号表示的内容。

乍一看,您可能想知道为什么父Map类会以这种方式键入方法。实际上,您尝试覆盖的+方法的返回类型为Map[String, B1]。但是,在一般地图的背景下,这是有道理的。假设您有以下代码:

class Parent
class Child extends Parent

val childMap = Map[String, Child]("Key" -> new Child)

val result = childMap + ("ParentKey" -> new Parent)

result的类型必须为Map[String, Parent]。鉴于此,Map中+方法的类型限制是有意义的,但是您的固定类型映射无法实现该方法的设计能力。它的签名允许你传入一个例如键入(String, AnyRef),但使用您在后续答案中提供的方法定义,当您尝试执行MatchErrorkey的分配时,您将获得value。< / p>

这有意义吗?

答案 2 :(得分:2)

我在尝试构建一个Bag[T] Map[T,Int]时遇到了同事的问题。我们找到了两种不同的解决方案:

  1. 使用适当的TraversableMap实施Builder而不是CanBuildFrom,并添加有用的地图方法(get,+, - )。如果需要将集合传递给将map作为参数的函数,则可以使用隐式转换。以下是我们的完整行李实施:https://gist.github.com/1136259

  2. 保持简单:

    object collection {
      type ParamMap = Map[String,String]
      object ParamMap {
        def apply( pairs: List[(String,String)] = Nil ) = Map( pairs:_* )
      }
    }
    

答案 3 :(得分:0)

编译器确实接受了这个:

  override def + [B1 >: String](kv: (String, B1)) = {
    val (key:String, value:String) = kv
    new ParamMap((key,value) :: pairs)
  }

但我不知道为什么那比原来更好。我想如果没有人有更好的解决方案,这是一个可以接受的解决方案。