简体   繁体   中英

Typesafe generics using TypeTag (with safe cast)

I would like to implement parametric classes (like List[T] ) that have the option to do a typesafe cast (eg, x.cast[List[U]] ).

By typesafe I mean that the cast may throw an exception if the type is incorrect at runtime, but it is guaranteed that if the cast succeeds, then the resulting value is of type List[U] . (For example, asInstanceOf does not do that. List(1,2,3).asInstanceOf[List[String]] will succeed, but return a List that does not contain String s.)

My approach is to tag all objects that should support casting with a TypeTag . Specifically, I would implement a trait Typesafe with a method cast[U] which expects an implicit TypeTag for type U and at runtime checks whether the types are subtypes. This is the code I have managed to come up with:

import scala.reflect.runtime.universe.TypeTag

trait Typesafe[+T <: Typesafe[T]] {
  val typeTag: TypeTag[_ <: T]

  def cast[U](implicit typeTag: TypeTag[U]) = {
    if (this.typeTag.tpe <:< typeTag.tpe)
      this.asInstanceOf[U]
    else
      throw new ClassCastException(s"Cannot cast ${this.typeTag} to ${typeTag}")
  }
}

The logic is: A class T that inherits Typesafe[T] will have to instantiate typeTag with a TypeTag[T] . And then the test in cast[U] can only succeed if T is indeed a subtype of U (otherwise, the implicit argument of cast does not exist).

We can implement this trait as follows (this is a simple wrapper class for Sets):

class TypesafeSet[T](val set : Set[T])
                    (implicit val typeTag:TypeTag[_<:TypesafeSet[T]])
  extends Typesafe[TypesafeSet[T]] {
}

Subtyping works, but unfortunately we need to specify the extends Typesafe[...] clause each time.

import scala.collection.immutable.ListSet
class TypesafeListSet[T](set: ListSet[T])
                        (implicit override val typeTag:TypeTag[_<:TypesafeListSet[T]])
  extends TypesafeSet[T](set) with Typesafe[TypesafeListSet[T]] {
}

Question 1: Can we improve on this pattern so that we don't have to repeat extends Typesafe[...] clause? (Currently, if we don't repeat it, TypesafeListSet[T] cannot be cast to TypesafeListSet[T] .)

However, in the following example, we have a problem:

class TypesafeList[T](val list : List[T])
                     (implicit val typeTag:TypeTag[_<:TypesafeList[T]])
  extends Typesafe[TypesafeList[T]] {
  val self = this
  def toSet : TypesafeSet[T] = new TypesafeListSet(ListSet(list : _*))
}

The method toSet does not compile, because the compiler cannot resolve the implicit TypeTag[TypesafeListSet[T]] for new TypesafeListSet . One would need to extract a TypeTag[T] from typeTag, and then reconstruct a TypeTag[TypesafeListSet[T]] from it. I don't know how this is possible.

Question 2: How to get the needed TypeTag in toSet ? (One option would be to add an implicit argument of type TypeTag[TypesafeListSet[T]] to toSet , but that pushes the problem outwards, and leaks an implementation detail, namely that toSet uses a ListSet .)

Finally, the following code can be written, violating the type safety:

class TypesafeOption[T](val option : Option[T])
                       (implicit val typeTag:TypeTag[_<:TypesafeList[T]])
  extends Typesafe[TypesafeList[T]] {
}

Here, we have "accidentally" used TypesafeList in the argument of the Typesafe trait. This compiles fine, but it means that now TypesafeOption will have a typeTag for a TypesafeList ! (And thus the check in cast will be incorrect, and wrong casts may happen.) I believe that such mixups can happen easily, and it would be good if they could be caught by the compiler. (To some extend, the type constraint T <: Typesafe[T] already avoid such mixups (following this ), but unfortunately not the one in TypesafeOption .)

Question 3 ( answered ): Can we refine the definition of the trait Typesafe so that it is impossible to instantiate Typesafe in a way so that cast behaves incorrectly?

Finally, a few lines of code how these classes should be used:

