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.
You can combine extractor with type check:
val first = seq collect { case x @ Bar(_: Foo2) => x }
val both = seq collect { case x @ Bar(_: ReadableFoo) => x }
But the return type will still be List[Bar[Foo]]
... so if you need that, with this approach you would need to cast or re-construct the Bar
object (case Bar(f: Foo2) => Bar(f)
).
With a heterogeneous Seq
I guess you are looking for collect
on the Seq
itself?
case class Bar(seq: Seq[Foo])
def onlyFoo2(b: Bar) = Bar(b.seq.collect { case f: Foo2 => f })
onlyFoo2(Bar(Seq(Foo1(), Foo2(1, true))))
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With