Como contornar a eliminação de tipos no Scala? Ou por que não consigo obter o parâmetro type das minhas coleções?

370

É um fato triste da vida no Scala que, se você instanciar uma lista [Int], pode verificar se sua instância é uma lista e se qualquer elemento individual dela é um Int, mas não é uma lista [ Int], como pode ser facilmente verificado:

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

A opção -unchecked coloca a culpa diretamente no apagamento de tipo:

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 que é isso e como faço para contornar isso?

Daniel C. Sobral
fonte
O Scala 2.8 Beta 1 RC4 fez algumas alterações na forma como o apagamento do tipo funciona. Não tenho certeza se isso afeta diretamente sua pergunta.
21811 Scott Morrison
11
Isso é apenas o que tipos de apagamento para , isso mudou. O resumo pode ser resumido como " Proposta: O apagamento de" Objeto com A "é" A "em vez de" Objeto ". " A especificação real é bastante mais complexa. É sobre mixins, de qualquer forma, e esta questão está relacionada aos genéricos.
22139 Daniel C. Sobral
Obrigado pelo esclarecimento - sou um novato em scala. Sinto que agora é uma má hora para pular para Scala. Antes, eu poderia ter aprendido as mudanças na 2.8 com uma boa base, depois nunca precisaria saber a diferença!
21711 Scott Morrison
11
Aqui está uma pergunta um pouco relacionada sobre TypeTags .
Pvorb
2
Em execução scala 2.10.2, vi esse aviso: <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?") ^acho sua pergunta e resposta muito úteis, mas não tenho certeza se esse aviso atualizado é útil para os leitores.
Kevin Meredith

Respostas:

243

Esta resposta usa Manifest-API, que foi descontinuada no Scala 2.10. Consulte as respostas abaixo para obter soluções mais atuais.

Scala foi definido com o Type Erasure porque a Java Virtual Machine (JVM), ao contrário do Java, não obteve genéricos. Isso significa que, no tempo de execução, apenas a classe existe, não seus parâmetros de tipo. No exemplo, a JVM sabe que está manipulando a scala.collection.immutable.List, mas não com a qual esta lista está parametrizada Int.

Felizmente, há um recurso no Scala que permite contornar isso. É o manifesto . Um manifesto é uma classe cujas instâncias são objetos que representam tipos. Como essas instâncias são objetos, você pode distribuí-las, armazená-las e geralmente chamar métodos sobre elas. Com o suporte de parâmetros implícitos, torna-se uma ferramenta muito poderosa. Veja o exemplo a seguir, por exemplo:

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

Ao armazenar um elemento, também armazenamos um "Manifesto". Um manifesto é uma classe cujas instâncias representam os tipos Scala. Esses objetos têm mais informações do que a JVM, o que nos permite testar o tipo completo e parametrizado.

Observe, no entanto, que a Manifestainda é um recurso em evolução. Como exemplo de suas limitações, atualmente ele não sabe nada sobre variação e assume que tudo é co-variante. Espero que fique mais estável e sólido assim que a biblioteca de reflexão Scala, atualmente em desenvolvimento, for concluída.

Daniel C. Sobral
fonte
3
O getmétodo pode ser definido como for ((om, v) <- _map get key if om <:< m) yield v.asInstanceOf[T].
Aaron Novstrup
4
@ Aaron Muito boa sugestão, mas receio que possa obscurecer o código para pessoas relativamente novas no Scala. Eu não tinha muita experiência com Scala quando escrevi esse código, algum tempo antes de colocá-lo nesta pergunta / resposta.
Daniel C. Sobral
6
@KimStebel Você sabe que TypeTagsão realmente usados ​​automaticamente na correspondência de padrões? Legal, né?
Daniel C. Sobral
11
Legal! Talvez você deva adicionar isso à resposta.
Kim Stebel
11
Para responder minha própria pergunta acima: Sim, o compilador gera o Manifestpróprio parâmetro, consulte: stackoverflow.com/a/11495793/694469 "a instância [manifest / tag de tipo] [...] está sendo criada implicitamente pelo compilador "
KajMagnus 12/11/12
96

Você pode fazer isso usando TypeTags (como Daniel já menciona, mas eu explicarei explicitamente):

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!")
}

Você também pode fazer isso usando ClassTags (que evita que você precise depender do 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 podem ser usadas desde que você não espere que o parâmetro type Aseja um tipo genérico.

Infelizmente, é um pouco detalhado e você precisa da anotação @unchecked para suprimir um aviso do compilador. O TypeTag pode ser incorporado à correspondência de padrões automaticamente pelo compilador no futuro: https://issues.scala-lang.org/browse/SI-6517

tksfz
fonte
2
Que tal remover o desnecessário, [List String @unchecked]pois ele não adiciona nada a essa correspondência de padrões (apenas o uso o case strlist if typeOf[A] =:= typeOf[String] =>fará, ou mesmo case _ if typeOf[A] =:= typeOf[String] =>se a variável vinculada não for necessária no corpo do case).
Nader Ghanbari 25/10
11
Eu acho que isso funcionaria para o exemplo dado, mas acho que a maioria dos usos reais se beneficiaria de ter o tipo dos elementos.
Tksfz 02/11/19
Nos exemplos acima, a peça desmarcada na frente da condição de guarda não faz um gesso? Você não obteria uma exceção de conversão de classe ao passar pelas correspondências no primeiro objeto que não pode ser convertido em uma string?
Toby
Hum, não, eu acredito que não há elenco antes de aplicar a guarda - o bit não verificado é uma espécie de não operação até que o código à direita da =>seja executado. (E quando o código sobre os RHS é executado, os guardas fornecer uma garantia estática do tipo de elementos Pode haver um elenco lá, mas é seguro..)
tksfz
Esta solução produz uma sobrecarga significativa no tempo de execução?
Stanislav.chetvertkov
65

Você pode usar a Typeableclasse type de informe para obter o resultado desejado,

Exemplo de sessão 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))

