Search code examples
scalacollectionstype-erasure

Scala: Filtering collection based on type parameter


What is the best way to filter a collection of objects based on the type parameters of those objects, assuming that I have control over both classes and that I need covariant filtering?

Here is some code that doesn't work properly:

trait Foo
case class Foo1() extends Foo
trait ReadableFoo extends Foo {def field: Int}
case class Foo2(field: Int, flag: Boolean) extends ReadableFoo
case class Foo3(field: Int, name: String) extends ReadableFoo

case class Bar[+F <: Foo](foo: F)

val seq = Seq(
  Bar[Foo1](Foo1()),
  Bar[Foo2](Foo2(1,true)), 
  Bar[Foo3](Foo3(1,"Fooz"))
)

// Should keep one
val first = seq collect {case x: Bar[Foo2] => x}

// Should keep two
val both = seq collect {case x: Bar[ReadableFoo] => x}

Now, I know that is it because the case x: Bar[Foo1] gets converted via type erasure to case x: Bar[_] after compilation. I have been unable to use Manifests to solve this problem. Is there some way to add a member type (i.e. memberType = F) to Bar that I can just switch on like case x if (x.memberType <:< ReadableFoo) => x?

Update

0__ quickly found a good solution to the original problem. A slight modification is when the case class field is itself a collection:

case class Bar[+F <: Foo](foo: Seq[F])

val seq = Seq(
  Bar[Foo1](Seq(Foo1())),
  Bar[Foo2](Seq(Foo2(1,true))),
  Bar[ReadableFoo](Seq(Foo2(1,true), Foo3(1,"Fooz")))
)

// Should keep one
val first = seq collect {case x: Bar[Foo2] => x}

// Should keep two
val both = seq collect {case x: Bar[ReadableFoo] => x}

I'm not sure this is possible since the Seq could be empty and, therefore, have no elements to test.


Solution

  • I wasn't aware of the type checking at the extractor trick so my initial solution to your fist problem would've been a little different. I would've provided an extractor for ReadableFoo

    object ReadableFoo { def unapply(x: ReadableFoo) = Some(x.field) }
    

    Then you could do

    val first = seq collect { case x @ Bar(Foo2(_,_)) => x }
    val both  = seq collect { case x @ Bar(ReadableFoo(_)) => x }
    

    But for your updated code, I think you'd need to drag along a manifest.

    case class Bar[+F <: Foo : Manifest](foo: Seq[F]) { 
        def manifest = implicitly[Manifest[_ <: F]] 
    }
    

    Since Bar is covariant and Manifest is invariant we can't simply promise to return a Manifest[F] but a Manifest of some subtype of F. (I guess this was your problem when trying to use manifests?)
    After that you can do

    val first = seq collect {case x if x.manifest <:< manifest[Foo2] => x}
    val both = seq collect {case x if x.manifest <:< manifest[ReadableFoo] => x}
    

    Still, using manifests always feels a little hacky. I'd see if I can use a different approach and rely on type matching and reification as little as possible.