I'm running into some kind of quirk in the Scala type system that has me a bit stumped. I am trying to make a class that extends Map[String,String] and I can't quite figure out how to implement the + method in such a way that the compiler accepts it.
Here's the code I have now:
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)
}
Scala tells me this:
type mismatch; found: (String, B1) required: (String, String)
I believe this is because B1 is allowed to be a subtype of String but my constructor expects just a String (?). My original attempt was:
override def +(kv: (String, String)) = new ParamMap(kv :: pairs)
But this complained because the type signature didn't match the trait:
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
I'm new to Scala and I think I'm getting over my head here in terms of how the type system works. Perhaps I'll try messing with casting but I have a feeling there might be a "better way" that, if I know it, will save me a lot of trouble in the future.
Any ideas?
Some background about Scala's type system.
The syntax B1 >: String
means that B1
is a supertype of String
. So B1
is less specific, and can't be cast to a String
. Conversely, B1 <: String
would be a subtype relationship.
The definition of the Map
trait is Map [A, +B]
, where A
represents the type of the key and B
the type of the value. The +B
notation says that Map
is covariant in the key type, which means that T <: S
implies Map[A, T] <: Map[A, S]
.
The full type of the Map.+
method is + [B1 >: B] (kv: (A, B1)): Map[A, B1]
. The covariance of B
kind of forces the use of B1 >: B
. Here's an example of how it works: given a map m: Map[String, String]
adding a key-value pair with a less specific type kv : (String, Any)
will result in a less specific map, (m + kv): Map[String, Any]
.
The last point illustrates the problem with your ParamMap
definition. According to the Map
interface, one should be able to add a key of type Any
to a map of type ParamMap <: Map[String, String]
and get back a Map[String, Any]
. But you're trying to define ParamMap.+
to always return ParamMap[String, String]
, which is incompatible with Map.+
.
One way to fix the problem is to give ParamMap
an explicit type parameter, something like (warning untested),
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)
}
but this may not be what you want. I don't think there's a way to fix the value type as String
and implement the Map[String, String]
interface.
Given all the above, why does the code in your answer compile? You've actually uncovered a limitation (unsoundness) of Scala's pattern matching, and it can lead to run-time crashes. Here's a simplified example:
def foo [B1 >: String](x: B1): Int = {
val (s1: Int, s2: Int) = (x, x)
s1
}
Although this compiles, it doesn't do anything useful. In fact, it will always crash with a MatchError
:
scala> foo("hello")
scala.MatchError: (hello,hello) (of class scala.Tuple2)
at .foo(<console>:9)
at .<init>(<console>:10)
at .<clinit>(<console>)
...
In your answer, you've basically told the compiler to convert a B1
instance to a String
, and if the conversion doesn't work, you'll get a runtime crash. It's equivalent to an unsafe cast,
(value: B1).asInstanceOf[String]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With