tags:

views:

59

answers:

1

I am a Scala novice so forgive me if this is a stupid question, but here goes...

Imagine I wish to create an extended Map type that includes additional methods. I can see a few ways to do this. The first would be with composition:

class Path[V](val m: Map[V, Int]) {
  // Define my methods
}

Another would be via inheritance, e.g.

class Path[V] extends Map[V, Int] {
// Define my methods
}

Finally, I've also considered the "trait" route, e.g.

trait Path[V] extends Map[V, Int] {
// Define my methods
}

Composition is a bit awkward because you are constantly having to reference the thing inside. Inheritance is a fairly natural but there is a wrinkle for me (more in a sec). Traits seems like a really elegant way to do this and using the "with" construct it is really nice but it also has an issue for me.

The wrinkle I'm running into is with methods like ++. They return a new map. So let's say the "my method" referred to above wishes to add something to the map (just an example, I know the map already has this), e.g.

trait Path[V] extends Map[V,Int] {
    def addOne(v: V, i: Int): Path[V] = this + (v -> i)
}

This generates an error because the return type is not Path[V]. Now I know I can use "with" on a new instance to add the Path[V] trait. But I don't control the construction of the new map here. Is there any way to add the Path[V] trait? I thought about creating a new immutable map that was pre-populated and then tagging on a "with Path[V]", but there is no such constructor I can use to create the pre-populated map.

I suspect (although I haven't confirmed it) that I would have a similar issue using inheritance. I could add a new method to add a new entry to the map, but I would not get back a "Path[V]" which is what I want. The compositional approach seems to be the only way to go here.

I hope this is clear. Comments?

+5  A: 

Perhaps the simplest way to do this is to use the MapProxy trait:

import collection._

class Path[V](val self: Map[V, Int]) extends MapProxy[V, Int] {
   // without the companion object below
   def addOne(v: V, i: Int): Path[V] = new Path(this + (v -> i))
   // with the companion object below
   def addOne(v: V, i: Int): Path[V] = this + (v -> i)
   // other methods
}

// this companion object is not strictly necessary, but is useful:
object Path {
   implicit def map2path[V](map: Map[V, Int]): Path[V] = new Path(map)
}

This eliminates the awkwardness with the compositional approach by allowing you to treat a Path as if it is a Map:

scala> new Path(Map("a" -> 1)) + ("b" -> 2)
res1: scala.collection.Map[java.lang.String,Int] = Map((a,1), (b,2))

scala> new Path(Map("a" -> 1)).get("a")
res2: Option[Int] = Some(1)

If you include the implicit conversion from Map to Path (defined in the Path companion object), then you can also treat a Map as if it is a Path:

scala> Map("a" -> 1).addOne("b", 2)
res3: Path[java.lang.String] = Map((a,1), (b,2))

There is a similar SeqProxy trait for adding behavior to a Seq.

In the more general case, a solution is to use the pimp my library pattern. This related question has an example.

Aaron Novstrup
Thank you very much not just for a solution, but for such a comprehensive explanation. This is very cool and useful information.
Michael Tiller