Successfully implement MapLike in Scala - scala

I'm having a dastard of a time implementing a Map in scala typed to (for our purposes) [String, Set[Foo]] that provides extra operations for the Foos in the values. The actual implementation is more complicated than presented below, but this is the gist. I need a class that implements all the Map-like functions and provides the extra-and-above aggregation on the collections that are the values of the map. The extends Map with MapLike patterns I've seen, particularly this, are not working.
What I've got so far:
import scala.collection.{immutable, Map, MapLike}
class Foo(a:Int)
class CustomMap
(val underlying:Map[String,Set[Foo]] = Map[String,Set[Foo]]())
extends Map[String, Set[Foo]] with MapLike[String, Set[Foo], CustomMap] {
override def empty = new CustomMap(underlying.empty)
def -(key: String) = new CustomMap(underlying - key)
def +[B1 >: Set[Foo]](kv: (String, B1)): scala.collection.Map[String,B1] = new CustomMap(underlying + (kv._1 -> kv._2))
def get(key: String): Option[Set[Foo]] = underlying.get(key)
def iterator: Iterator[(String, Set[Foo])] = underlying.iterator
override def size = underlying.size
def getAllFoos() = underlying.values.flatten.toSet
}
val cm1:CustomMap = new CustomMap(Map("a" -> Set(new Foo(1))))
val cm2:CustomMap = cm1 + ("a" -> Set(new Foo(2)))
println(cm2.getAllFoos)
There are issues both with the + method and accessing the extra aggregate methods.
Any pointers?
file.scala:12: error: type mismatch;
found : B1
required: Set[this.Foo]
def +[B1 >: Set[Foo]](kv: (String, B1)): scala.collection.Map[String,B1] = new CustomMap(underlying + (kv._1 -> kv._2))
^
file.scala:24: error: type mismatch;
found : scala.collection.Map[String,Set[this.Foo]]
required: this.CustomMap
val cm2:CustomMap = cm1 + ("a" -> Set(new Foo(2)))
^
two errors found

+ can't return a CustomMap because sometimes B1 won't be a Set[Foo] but some other supertype of Set[Foo]. This is the source of your errors. Map and MapLike are meant for classes that provide a map implementation that could safely have any value added to it and will return a usable Map. So a Map[String, Set[Foo]] can always have an ("", 5) put into it and become a Map[String, Any].
You can eliminate the wrapper around your underlying, as well as avoid these problems, by using the "pimp-my-library" pattern:
implicit class FooSetMap(val map: Map[String,Set[Foo]]) extends AnyVal {
def getAllFoos = map.values.flatten.toSet
}
If you are willing to use a mutable map, take a look at collection.mutable.MultiMap. It is a mixin trait which adds extra methods to subtypes of mutable.Map[A, mutable.Set[B]] for working with multimaps - you could do something similar for your needs.

Related

Binding Existential types in Scala

