Search code examples
scalagenericstype-safety

Typesafe filteration of List of ADTs by a type parameter


Consider the following example:

I.

class A

class B

sealed trait Test {
  type Meta
}

case class Test1() extends Test {
  type Meta = A
}
case class Test2() extends Test {
  type Meta = B
}

case class Info[T <: Test](t: T, m: T#Meta)

//Filters List[Info[_ <: Test]] by a generic type T <: Test and returns List[Info[T]]
def filter[T <: Test: ClassTag](lst: List[Info[_ <: Test]])(
    implicit ev: ClassTag[T#Meta]): List[Info[T]] =
  lst.collect {
    case Info(t: T, a: T#Meta) =>
      val i: Info[T] = Info[T](t, a)
      i
  }

SCASTIE DEMO

The thing that made me stuck is if the PartialFunction's case is exhaustive. I tried to fully pattern match the Info[_ <: Test] as follows:

II.

val t: Info[_ <: Test] = ???
t match {
  case Info(t: Test1, a: Test1#Meta) =>
    println("1")
  case Info(t: Test2, a: Test2#Meta) =>
    println("2")
}

SCASTIE DEMO

and got the following (pretty scary) warning:

match may not be exhaustive.
It would fail on the following inputs: 
Info((x: _$2 forSome x not in (Test1, Test2)), (x: _$2#Meta forSome x not in (A, B))), 
Info((x: _$2 forSome x not in (Test1, Test2)), ??), Info((x: _$2 forSome x not in (Test1, Test2)), A()), 
Info((x: _$2 forSome x not in (Test1, Test2)), B()), Info(??, (x: _$2#Meta forSome x not in (A, B))), 
Info(Test1(), (x: _$2#Meta forSome x not in (A, B))), 
Info(Test1(), B()), Info(Test2(), (x: _$2#Meta forSome x not in (A, B))),   
Info(Test2(), A())

QUESTION: Is the filter implementation under the case I correct in terms of semantic or some weird cases are missed?


Solution

  • Well, as you can see, you have a list of suggested breaking cases. For example, Info(Test1(), B()). Let's construct one of those.

    Unlike path dependent types as in def foo(t: Test)(meta: t.Meta) = ???, type projections as you use them in case class Info don't encode the implication of T = Test1 => T#Meta = A. Though I'll need a simple utility method to convince scalac into inferring the type I want.

    def castMeta[T <: Test](t: T#Meta): Test#Meta = t
    

    With it, we're able to obtain values typed as Test#Meta from both A and B.

    So,

    val t: Info[_ <: Test] = Info[Test](Test1(), castMeta[Test2](new B))
    

    will cause a MatchError at runtime, and making it didn't involve any "hacks" like nulls, asInstanceOfs or ClassTags.

    Note also that general type projections of form Type#Member are scheduled for removal in Scala 3 due to soundness issues.

    Edit: note that your filter will likely do what you want since you're supplying the target pair of types yourself, it's just that your model totally allows weird cases that scalac is warning you about :)