¿Cómo puedo evitar el borrado de texto en Scala? O, ¿por qué no puedo obtener el parámetro de tipo de mis colecciones?

370

Es un hecho triste de la vida en Scala que si crea una instancia de una Lista [Int], puede verificar que su instancia sea una Lista, y puede verificar que cualquier elemento individual de la misma sea una Int, pero no que sea una Lista [ Int], como se puede verificar 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 sin marcar pone la culpa directamente en el tipo de borrado:

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?!

¿Por qué es eso y cómo puedo evitarlo?

Daniel C. Sobral
fuente
Scala 2.8 Beta 1 RC4 acaba de hacer algunos cambios en el funcionamiento del borrado de tipo. No estoy seguro si esto afecta directamente su pregunta.
Scott Morrison
1
Eso es justo lo que los tipos de borrado a , que ha cambiado. El resumen puede resumirse como " Propuesta: la eliminación de" Objeto con A "es" A "en lugar de" Objeto ". " La especificación real es bastante más compleja. Se trata de mixins, en cualquier caso, y esta pregunta se refiere a los genéricos.
Daniel C. Sobral
Gracias por la aclaración: soy un recién llegado scala. Siento que ahora es un mal momento para saltar a Scala. Anteriormente, podría haber aprendido los cambios en 2.8 desde una buena base, ¡más tarde nunca tendría que saber la diferencia!
Scott Morrison
1
Aquí hay una pregunta algo relacionada sobre TypeTags .
pvorb
2
En ejecución scala 2.10.2, vi esta advertencia: <console>:9: warning: fruitless type test: a value of type List[Int] cannot also be a List[String] (but still might match its erasure) case list: List[String] => println("a list of strings?") ^creo que su pregunta y respuesta son muy útiles, pero no estoy seguro de si esta advertencia actualizada es útil para los lectores.
Kevin Meredith

Respuestas:

243

Esta respuesta usa 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, solo 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 evitar eso. 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, por lo general, invocar métodos en ellas. Con el soporte de parámetros implícitos, se convierte en una herramienta muy poderosa. Tome 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". 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 completo y parametrizado.

Sin embargo, Manifesttenga en cuenta que a sigue siendo 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 la biblioteca de reflexión Scala, actualmente en desarrollo, se termine.

Daniel C. Sobral
fuente
3
El getmétodo se puede definir como for ((om, v) <- _map get key if om <:< m) yield v.asInstanceOf[T].
Aaron Novstrup
44
@ Aaron Muy buena sugerencia, pero me temo que podría oscurecer el código para personas relativamente nuevas en Scala. Yo no tenía mucha experiencia con Scala cuando escribí ese código, que fue en algún momento antes de ponerlo en esta pregunta / respuesta.
Daniel C. Sobral
66
@KimStebel ¿Sabes que en TypeTagrealidad se usan automáticamente en la coincidencia de patrones? Genial, ¿eh?
Daniel C. Sobral
1
¡Frio! Tal vez deberías agregar eso a la respuesta.
Kim Stebel
1
Para responder a mi propia pregunta justo arriba: Sí, el compilador genera el Manifestparámetro en sí mismo, consulte: stackoverflow.com/a/11495793/694469 "la instancia [manifest / type-tag] [...] está siendo creada implícitamente por el compilador "
KajMagnus
96

