Can we elegantly match an erased type in scala?

后端 未结 1 1803
借酒劲吻你
借酒劲吻你 2021-01-14 06:20

Is there any elegant way, to arrive from:

def foo[T: TypeTag](a: A[T]) {

  // can we match on the type of T here?

}

at a match expression

相关标签:
1条回答
  • 2021-01-14 06:59

    Sadly no, as the compiler does not take type tags into account if you add type checks to a pattern. I'm not sure why and whether this is planned. You can however compare type tags for equality:

    typeOf[T] =:= typeOf[List[String]]
    

    You can use that in an if or match condition and then cast to the target type.

    After thinking a bit more about it, I recognized that it would be quite easy to write my own pattern extractor, that hides the check and cast:

    import scala.reflect.runtime.universe._
    class TypeTest[A: TypeTag]() {
      def unapply[B: TypeTag](v: B): Option[A] =
        if(typeOf[B] <:< typeOf[A])
          Some(v.asInstanceOf[A])
        else
          None
    }
    object TypeTest {
      def apply[A: TypeTag] = new TypeTest()
    }
    

    Now, we can do stuff like this:

    def printIfStrings[T: TypeTag](v: T) {
      val string = TypeTest[List[String]]
      v match {
        case string(s) => printString(s)
        case _ =>
      }
    }
    
    def printString(s: List[String]) {
      println(s)
    }
    
    printIfStrings(List(123))
    printIfStrings(List("asd"))
    

    This is already quite neat, but as Scala does not support passing an argument directly to an extractor in a pattern, we have to define all extractors as val string before the match expression.

    Macros

    Macros can transform code, so it should be easy enough to transform any unchecked typechecks in a match expression into an appropriate pattern or add explicit checks using the type tags directly.

    This however requires that we have a macro invocation wrapped around every critical match expression, which would be quite ugly. An alternative is to replace match expressions by some method call that takes a partial function as an argument. This method can be provide for an arbitrary type using an implicit conversion.

    The only remaining problem then is that the compiler typechecks the code before any macros are invoked, so it will generate a warning for the unchecked cast even though it is now checked. We can still us @unchecked to suppress these warnings.

    I chose to replace type checks in patterns by the extractor described above instead of adding a condition to the case and explicit type casts. The reason for that is that this transformation is local (I just have to replace a subexpression with another).

    So here is the macro:

    import scala.language.experimental.macros
    import scala.language.implicitConversions
    import scala.reflect.macros.blackbox.Context
    
    object Switch {
    
      implicit class Conversion[A](val value: A) {
        def switch[B](f: PartialFunction[A, B]): B = macro switchImpl
      }
    
      def switchImpl(c: Context)(f: c.Tree): c.Tree = {
        import c.universe._
    
        val types = collection.mutable.Map[Tree,String]()
        val t1 = new Transformer {
          override def transformCaseDefs(trees: List[CaseDef]) = {
            val t2 = new Transformer {
              override def transform(tree: Tree) = {
                def pattern(v: String, t: Tree) = {
                  val check = types.getOrElseUpdate(t, c.freshName())
                  pq"${TermName(check)}(${TermName(v)})"
                }
                tree match {
                  case Bind(TermName(v),Typed(Ident(termNames.WILDCARD),
                      Annotated(Apply(
                        Select(New(Ident(TypeName("unchecked"))),
                        termNames.CONSTRUCTOR), List()
                      ), t)))
                    => pattern(v,t)
                  case Bind(TermName(v),Typed(Ident(termNames.WILDCARD),t)) 
                    => pattern(v,t)
                  case _ => super.transform(tree)
                }
              }
            }
            t2.transformCaseDefs(trees)
          }
        }
        val tree = t1.transform(c.untypecheck(f))
        val checks =
          for ((t,n) <- types.toList) yield 
            q"val ${TermName(n)} = Switch.TypeTest[$t]"
    
        q"""
          ..$checks
          $tree(${c.prefix}.value)
        """
      }
    
      import scala.reflect.runtime.universe._
      class TypeTest[A: TypeTag]() {
        def unapply[B: TypeTag](v: B): Option[A] =
          if(typeOf[B] <:< typeOf[A]) Some(v.asInstanceOf[A])
          else None
      }
      object TypeTest {
        def apply[A: TypeTag] = new TypeTest()
      }
    }
    

    And now magically type checks in patterns work:

    import Switch.Conversion
    val l = List("qwe")
    
    def printIfStrings2[T: scala.reflect.runtime.universe.TypeTag](v: T) {
      v switch {
        case s: Int => println("int")
        case s: List[String] @unchecked => printString(s)
        case _ => println("none")
      }
    }
    
    printIfStrings2(l)
    printIfStrings2(List(1, 2, 3))
    printIfStrings2(1)
    

    I'm not sure whether I handle all possible cases correctly, but every thing I tried worked fine. A type with multiple annotations is possibly not handled correctly if it is also annotated by @unchecked, but I couldn't find an example in the standard library to test this.

    If you leave out the @unchecked the result is exactly the same, but as mentioned above you will get a compiler warning. I don't see a way to get rid of that warning with normal macros. Maybe annotation macros can do it but they are not in the standard branch of Scala.

    0 讨论(0)
提交回复
热议问题