This is my basic CMap which map classes (Class[T] for any T) to any type of value.
scala> type CMap = Map[Class[T] forSome{type T}, Any]
defined type alias CMap
scala> val cMap: CMap = Map(classOf[Int]->5, classOf[String]->"abc", classOf[Double]->"ddd")
cMap: CMap = Map(int -> 5, class java.lang.String -> abc, double -> ddd)
Now I want a "bound" CMap (call it CMapBind). Like CMap, it maps classes (any classes) to values (any values). But unlike the CMap, CMapBind has a type binding between the key and value, meaning I hope the following behavior:
val cMapBind: CMapBind = Map(classOf[Int]->5, classOf[String]-> "aa") // should compile
val cMapBind: CMapBind = Map(classOf[Int]->5, classOf[String]-> 0) // should fail compile
How do I implement CMapBind?
I know the following two won't work syntactically/logically.
scala> type CMapBind = Map[Class[T] forSome{type T}, T]
<console>:8: error: not found: type T
type CMapBind = Map[Class[T] forSome{type T}, T]
scala> type CMapBind = Map[Class[T], T] forSome{type T}
scala> val cMapBind: CMapBind = Map(classOf[Int]->5, classOf[String]->"str")
<console>:8: error: type mismatch;
found : scala.collection.immutable.Map[Class[_ >: String with Int],Any]
required: CMapBind
(which expands to) Map[Class[T],T] forSome { type T }
val cMapBind: CMapBind = Map(classOf[Int]->5, classOf[String]->"str")
Note that here I am using type constructor Class[T] as an example to illustrate the question. In my code I have my own types, e.g. trait Animal[S, T], class Dog extends Animal[Int, String].
Edit 1:
I should have mentioned that I am using immutable Map as an example, but what I really need is a mutable heterogeneous Map).
Let's try to implement mutable HMap. Some explanations about Scala type system is here by #MilesSabin: http://www.chuusai.com/2011/07/16/fundeps-in-scala/
The idea is to statically check the constructor (here you will see, that it's arity depends on your hands, so it is possible to generate it or smth else), and the insert method. By the way, same way immutable HMap implemented in shapeless.
import scala.collection.mutable.Map
class HMapBuilder[R[_, _]] { // constructor arity is two
def apply[K1, V1](e1: (K1, V1))(implicit ev1: R[K1, V1]) =
new HMap[R](Map(e1))
def apply[K1, V1, K2, V2](e1: (K1, V1), e2: (K2, V2))
(implicit ev1: R[K1, V1], ev2: R[K2, V2]) =
new HMap[R](Map(e1, e2))
}
So it is a constructor of our map. Evidences will statically check types of the inserting data. Next, lets wrap the default scala collection:
class HMap[R[_, _]](underlying : Map[Any, Any] = Map.empty) {
def get[K, V](k : K)(implicit ev : R[K, V]) : Option[V] =
underlying.get(k).asInstanceOf[Option[V]]
def +=[K, V](kv : (K, V))(implicit ev : R[K, V]) : HMap[R] = {
underlying += kv
this
}
def -=[K](k : K) : HMap[R] = {
underlying -= k
this
}
override def toString = underlying.toString
}
Finally wrapping HMapBuilder, to make a pleasant constructor:
object HMap {
def apply[R[_, _]] = new HMapBuilder[R]
def empty[R[_, _]] = new HMap[R]
def empty[R[_, _]](underlying : Map[Any, Any]) =
new HMap[R](underlying)
}
In result, the usage is similar to shapeless HMap:
class Mapping[K, V]
implicit def mappingFromClass[A] = new Mapping[Class[A], A]
val hm = HMap[Mapping](classOf[Int] -> 5) // ok
hm += (classOf[String] -> "string") // ok
hm += (classOf[Boolean] -> false) // ok
hm += (classOf[Double] -> "sss") // compile fail
Works as expected. I implemented only insert and remove functions, other functions is possible to define same way.

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 JsonElements 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, i.e. 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 e.g. 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.

Scala: implementing Map with concrete types

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]
You're correct that your constructor expects a value of type List[String, String], but the issue isn't that B1 could be a subclass of String, it's that it could be a superclass -- this is what the B1 :> String notation indicates.
At first glance, you might wonder why the parent Map class would have the method typed this way. In fact, the return type of the + method you're attempting to override there is Map[String, B1]. In the context of a general map, though, this makes sense. Suppose you had the following code:
class Parent
class Child extends Parent
val childMap = Map[String, Child]("Key" -> new Child)
val result = childMap + ("ParentKey" -> new Parent)
The type of result would then have to be Map[String, Parent]. In light of this, the type restrictions on the + method in Map makes sense, but your fixed-type map isn't capable of fulfilling what the method is designed to be able to do. Its signature allows you to pass in a value of e.g. type (String, AnyRef), but using the method definition you gave in your followup answer, you'll get a MatchError when it tries to perform the assignment to key and value.
Does that make sense?
I ran the same problem with a colleague, when trying to build a Bag[T] which is a Map[T,Int]. We found two different solutions:
Implement Traversable rather than Map with appropriate Builder and CanBuildFrom, and add the useful map methods (get,+,-). If you need to pass the collection to a function taking maps as arguments, you can use implicit conversions. Here is our full Bag implementation: https://gist.github.com/1136259
Stay simple:
object collection {
type ParamMap = Map[String,String]
object ParamMap {
def apply( pairs: List[(String,String)] = Nil ) = Map( pairs:_* )
}
}
The compiler does seem to accept this one:
override def + [B1 >: String](kv: (String, B1)) = {
val (key:String, value:String) = kv
new ParamMap((key,value) :: pairs)
}
But I don't know why that is better than the original. I suppose this is an acceptable solution if nobody has a better one.