Puede hacer esto usando TypeTags (como Daniel ya menciona, 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 puede hacer esto usando ClassTags (que le ahorra 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!")
}

Las etiquetas de clase se pueden usar siempre que no espere que el parámetro de tipo Asea ​​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 la TypeTag en la coincidencia de patrones en el futuro: https://issues.scala-lang.org/browse/SI-6517

tksfz
fuente
2
¿Qué pasa con la eliminación innecesaria [List String @unchecked], ya que no añade nada a esta coincidencia de patrones (sólo usar case strlist if typeOf[A] =:= typeOf[String] =>lo hará, o incluso case _ if typeOf[A] =:= typeOf[String] =>si no se necesita la variable ligada en el cuerpo del case).
Nader Ghanbari
1
Supongo que eso funcionaría para el ejemplo dado, pero creo que la mayoría de los usos reales se beneficiarían de tener el tipo de elementos.
tksfz
En los ejemplos anteriores, ¿la parte no verificada frente a la condición de guardia no hace un lanzamiento? ¿No obtendrías una excepción de lanzamiento de clase al pasar por las coincidencias en el primer objeto que no se puede lanzar a una cadena?
Toby
Hm, no, creo que no hay un elenco antes de aplicar el guardia: el bit no verificado es una especie de no-op hasta que =>se ejecuta el código a la derecha del . (Y cuando se ejecuta el código en la HR, los guardias brindan una garantía estática sobre el tipo de elementos. Puede haber un elenco allí, pero es seguro.)
tksfz
¿Esta solución produce una sobrecarga de tiempo de ejecución significativa?
stanislav.chetvertkov
65

Puede usar la Typeableclase de tipo de shapeless para obtener el resultado que busca,

Ejemplo de sesión REPL,

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 castoperación será tan precisa como sea posible debido a las Typeableinstancias disponibles dentro del alcance .

Miles Sabin
fuente
14
Cabe señalar que la operación de "conversión" recorrerá de manera recursiva toda la colección y sus subcolecciones y comprobará si todos los valores involucrados son del tipo correcto. (Es decir, l1.cast[List[String]]aproximadamente for (x<-l1) assert(x.isInstanceOf[String]) Para grandes estructuras de datos o si los moldes ocurren con mucha frecuencia, esto puede ser una sobrecarga inaceptable.
Dominique Unruh
16

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 las clases de envoltura 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

thricejamie
fuente
14

Hay una manera de superar el problema de borrado de tipo en Scala. En Overcoming Type Erasure en la coincidencia 1 y Overcoming Type Erasure en Matching 2 (Variance) hay alguna explicación de cómo codificar algunos ayudantes para ajustar los tipos, incluida la Variance, para la coincidencia.

axaluss
fuente
Esto no supera el borrado de tipo. En su ejemplo, haciendo val x: Any = List (1,2,3); x match {case IntList (l) => println (s "Match $ {l (1)}"); case _ => println (s "No match")} produce "No match"
user48956
podrías echar un vistazo a las macros scala 2.10.
Alex
11

Encontré una solución ligeramente mejor para esta limitación del lenguaje que de otra manera sería increíble.

En Scala, el problema del borrado de tipo no ocurre con las matrices. Creo que es más fácil demostrar esto con un ejemplo.

Digamos que tenemos una lista de (Int, String), luego lo siguiente da un tipo de advertencia de borrado

x match {
  case l:List[(Int, String)] => 
  ...
}

Para evitar esto, primero cree una clase de caso:

case class IntString(i:Int, s:String)

luego, en la coincidencia de patrones, haga algo como:

x match {
  case a:Array[IntString] => 
  ...
}

que parece funcionar perfectamente

Esto requerirá cambios menores en su código para trabajar con matrices en lugar de listas, pero no debería ser un problema importante.

Tenga en cuenta que el uso case a:Array[(Int, String)]seguirá dando una advertencia de borrado de tipo, por lo que es necesario utilizar una nueva clase de contenedor (en este ejemplo IntString).

Jus12
fuente
10
"limitación del lenguaje de otro modo impresionante" es menos una limitación de Scala y más una limitación de la JVM. Quizás Scala podría haber sido diseñado para incluir información de tipo mientras se ejecutaba en la JVM, pero no creo que un diseño como ese hubiera preservado la interoperabilidad con Java (es decir, según lo diseñado, puede llamar a Scala desde Java).
Carl G
1
Como seguimiento, el soporte para genéricos reified para Scala en .NET / CLR es una posibilidad continua.
Carl G
6

Como Java no conoce el tipo de elemento real, me pareció más útil usarlo List[_]. Luego, la advertencia desaparece y el código describe la realidad: es una lista de algo desconocido.

llovió
fuente
4

Me pregunto si esta es una solución alternativa adecuada:

scala> List(1,2,3) match {
     |    case List(_: String, _*) => println("A list of strings?!")
     |    case _ => println("Ok")
     | }

¡No coincide con el caso de "lista vacía", pero da un error de compilación, no una advertencia!

error: type mismatch;
found:     String
requirerd: Int

Esto por otro lado parece funcionar ...

scala> List(1,2,3) match {
     |    case List(_: Int, _*) => println("A list of ints")
     |    case _ => println("Ok")
     | }

¿No es aún mejor o me estoy perdiendo el punto aquí?

agilesteel
fuente
3
No funciona con List (1, "a", "b"), que tiene el tipo List [Any]
sullivan-
1
Aunque el punto de Sullivan es correcto y hay problemas relacionados con la herencia, todavía encuentro esto útil.
Seth
0

Quería agregar una respuesta que generalice el problema a: ¿Cómo puedo obtener una representación de cadena del tipo de mi lista en tiempo de ejecución?

import scala.reflect.runtime.universe._

def whatListAmI[A : TypeTag](list : List[A]) = {
    if (typeTag[A] == typeTag[java.lang.String]) // note that typeTag[String] does not match due to type alias being a different type
        println("its a String")
    else if (typeTag[A] == typeTag[Int])
        println("its a Int")

    s"A List of ${typeTag[A].tpe.toString}"
}

val listInt = List(1,2,3)
val listString = List("a", "b", "c")

println(whatListAmI(listInt))
println(whatListAmI(listString))
Steve Robinson-Burns
fuente
-18

Usando guardia de coincidencia de patrones

    list match  {
        case x:List if x.isInstanceOf(List[String]) => do sth
        case x:List if x.isInstanceOf(List[Int]) => do sth else
     }
Huangmao Quan
fuente
44
La razón por la que este no funcionará es que isInstanceOfrealiza una verificación de tiempo de ejecución en función de la información de tipo disponible para la JVM. Y esa información de tiempo de ejecución no contendrá el argumento de tipo a List(debido a la eliminación de tipo).
Dominique Unruh