3

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 on the type of T?

Obviously this below does not overcome erasure for T, so must we inspect the TypeTag by hand?

  a match {
    case _:A[SomeSpecificType] => ...

Or does scala afford some elegance to that end?

matanster
  • 15,072
  • 19
  • 88
  • 167
  • There is an open enhancement ticket on this at https://issues.scala-lang.org/browse/SI-6517 – Seth Tisue Feb 03 '16 at 18:02
  • @SethTisue thanks for mentioning the tumbleweed ticket... If I had to choose one improvement for the next scala version, it would be streamlining the type system for type erasure resiliency. In my experience, any serious type architecture bumps into this glass sealing very quickly, resulting in really rough boilerplate and non-idiomatic code. At the end of the day, a type system with type erasure is rather oxymoronic... Can this be "fixed"? I don't know. – matanster Feb 03 '16 at 21:32

1 Answers1

4

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.

dth
  • 2,287
  • 10
  • 17
  • Thanks for the well attuned answer. Just keeping this open in case something else pops up. – matanster Feb 03 '16 at 16:15
  • well, i did pop up again ;-) this solution is actually much nicer than directly using =:=, sadly there is no way to give a parameter directly to an extractor, so this is probably as nice as it can be. – dth Feb 03 '16 at 16:34
  • Thanks for the brilliant sugar, and I share in the sadness, it is really sad that we use a language emphasizing an elaborate type system ... for a runtime that erases its types. – matanster Feb 03 '16 at 16:48
  • A macro might ease things a bit further :) – matanster Feb 03 '16 at 16:56
  • Well, macros cannot really modify syntax. It seems there is a compiler plugin somewhere that allows to pass parameters to extractors, which would be an improvement. Also, one could write a macro, lets call it "ematch" that takes a partial function as an argument and thus looks like a match-statement. This could be used to implement the typechecks. However, the compiler might still generate warnings before applying the macro and the code generated for partial functions is quite complicated. it would be difficult but possible to transform it as required. If I get bored I might try to do it ;) – dth Feb 03 '16 at 22:25
  • Frankly it sounds like something that should be avoided ;( – matanster Feb 03 '16 at 23:21
  • So, I had the time to try. I'm currently playing arround a bit with macros anyway, so this was a nice exercise. It turned out to be not as complicated as the API directly provides a way to transform only selected subexpressions of a larger tree, so the macro does not depend too much on how partialfunctions are encoded. Of course macros are still experimental, so I would probably not use this excessively in production code. – dth Feb 04 '16 at 13:35