Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 627
drhagen Avatar asked Jan 17 '23 05:01

drhagen


2 Answers

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))))
like image 71
0__ Avatar answered Jan 22 '23 19:01

0__


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.

like image 20
Kaito Avatar answered Jan 22 '23 18:01

Kaito