简体   繁体   中英

Scala: Extending Map and defining +

I'm trying to write a small wrapper class to make the Gson library a bit more Scala friendly. Unfortunately, I'm running into a compile error when I try to get this going the way I would like.

This is the code I've got so far:

package com.test

import com.google.gson.{JsonObject, JsonElement}
import scala.collection.Iterator
import scala.collection.immutable.Map

case class GsonMap ( private val inner: JsonObject = new JsonObject )
    extends Map[String, JsonElement] {

    /** {@inheritDoc} */
    override def iterator: Iterator[(String, JsonElement)]
        = new Iterator[(String, JsonElement)] {
            private val entries = inner.entrySet.iterator
            override def hasNext: Boolean = entries.hasNext
            override def next: (String, JsonElement) = {
                val elem = entries.next
                ( elem.getKey, elem.getValue )
            }
        }

    /**
     * Returns a clone of the inner JsonObject
     */
    private def cloneInner: JsonObject = {
        val result = new JsonObject()
        iterator.foreach { (item) => result.add( item._1, item._2 ) }
        result
    }

    /** {@inheritDoc} */
    override def + ( kv: (String, JsonElement) ): GsonMap = {
        val cloned = cloneInner
        cloned.add( kv._1, kv._2 )
        GsonMap( cloned )
    }

    /** {@inheritDoc} */
    override def get( key: String ): Option[JsonElement]
        = Option( inner.get(key) )

    /** {@inheritDoc} */
    override def - ( key: String ): GsonMap = {
        val cloned = cloneInner
        cloned.remove( key )
        GsonMap( cloned )
    }

}

Now, I know that the + method doesn't match what is defined in the Map class. That's the problem, really. I want the + method to accept JsonElement s and return a GsonMap , but I'm not really sure how to make that work. I've tried a few variations at this point, but with no luck

for reference, this is the compile error I'm receiving:

[info] Compiling 1 Scala source to target/scala-2.9.2/classes...
[error] src/main/scala/GsonMap.scala:7: class GsonMap needs to be abstract, since method + in trait Map of type [B1 >: com.google.gson.JsonElement](kv: (String, B1))scala.collection.immutable.Map[String,B1] is not defined
[error] case class GsonMap ( val inner: JsonObject = new JsonObject )
[error]            ^
[error] src/main/scala/GsonMap.scala:31: method + overrides nothing
[error]     override def + ( kv: (String, JsonElement) ): GsonMap = {
[error]                  ^
[error] two errors found

Any advice out there about this?


UPDATE:

As was also suggested below, this is one of the variations I tried:

override def +[T >: JsonElement] ( kv: (String, T) ): GsonMap = {
    val cloned = cloneInner
    cloned.add( kv._1, kv._2 )
    GsonMap( cloned )
}

However, it fails too:

[info] Compiling 1 Scala source to target/scala-2.9.2/classes...
[error] /src/main/scala/GSON.scala:33: type mismatch;
[error]  found   : T
[error]  required: com.google.gson.JsonElement
[error]         cloned.add( kv._1, kv._2 )
[error]                               ^
[error] one error found

My understanding of the >: operator is that T must be a parent of JsonElement, which I don't think is what I'm looking for. In this case, this map can only ever contain instances of JsonElements, so it wouldn't be appropriate to put in parents of JsonElements.

The direct cause of your error is that your + only accepts JsonElement, while the + in the trait expects a type parameter with an upper bound of JsonElement .

override def +[T >: JsonElement] ( kv: (String, T) ): GsonMap = {
    val cloned = cloneInner
    cloned.add( kv._1, kv._2 )
    GsonMap( cloned )
}

The reason is (as pointed out in @Frank's answer) is that Map is covariant in its value argument, ie if Child is a subtype of Parent , Map[String,Parent] will be a supertype of Map[String, Child] , and this add definition allows you to "up-add" to a Map :

scala> class Element;
defined class Element

scala> class SubElement extends Element;
defined class SubElement

scala> val m = Map("foo"-> new SubElement)
m: scala.collection.immutable.Map[java.lang.String,SubElement] = Map(foo -> SubElement@6a63afa4)

scala> m + ("bar" -> new Element)
res0: scala.collection.immutable.Map[java.lang.String,Element] = Map(foo -> SubElement@2e7ff81e, bar -> Element@654ab15b)

scala> m + ("bar" -> new Element) + ("baz" -> "Text")
res1: scala.collection.immutable.Map[java.lang.String,java.lang.Object] = Map(foo -> SubElement@6a63afa4, bar -> Element@233d0d04, baz -> Text)

If you're trying to implement the immutable Map trait on a mutable backing object, you will have to provide this "up-casting" yourself, or you can give in to the warm embrace of the Scala standard library and instead extend mutable.Map , which already does precisely that for you. If your Java type implements the java.util.Map interface, there's even ready-made wrappers and implicit conversions in scala.collection.JavaConversions .

I don't know what you're trying to do with your custom Map , but it's fairly likely that extending Map isn't the way to go at all (the example for extending maps in the standard intro to the Scala collection library implements a new data structure) and you rather want to deal with Scala maps in most of your code and then provide an implicit to eg convert a Map to the GSON equivalent at the boundaries.

The error is pretty detailled and to the point: you try to overwrite something which isn't in the base class and you have not implemented a required method.

In terms of a solution, what you essentially missed is the variance annotation that Map uses. Look at the ScalaDoc for the Map class and you will see this: Map[A, +B] . This little + is causing your problems.

To understand what's going on, I'd suggest you read up on covariance, and then understand why the + method has a different type signature and does not return a Map[A, B] , but instead a Map[A, B1] , where B1 >: B . You should do the same, as this will also allow you to not only keep a map of invariant JsonElement objects, but profit from the covariance when you have subclasses.

The "+" method needs to have the following signature: +[B1 >: B](kv: (A, B1)): Map[A, B1]

More of an observation than an answer: your GSonMap has a constructor which receives an JsonObject and uses it internally. It also exposes the JsonObject as a public field. The problem is that JsonObject is mutable, and because of the way you expose it in GsonMap the latter also becomes mutable (that's because anyone can modify the JsonObject from the exterior).

So please consider cloning the JsonObject in the constructor and exposing inner as a method that returns a cloned copy of JsonObject instead of the internal object. In this way the immutability of the GsonMap is guaranteed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM