我在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的新手,我想我已经在类型系统的工作方式上超越了我的头脑。也许我会尝试搞乱演员,但我觉得可能有一种“更好的方式”,如果我知道的话,将来会给我带来很多麻烦。
有什么想法吗?
答案 0 :(得分:7)
关于Scala类型系统的一些背景知识。
语法B1 >: String
表示B1
是String
的超类型。因此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)
,但使用您在后续答案中提供的方法定义,当您尝试执行MatchError
和key
的分配时,您将获得value
。< / p>
这有意义吗?
答案 2 :(得分:2)
我在尝试构建一个Bag[T]
Map[T,Int]
时遇到了同事的问题。我们找到了两种不同的解决方案:
使用适当的Traversable
和Map
实施Builder
而不是CanBuildFrom
,并添加有用的地图方法(get,+, - )。如果需要将集合传递给将map作为参数的函数,则可以使用隐式转换。以下是我们的完整行李实施:https://gist.github.com/1136259
保持简单:
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)
}
但我不知道为什么那比原来更好。我想如果没有人有更好的解决方案,这是一个可以接受的解决方案。