import scala.reflect.runtime.universe.typeTag
object Test {
  def main(args:Array[String]) = {
    val list = new TypesafeList(List(1,2,3))
    val set = list.toSet
    val listSet : TypesafeListSet[Int] = set.cast[TypesafeListSet[Int]]
  }
}

Unfortunately, this code does not compile. The compiler does not find the TypeTag for the call new TypesafeList . We need to add explicitly (typeTag[TypesafeList[Int]]) in that line! (The reason is that new TypesafeList expects a TypeTag[_ <: TypesafeList[Int]] , and the compiler is not clever enough to see that he can just construct a TypeTag[TypesafeList[Int]] .)

Question 4: How can we define TypesafeList so that one does not need to explicitly give TypeTag s?

Finally, I have some questions concerning the overall example:

Question 5: There are (at least) two different TypeTag classes in the runtime, namely scala.reflect.runtime.universe.TypeTag and scala.reflect.api.TypeTags#TypeTag . Which one is correct here?

Question 6: I am comparing the types contained in the TypeTags (ie, typeTag.tpe ). I ignore the mirrors. Should the mirrors be compared, too? (In other words, if two type tags have compatible types but different mirrors, will they be assignment-compatible?)

Question 7: (Possibly related to Question 6.) What happens if types of the same qualified name have been loaded by different classloaders? Will the code above be correct in this case? (Ie, it should not be possible to cast test.Test loaded from classloader 1 to test.Test loaded from classloader 2, as far as I understand.)

Question 8 ( answered ): Is TypeTag the right instrument here? Or should I rather tag the objects directly with Type s? (After all, I compare the types only in cast .) But as far as I can see (from various discussions), TypeTag s are presented as a solution to the problem of tagging classes for typesafety. Why? Or what are TypeTag s for?

Question 9: Any comments on the performance of this? Comparing two types at runtime (with <:< ) sounds potentially expensive... Is there an alternative? (I thought of possibly constructing a map from TypeTags -pairs to Boolean s that remembers which types are assignment-compatible. However, would such the lookup be any faster? TypeTag s do not have unique ids for quick lookup as far as I know. (GHC uses "fingerprints" for this, I think.))

Question 10: Any other observations? Something that I do wrong? Is my conclusion that the cast is typesafe correct?

EDIT: This solution is wrong. When using this solution, it is still possible to use val typeTag = typeTag[Nothing] , and then cast that class to any type.

Answer to Question 3 ( Can we refine the definition of the trait Typesafe so that it is impossible to instantiate Typesafe in a way so that cast behaves incorrectly? )

This can be done by specifying a "self-type" (see spec ). In a trait, one can specify the type T that a class inheriting the class should have. This is done by writing this:T => in the beginning of the trait's definition. In our particular case:

trait Typesafe[+T <: Typesafe[T]] {
  this : T =>
  val typeTag: TypeTag[_ <: T]

  def cast[U](implicit typeTag: TypeTag[U]) = {
    if (this.typeTag.tpe <:< typeTag.tpe)
      this.asInstanceOf[U]
    else
      throw new ClassCastException(s"Cannot cast ${this.typeTag} to ${typeTag}")
  }
}

Now any class that extends Typesafe[T] must be of type T . That is, one can only write class X extends Typesafe[T] if T is a supertype of X , and hence the type tag provided to Typesafe will be guaranteed to be for a supertype of X .

Answer to Question 8 (Is TypeTag the right instrument here? Or should I rather tag the objects directly with Types?) :

One reason is that a TypeTag has a type parameter that represents the type that is tagged. That is, TypeTag[T] statically enforces that we have a TypeTag for T . If we would use Type instead, it would not be enforced that the value Typesafe.typeTag is a tag of the right type.

Example:

trait Typesafe[+T <: Typesafe[T]] {
  val typ: Type
  [...]
}

Now one could write:

class TypesafeSet[T](val set : Set[T])
  extends Typesafe[TypesafeSet[T]] {
  val typ = typeOf[String] // !!!
}

There seems to be no way to avoid this without a TypeTag .

(This does not explain, however, why the TypeTag needs to have a mirror in it in addition to the type.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM