¿Cómo puedo evitar el borrado de tipos en Scala? ¿O por qué no puedo obtener el parámetro de tipo de mis colecciones?
Es una triste realidad en Scala que si crea una instancia de una Lista[Int], puede verificar que su instancia es una Lista, y puede verificar que cualquier elemento individual de ella es un Int, pero no que sea una Lista[ Int], como se puede comprobar fácilmente:
scala> List(1,2,3) match {
| case l : List[String] => println("A list of strings?!")
| case _ => println("Ok")
| }
warning: there were unchecked warnings; re-run with -unchecked for details
A list of strings?!
La opción -unchecked culpa directamente al borrado de tipos:
scala> List(1,2,3) match {
| case l : List[String] => println("A list of strings?!")
| case _ => println("Ok")
| }
<console>:6: warning: non variable type-argument String in type pattern is unchecked since it is eliminated by erasure
case l : List[String] => println("A list of strings?!")
^
A list of strings?!
¿A qué se debe esto y cómo puedo solucionarlo?
Esta respuesta utiliza
Manifest
-API, que está en desuso a partir de Scala 2.10. Consulte las respuestas a continuación para obtener soluciones más actuales.
Scala se definió con Type Erasure porque la Máquina Virtual Java (JVM), a diferencia de Java, no obtuvo genéricos. Esto significa que, en tiempo de ejecución, sólo existe la clase, no sus parámetros de tipo. En el ejemplo, JVM sabe que está manejando un scala.collection.immutable.List
, pero no que esta lista está parametrizada con Int
.
Afortunadamente, hay una característica en Scala que te permite solucionar este problema. Es el Manifiesto . Un Manifiesto es una clase cuyas instancias son objetos que representan tipos. Dado que estas instancias son objetos, puede pasarlas, almacenarlas y, en general, llamar a métodos en ellas. Con el soporte de parámetros implícitos, se convierte en una herramienta muy poderosa. Tomemos el siguiente ejemplo, por ejemplo:
object Registry {
import scala.reflect.Manifest
private var map= Map.empty[Any,(Manifest[_], Any)]
def register[T](name: Any, item: T)(implicit m: Manifest[T]) {
map = map.updated(name, m -> item)
}
def get[T](key:Any)(implicit m : Manifest[T]): Option[T] = {
map get key flatMap {
case (om, s) => if (om <:< m) Some(s.asInstanceOf[T]) else None
}
}
}
scala> Registry.register("a", List(1,2,3))
scala> Registry.get[List[Int]]("a")
res6: Option[List[Int]] = Some(List(1, 2, 3))
scala> Registry.get[List[String]]("a")
res7: Option[List[String]] = None
Al almacenar un elemento, también almacenamos un "Manifiesto" del mismo. Un manifiesto es una clase cuyas instancias representan tipos de Scala. Estos objetos tienen más información que JVM, lo que nos permite probar el tipo parametrizado completo.
Sin embargo, tenga en cuenta que Manifest
todavía es una característica en evolución. Como ejemplo de sus limitaciones, actualmente no sabe nada sobre la varianza y supone que todo es covariante. Espero que se vuelva más estable y sólido una vez que se termine la biblioteca de reflexión de Scala, actualmente en desarrollo.
Puedes hacer esto usando TypeTags (como ya menciona Daniel, pero lo explicaré explícitamente):
import scala.reflect.runtime.universe._
def matchList[A: TypeTag](list: List[A]) = list match {
case strlist: List[String @unchecked] if typeOf[A] =:= typeOf[String] => println("A list of strings!")
case intlist: List[Int @unchecked] if typeOf[A] =:= typeOf[Int] => println("A list of ints!")
}
También puedes hacer esto usando ClassTags (lo que te evita tener que depender de scala-reflect):
import scala.reflect.{ClassTag, classTag}
def matchList2[A : ClassTag](list: List[A]) = list match {
case strlist: List[String @unchecked] if classTag[A] == classTag[String] => println("A List of strings!")
case intlist: List[Int @unchecked] if classTag[A] == classTag[Int] => println("A list of ints!")
}
ClassTags se pueden utilizar siempre y cuando no se espere que el parámetro de tipo A
sea un tipo genérico.
Desafortunadamente, es un poco detallado y necesita la anotación @unchecked para suprimir una advertencia del compilador. El compilador puede incorporar automáticamente el TypeTag en la coincidencia de patrones en el futuro: https://issues.scala-lang.org/browse/SI-6517
Puedes usar la Typeable
clase de tipo de sin forma para obtener el resultado que buscas.
Sesión REPL de muestra,
scala> import shapeless.syntax.typeable._
import shapeless.syntax.typeable._
scala> val l1 : Any = List(1,2,3)
l1: Any = List(1, 2, 3)
scala> l1.cast[List[String]]
res0: Option[List[String]] = None
scala> l1.cast[List[Int]]
res1: Option[List[Int]] = Some(List(1, 2, 3))
La cast
operación será un borrado lo más preciso posible dadas las Typeable
instancias disponibles dentro del alcance.
Se me ocurrió una solución relativamente simple que sería suficiente en situaciones de uso limitado, esencialmente envolviendo tipos parametrizados que sufrirían el problema de borrado de tipos en clases contenedoras que se pueden usar en una declaración de coincidencia.
case class StringListHolder(list:List[String])
StringListHolder(List("str1","str2")) match {
case holder: StringListHolder => holder.list foreach println
}
Esto tiene el resultado esperado y limita el contenido de nuestra clase de caso al tipo deseado, Listas de cadenas.
Más detalles aquí: http://www.scalafied.com/?p=60