Christian P.
Christian P.

Reputation: 4884

Filter with type on generics type

I have the following code (simplified):

sealed trait Funding {
  id: Int
}

final case class FlatRate(id: Int, days: Int, amount: BigDecimal) extends Funding
final case class PerItem(id: Int, amount: BigDecimal, discount: BigDecimal) extends Funding

final case class AppliedFunding[+A <: Funding](
  person: String
)

I have some code that returns a list of "generic" AppliedFunding and I would like to get a specific AppliedFunding with the correct type. Pseudo-code:

val allFunding: List[AppliedFunding[Funding]] = <...>
val flatrateFunding: List[AppliedFunding[FlatRate]] = allFunding.someMagicFunction

In place of someMagicFunction I am looking for a way to filter the correct AppliedFunding instances out and return them with the specific type. I can do allFunding.collect { ... }, but no matter what I tried it would still return List[AppliedFunding[Funding]].

Any help is much appreciated.

Upvotes: 3

Views: 300

Answers (2)

Alexander Arendar
Alexander Arendar

Reputation: 3435

Below is a solution on how this can be done with help of isInstanceOf and asInstanceOf. Another working solution is provided by Dominic in other answer, but for filtering use case I am not sure that creating a new case class instance for each item which passes the filter is a proper way to go. Moreover, desugaring with scalac -print shows that isInstanceOf invocation is not going anywhere in that case, it will be embedded in generated isDefinedAt and applyOrElse methods for the partial function used in collect.

Some people say that using isInstanceOf and asInstanceOf is a code smell but for me it looks that in this particular case usage is safe and correct and brings no single benefit over the solution with collect: invocation of collect just hides isInstanceOf in the generated source code and instantiating new case classes instances is a trade off for a safe call of asInstanceOf on a filtered collection.

Also please pay attention that I've added a funding field to the AppliedFunding case class. I think that having AppliedFunding instances without embedded info about funding itself is not very useful anyway.

object Test {
  sealed trait Funding {
    val id: Int
  }

  final case class FlatRate(id: Int, days: Int, amount: BigDecimal) extends Funding
  final case class PerItem(id: Int, amount: BigDecimal, discount: BigDecimal) extends Funding

  final case class AppliedFunding[+A <: Funding](
    person: String, funding: A)

  val allFundings: List[AppliedFunding[Funding]] = List(AppliedFunding[FlatRate]("alex", FlatRate(1, 10, 100)), AppliedFunding[PerItem]("christian", PerItem(2, 100, 10)))

  def flatRatePredicate[A <: Funding](appliedFunding: AppliedFunding[A]): Boolean = appliedFunding.funding.isInstanceOf[FlatRate]

  val flatRateFundings: List[AppliedFunding[FlatRate]] = allFundings.filter(flatRatePredicate).asInstanceOf[List[AppliedFunding[FlatRate]]]

  def main(args: Array[String]): Unit = {
    println(flatRateFundings)
  }
}

Upvotes: 0

Dominic Egger
Dominic Egger

Reputation: 1016

below are 2 ways how to do it. Just a sideremark: collect is one of the very few situations where PartialFunctionis a good thing

sealed trait Foo
final case class Bar(i:Int) extends Foo
final case class Baz(d:Double) extends Foo

final case class Container[F <: Foo](s:String, f:F) 


val all:List[Container[Foo]] = List(
  Container("bar", Bar(3)),
  Container("bar", Bar(4)),
  Container("bar", Bar(5)),
  Container("bar", Bar(6)),
  Container("bar", Bar(7)),
  Container("baz", Baz(1.5)),
  Container("baz", Baz(2.5)),
  Container("baz", Baz(3.5)),
  Container("baz", Baz(4.5)),
  Container("baz", Baz(5.5))
  )


val bars:List[Container[Bar]] = all.collect { case Container(s, Bar(i)) => Container(s, Bar(i))}
val baz:List[Container[Baz]] = all.foldRight(List.empty[Container[Baz]]) (
    (elem, lst) => elem match {
      case Container(s, Baz(d)) => Container(s, Baz(d)) :: lst
      case _ => lst
    }
  )

println(bars)
println(baz)

Upvotes: 3

Related Questions