A castoperação será a mais precisa possível de apagamento, considerando as Typeableinstâncias disponíveis no escopo .

Miles Sabin
fonte
14
Deve-se observar que a operação "cast" passará recursivamente por toda a coleção e suas sub-coleções e verificará se todo o valor envolvido é do tipo correto. ( Ou seja, l1.cast[List[String]]funciona aproximadamente for (x<-l1) assert(x.isInstanceOf[String]) Para grandes estruturas de dados ou se as transmissões ocorrem com muita frequência, isso pode ser uma sobrecarga inaceitável.
Dominique Unruh
16

Eu vim com uma solução relativamente simples que seria suficiente em situações de uso limitado, essencialmente agrupando tipos parametrizados que sofreriam com o problema de apagamento de tipo nas classes de invólucro que podem ser usadas em uma declaração de correspondência.

case class StringListHolder(list:List[String])

StringListHolder(List("str1","str2")) match {
    case holder: StringListHolder => holder.list foreach println
}

Isso tem a saída esperada e limita o conteúdo de nossa classe de caso ao tipo desejado, String Lists.

Mais detalhes aqui: http://www.scalafied.com/?p=60

thricejamie
fonte
14

Existe uma maneira de superar o problema de apagamento de tipo no Scala. Em Superando apagamento de tipo na correspondência 1 e Superando apagamento de tipo na correspondência 2 (variação), há algumas explicações sobre como codificar alguns auxiliares para agrupar os tipos, incluindo variação, para correspondência.

axaluss
fonte
Isso não supera o apagamento de tipo. No exemplo dele, fazendo val x: Any = List (1,2,3); x corresponde a {case IntList (l) => println (s "Corresponde a $ {l (1)}"); case _ => println (s "Sem correspondência")} produz "Sem correspondência"
user48956 18/13
você pode dar uma olhada nas macros do scala 2.10.
Alex
11

Encontrei uma solução um pouco melhor para essa limitação da linguagem impressionante.

Em Scala, o problema do apagamento de tipo não ocorre com matrizes. Eu acho que é mais fácil demonstrar isso com um exemplo.

Digamos que temos uma lista de (Int, String), então o seguinte fornece um aviso de apagamento de tipo

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

Para contornar isso, primeiro crie uma classe de caso:

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

então, na correspondência de padrões, faça algo como:

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

o que parece funcionar perfeitamente.

Isso exigirá pequenas alterações no seu código para trabalhar com matrizes em vez de listas, mas não deve ser um grande problema.

Observe que o uso case a:Array[(Int, String)]ainda emitirá um aviso de apagamento de tipo; portanto, é necessário usar uma nova classe de contêiner (neste exemplo IntString).

Jus12
fonte
10
"limitação da linguagem impressionante" é menos uma limitação do Scala e mais uma limitação da JVM. Talvez o Scala pudesse ter sido projetado para incluir informações de tipo, como era executada na JVM, mas não acho que um design como esse tivesse preservado a interoperabilidade com o Java (ou seja, conforme projetado, você pode chamar o Scala de Java.)
Carl G
11
Como acompanhamento, o suporte a genéricos reificados para Scala no .NET / CLR é uma possibilidade contínua.
Carl G
6

Como o Java não conhece o tipo de elemento real, achei mais útil apenas usar List[_]. Então o aviso desaparece e o código descreve a realidade - é uma lista de algo desconhecido.

rained_in
fonte
4

Gostaria de saber se esta é uma solução adequada:

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

Não corresponde ao caso "lista vazia", ​​mas gera um erro de compilação, não um aviso!

error: type mismatch;
found:     String
requirerd: Int

Por outro lado, isso parece funcionar ....

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

Não é ainda melhor ou estou perdendo o objetivo aqui?

agilesteel
fonte
3
Não funciona com List (1, "a", "b"), que tem tipo List [Qualquer]
sullivan-
11
Embora o argumento de sullivan esteja correto e haja problemas relacionados à herança, eu ainda achei isso útil.
S23
0

Eu queria adicionar uma resposta que generalize o problema para: Como obter uma representação String do tipo da minha lista em tempo de execução

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
fonte
-18

Usando guarda de correspondência de padrão

    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
fonte
4
A razão pela qual este não funcionará é que isInstanceOffaz uma verificação de tempo de execução com base nas informações de tipo disponíveis para a JVM. E essas informações de tempo de execução não conterão o argumento de tipo para List(por causa do apagamento do tipo).
Dominique Unruh