How to subclass Scala immutable.Map with fixed type parameters?

I can't figure out how to deal with overriding "+" in an immutable map if the map can only store an invariant type for its values.
Something like:
class FixedMap(val impl : Map[String, Int])
extends immutable.Map[String, Int] with immutable.MapLike[String, Int, FixedMap] {
// This should return FixedMap if B1 is Int, and Map[String,B1]
// if B1 is a superclass of Int; but there's no way to do that.
// It is possible to return FixedMap here but then you have to
// throw at runtime if B1 is not Int
override def +[B1 >: Int](kv : (String, B1)) : Map[String, B1] = {
kv match {
case (k, v : Int) =>
new FixedMap(impl + Pair(k, v))
case _ =>
impl + kv
}
}
// ...
}
I'd like this to work like the methods that use CanBuildFrom and always keep the original type if possible. Is there a way? Or do Map subclasses always have to leave the value type as a type parameter?
Here's a complete compilable example:
import scala.collection.immutable
// pointless class that wraps another map and adds one method
class FixedMap(val impl : Map[String, Int])
extends immutable.Map[String, Int] with immutable.MapLike[String, Int, FixedMap] {
override val empty : FixedMap = FixedMap.empty
// This should return FixedMap if B1 is Int, and Map[String,B1]
// if B1 is a superclass of Int; but there's no way to do that.
// It is possible to return FixedMap here but then you have to
// throw at runtime if B1 is not Int
override def +[B1 >: Int](kv : (String, B1)) : Map[String, B1] = {
kv match {
case (k, v : Int) =>
new FixedMap(impl + Pair(k, v))
case _ =>
impl + kv
}
}
override def -(key : String) : FixedMap = {
new FixedMap(impl - key)
}
override def get(key : String) : Option[Int] = {
impl.get(key)
}
override def iterator : Iterator[(String, Int)] = {
impl.iterator
}
def somethingOnlyPossibleOnFixedMap() = {
println("FixedMap says hi")
}
}
object FixedMap {
val empty : FixedMap = new FixedMap(Map.empty)
}
object TestIt {
val empty = FixedMap.empty
empty.somethingOnlyPossibleOnFixedMap()
val one = empty + Pair("a", 1)
// Can't do the below because one is a Map[String,Int] not a FixedMap
// one.somethingOnlyPossibleOnFixedMap()
}
Here's what I'd try:
class FixedMap(val impl: immutable.Map[String, Int])
extends immutable.Map[String, Int] with immutable.MapLike[String, Int, FixedMap] {
override def +[B1 >: Int](kv: (String, B1)): immutable.Map[String, B1] = impl + kv
def +(kv: (String, Int))(implicit d: DummyImplicit): FixedMap = new FixedMap(impl + kv)
// ...
}
You're right: you cannot override the + that already exists, so you have to leave it there — otherwise your subclass wouldn't be able to do things that the superclasses can do, which violates the Liskov substitution principle. But you can add an additional method wich the exact arguments that you want (and you don't need a CanBuildFrom in this particular case).
The only problem is that the new method has the exact same type erasure as the one you were trying to override, but which has an incompatible signature. To solve this, you can add the DummyImplicit — defined in Predef as a class for which an implicit value is always available. Its main use is to work around type erasure in case of overloading like this.
Note that the static type of the map on which you want to call your overloaded method has to be FixedMap for this to work. If an object has a run-time type of FixedType but is statically typed to a regular Map[String, Int], the compiler won't call your new overloaded method.
It's possible to implement what you want using CanBuildFrom. The question is - do you really want/need to make it? The are several similar questions in SO, here is one of them (hope you will find answer there):
Extending Scala collections
Generally Scala gives use enough tools to avoid this (extending collections). And you really need a good reason to start with this.
It looks like it does work (best I can tell so far) if you add another + overload:
def +(kv : (String, Int))(implicit bf : CanBuildFrom[FixedMap, (String, Int), FixedMap]) : FixedMap = {
val b = bf(empty)
b ++= this
b += kv
b.result
}
The problem I was seeing doing this before was caused by returning FixedMap from the overridden +, thus preventing any upgrade to a generic map. I guess this allowed implicit conversion of the pair being +'d to work. But if you fix that overridden + method to return Map[String,B1] again, you can't use implicit conversion on the pair's value anymore. No way for the compiler to know whether to go to a superclass map i.e. Map[String,Any] or implicitly convert to Int in order to stick with FixedMap. Interesting that the return type of the method changes whether implicit conversions are used; I guess given a FixedMap return type the compiler can deduce that B1 is always just B (or something like that!).
Anyway this seems to be my bug; you just can't use implicit conversion on the pair._2 passed to + while being compatible with the Map interface, even conceptually. Now that I gave up on that, I think the overloaded + will work OK.

Mixing in generic traits in parameterized classes without duplicating type parameters

Let's assume I want to create a trait that I can mix in into any Traversable[T]. In the end, I want to be able to say things like:
val m = Map("name" -> "foo") with MoreFilterOperations
and have methods on MoreFilterOperations that are expressed in anything Traversable has to offer, such as:
def filterFirstTwo(f: (T) => Boolean) = filter(f) take 2
However, the problem is clearly that T is not defined as a type parameter on MoreFilterOperations. Once I do that, it's doable of course, but then my code would read:
val m = Map("name" -> "foo") with MoreFilterOperations[(String,String)]
or if I define a variable of this type:
var m2: Map[String,String] with MoreFilterOperations[(String,String)] = ...
which is way to verbose for my taste. I would like to have the trait defined in such a way that I could write the latter as:
var m2: Map[String,String] with MoreFilterOperations
I tried self types, abstract type members, but it hasn't resulted in anything useful. Any clues?
Map("name" -> "foo") is a function invocation and not a constructor, this means that you can't write:
Map("name" -> "foo") with MoreFilterOperations
any more that you can write
val m = Map("name" -> "foo")
val m2 = m with MoreFilterOperations
To get a mixin, you have to use a concrete type, a naive first attempt would be something like this:
def EnhMap[K,V](entries: (K,V)*) =
new collection.immutable.HashMap[K,V] with MoreFilterOptions[(K,V)] ++ entries
Using a factory method here to avoid having to duplicate the type params. However, this won't work, because the ++ method is just going to return a plain old HashMap, without the mixin!
The solution (as Sam suggested) is to use an implicit conversion to add the pimped method. This will allow you to transform the Map with all the usual techniques and still be able to use your extra methods on the resulting map. I'd normally do this with a class instead of a trait, as having constructor params available leads to a cleaner syntax:
class MoreFilterOperations[T](t: Traversable[T]) {
def filterFirstTwo(f: (T) => Boolean) = t filter f take 2
}
object MoreFilterOperations {
implicit def traversableToFilterOps[T](t:Traversable[T]) =
new MoreFilterOperations(t)
}
This allows you to then write
val m = Map("name"->"foo", "name2"->"foo2", "name3"->"foo3")
val m2 = m filterFirstTwo (_._1.startsWith("n"))
But it still doesn't play nicely with the collections framework. You started with a Map and ended up with a Traversable. That isn't how things are supposed to work. The trick here is to also abstract over the collection type using higher-kinded types
import collection.TraversableLike
class MoreFilterOperations[Repr <% TraversableLike[T,Repr], T] (xs: Repr) {
def filterFirstTwo(f: (T) => Boolean) = xs filter f take 2
}
Simple enough. You have to supply Repr, the type representing the collection, and T, the type of elements. I use TraversableLike instead of Traversable as it embeds its representation; without this, filterFirstTwo would return a Traversable regardless of the starting type.
Now the implicit conversions. This is where things get a bit trickier in the type notation. First, I'm using a higher-kinded type to capture the representation of the collection: CC[X] <: Traversable[X], this parameterises the CC type, which must be a subclass of Traversable (note the use of X as a placeholder here, CC[_] <: Traversable[_] does not mean the same thing).
There's also an implicit CC[T] <:< TraversableLike[T,CC[T]], which the compiler uses to statically guarantee that our collection CC[T] is genuinely a subclass of TraversableLike and so a valid argument for the MoreFilterOperations constructor:
object MoreFilterOperations {
implicit def traversableToFilterOps[CC[X] <: Traversable[X], T]
(xs: CC[T])(implicit witness: CC[T] <:< TraversableLike[T,CC[T]]) =
new MoreFilterOperations[CC[T], T](xs)
}
So far, so good. But there's still one problem... It won't work with maps, because they take two type parameters. The solution is to add another implicit to the MoreFilterOperations object, using the same principles as before:
implicit def mapToFilterOps[CC[KX,VX] <: Map[KX,VX], K, V]
(xs: CC[K,V])(implicit witness: CC[K,V] <:< TraversableLike[(K,V),CC[K,V]]) =
new MoreFilterOperations[CC[K,V],(K,V)](xs)
The real beauty comes in when you also want to work with types that aren't actually collections, but can be viewed as though they were. Remember the Repr <% TraversableLike in the MoreFilterOperations constructor? That's a view bound, and permits types that can be implicitly converted to TraversableLike as well as direct subclasses. Strings are a classic example of this:
implicit def stringToFilterOps
(xs: String)(implicit witness: String <%< TraversableLike[Char,String])
: MoreFilterOperations[String, Char] =
new MoreFilterOperations[String, Char](xs)
If you now run it on the REPL:
val m = Map("name"->"foo", "name2"->"foo2", "name3"->"foo3")
// m: scala.collection.immutable.Map[java.lang.String,java.lang.String] =
// Map((name,foo), (name2,foo2), (name3,foo3))
val m2 = m filterFirstTwo (_._1.startsWith("n"))
// m2: scala.collection.immutable.Map[java.lang.String,java.lang.String] =
// Map((name,foo), (name2,foo2))
"qaxfwcyebovjnbointofm" filterFirstTwo (_ < 'g')
//res5: String = af
Map goes in, Map comes out. String goes in, String comes out. etc...
I haven't tried it with a Stream yet, or a Set, or a Vector, but you can be confident that if you did, it would return the same type of collection that you started with.
It's not quite what you asked for, but you can solve this problem with implicits:
trait MoreFilterOperations[T] {
def filterFirstTwo(f: (T) => Boolean) = traversable.filter(f) take 2
def traversable:Traversable[T]
}
object FilterImplicits {
implicit def traversableToFilterOps[T](t:Traversable[T]) = new MoreFilterOperations[T] { val traversable = t }
}
object test {
import FilterImplicits._
val m = Map("name" -> "foo", "name2" -> "foo2", "name3" -> "foo3")
val r = m.filterFirstTwo(_._1.startsWith("n"))
}
scala> test.r
res2: Traversable[(java.lang.String, java.lang.String)] = Map((name,foo), (name2,foo2))
Scala standard library uses implicits for this purpose. E.g. "123".toInt. I think its the best way in this case.
Otherwise you'll have to go through full implementation of your "map with additional operations" since immutable collections require creation of new instances of your new mixed class.
With mutable collections you could do something like this:
object FooBar {
trait MoreFilterOperations[T] {
this: Traversable[T] =>
def filterFirstTwo(f: (T) => Boolean) = filter(f) take 2
}
object moreFilterOperations {
def ~:[K, V](m: Map[K, V]) = new collection.mutable.HashMap[K, V] with MoreFilterOperations[(K, V)] {
this ++= m
}
}
def main(args: Array[String]) {
val m = Map("a" -> 1, "b" -> 2, "c" -> 3) ~: moreFilterOperations
println(m.filterFirstTwo(_ => true))
}
}
I'd rather use implicits.