0

I just would like to check for any occurrences of type T in the example bellow. Assert fails though (try me):

val seq = List(AcceptableRisk, UnacceptableRisk)
def any[T] = 
    seq.collect { case x: T => x }.nonEmpty 

assert(any[UnknownRisk] == false) 

trait Risk
case class AcceptableRisk() extends Risk 
case class UnacceptableRisk() extends Risk 
case class UnknownRisk() extends Risk

There is a compilation warning:

jdoodle.scala:5: warning: abstract type pattern T is unchecked since
it is eliminated by erasure
   seq.collect { case x: T => x }.nonEmpty
                         ^

Why does it fail?

Dmitry Nogin
  • 3,670
  • 1
  • 19
  • 35
  • This is usually a code smell that shows that there is a design error somewhere. – Luis Miguel Mejía Suárez Apr 15 '20 at 01:29
  • Are there any recommended way to check for type occurrences in a heterogenous collection? I would not like to call `collect` and `nonEmpty` directly in multiple places where T is known. – Dmitry Nogin Apr 15 '20 at 02:08
  • Not having a heterogeneous collection in the first place. There isn't a direct simple answer to how to solve this, but every time someone says I have a heterogeneous collection and I need to extract some arbitrary type, it means there is a design error somewhere before. – Luis Miguel Mejía Suárez Apr 15 '20 at 02:20
  • Could you point me to any kind of a literature judging on this? What is wrong about type driven pattern matching over the collection content? I bet it is totally ok for items to expose specific traits to be filtered by and processed accordingly. We are not violating LSP by making only some controls on the screen be clickable by the mouse. – Dmitry Nogin Apr 15 '20 at 02:33
  • Any heterogeneous is a **List[Any]** or close to that, which are bad practice in Scala, since you really can not do that much with them. Pattern matching by type doesn't always work due erasure. For example, if you pass a `List[String]` as your `T` it will still match inner `List[Int]` and `List[Boolean]` and any kind of list. Now, if your `T` can only be a subtype of `Risk`, then there isn't really any point of all of that, just `seq.exist(_ == UnacceptableRisk)` – Luis Miguel Mejía Suárez Apr 15 '20 at 02:40
  • My risks are actually `case class UnacceptableRisk(name: String)`, so I cannot use value equality, but very interested in the presence of those three fundamental types (there never going to be a forth one). Would you say that here i get `Seq` somehow? I was expecting `List[Risk]` to be deduced here. – Dmitry Nogin Apr 15 '20 at 02:50
  • Yeah you get a **List[Risk]** so you do not have a heterogeneous list. I will post an answer with some alternatives later. – Luis Miguel Mejía Suárez Apr 15 '20 at 03:01

2 Answers2

1

Since your domain is limited, I would just go with something like this:

trait Risk extends Product with Serializable
object Risk {
  def isUnacceptableRisk(risk: Risk): Boolean = risk match {
    case UnacceptableRisk(_) => true
    case _                   => false
  }
  // Repeat for other types if needed.
}

final case class AcceptableRisk(name: String) extends Risk 
final case class UnacceptableRisk(name: String) extends Risk 
final case class UnknownRisk(name: String) extends Risk

Then you can just: seq.exists(Risk.isUnacceptableRisk)

A more generic solution could be done using typeclasses, like this:

trait Is[T] {
  type U
  def check(t: T): Boolean
}

object Is {
  type Aux[T, _U] = Is[T] { type U = _U }
}

object syntax {
  object is {
     implicit class SeqOps[T](private val seq: Seq[T]) extends AnyVal {
       def any[U <: T](implicit ev: Is.Aux[T, U]): Boolean =
         seq.exists(ev.check)
     }
  } 
}

trait Risk extends Product with Serializable
object Risk {
  implicit final val IsUnacceptableRisk: Is.Aux[Risk, UnacceptableRisk] =
    new Is[Risk] {
      override type U = UnacceptableRisk
      override def check(risk: Risk): Boolean = risk match {
        case UnacceptableRisk(_) => true
        case _                   => false
      }
    }
    // Repeat for other types if needed.
}

final case class AcceptableRisk(name: String) extends Risk 
final case class UnacceptableRisk(name: String) extends Risk 
final case class UnknownRisk(name: String) extends Risk

import syntax.is._
List(AcceptableRisk("foo"), UnacceptableRisk("bar")).any[UnacceptableRisk] // true.

But sincerely IMHO, it is too much work for not really winning too much.

  • Why do you need type `U` in type class `Is`? Actually you don't use it. – Dmytro Mitin Apr 15 '20 at 09:51
  • I can't see any useful difference from https://gist.github.com/DmytroMitin/87f2748d650d87e7882ffbf6e3eb82b4 – Dmytro Mitin Apr 15 '20 at 09:57
  • 1
    @DmytroMitin what if you want to check if there is any of the others types of Risk? My idea was that `T` means the upper type to which the collection would be and `U` is the specific type you want to search. In any case, as I said, I am not really happy with `Is` it feels like too much boilerplate for too little gain. – Luis Miguel Mejía Suárez Apr 15 '20 at 12:27
0

It helps:

def any[T <: Risk: Manifest] = 
    seq.collect { case x: T => x }.nonEmpty 
Dmitry Nogin
  • 3,670
  • 1
  • 19
  • 35