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?
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:
I'd rather use implicits.
It's not quite what you asked for, but you can solve this problem with implicits:
Map("name" -> "foo")
is a function invocation and not a constructor, this means that you can't write:any more that you can write
To get a mixin, you have to use a concrete type, a naive first attempt would be something like this:
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 oldHashMap
, 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:
This allows you to then write
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 typesSimple enough. You have to supply
Repr
, the type representing the collection, andT
, the type of elements. I useTraversableLike
instead ofTraversable
as it embeds its representation; without this,filterFirstTwo
would return aTraversable
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 theCC
type, which must be a subclass of Traversable (note the use ofX
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 collectionCC[T]
is genuinely a subclass ofTraversableLike
and so a valid argument for theMoreFilterOperations
constructor: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: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 theMoreFilterOperations
constructor? That's a view bound, and permits types that can be implicitly converted toTraversableLike
as well as direct subclasses. Strings are a classic example of this:If you now run it on the REPL:
Map goes in, Map comes out. String goes in, String comes out. etc...
I haven't tried it with a
Stream
yet, or aSet
, or aVector
, but you can be confident that if you did, it would return the same type of collection that you started with.