scalagenericsruntime-type

scala filter by type


I have read TypeTag related article, but I am unable to realize filter a collection by elements type.

Example:

trait A
class B extends A
class C extends A

val v = Vector(new B,new C)
v filter ( _.isInstanceOf[B] )

The code above works fine. However I want to extract filter out of v. E.g.

def filter[T,T2](data:Traversable[T2]) = (data filter (  _.isInstanceOf[T])).asInstanceOf[Traversable[T]]

//Then filter v by
filter[B,A](v)

In this case I get warning abstract type T is unchecked since it is eliminated by erasure. I tried to use TypeTag, but it seems not easy to get Type on runtime.

Is there any elegant solution to realize function filter? Any solution via scala macro is also acceptable.


Solution

  • You need to provide a ClassTag, not a TypeTag, and use pattern matching. ClassTags work well with pattern matching. You can even use the collect method to perform the filter and map together:

    def filter[T, T2](data: Traversable[T2])(implicit ev: ClassTag[T]) = data collect {
        case t: T => t
    }
    

    For example:

    val data = Seq(new B, new B, new C, new B)
    filter[B, A](data) //Traversable[B] with length 3
    filter[C, A](data) //Traversable[C] with length 1
    

    One problem with this is that it might not work as expected with nested generic types.

    The collect method takes in a parameter of type PartialFunction, representing a function that does not need to be defined on the entire domain. When using collect elements where the PartialFunction is undefined are filtered out, and elements that matched some case statement are mapped accordingly.

    You can also use existential types and let the compiler deduce the type of the data parameter for a more concise syntax. You can also use context bounds:

    def filter[T : ClassTag](data: Traversable[_]) = data collect { case t: T => t }
    filter[B](data)
    

    One problem with the methods here is that there is a significant difference between the native filter method you have: these methods always returns a Traversable while the native filter returns the best type it can. For example:

    val data = Vector(new B, new B, new C, new B)
    data filter { _.isInstanceOf[B] } //Vector[A]
    data filter { _.isInstanceOf[B] } map { _.asInstanceOf[B] } //Vector[B]
    data collect { case t: B => t } //Vector[B].  Note that if you know the type at the calling point, this is pretty concise and might not need a helper method at all
    
    //As opposed to:
    filter[B](data) //Traversable[B], not a Vector!
    

    You can fix this by using the CanBuildFrom pattern using another implicit parameter. You can also use implicit classes to essentially add the method to the class (as opposed to calling the method in the static style shown above). This all adds up to a pretty complicated method, but I'll leave it here if you're interested in these enhancements:

    implicit class RichTraversable[T2, Repr <: TraversableLike[T2, Repr], That](val trav: TraversableLike[T2, Repr]) extends AnyVal {
        def withType[T : ClassTag](implicit bf: CanBuildFrom[Repr, T, That]) = trav.collect {
            case t: T => t
        }
    }
    

    This would allow you to do:

    data.withType[B] //Vector[B], as desired