Jak ominąć wymazywanie tekstu na Scali? Lub dlaczego nie mogę uzyskać parametru typu moich kolekcji?

370

Smutnym faktem z życia Scali jest to, że jeśli tworzysz List [Int], możesz sprawdzić, czy twoja instancja jest Listą, i możesz zweryfikować, czy każdy jej pojedynczy element to Int, ale nie to, że jest Listą [ Int], jak można łatwo zweryfikować:

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

Opcja -unchecked obciąża winę za kasowanie typu:

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

Dlaczego tak jest i jak sobie z tym poradzić?

Daniel C. Sobral
źródło
Scala 2.8 Beta 1 RC4 właśnie wprowadziła pewne zmiany w sposobie wymazywania typu. Nie jestem pewien, czy to bezpośrednio wpływa na twoje pytanie.
Scott Morrison,
1
To jest właśnie to, co rodzaje wymazanie do , który został zmieniony. Krótko mówiąc, można go podsumować jako „ Propozycja: usunięcie„ Object with A ”to„ A ”zamiast„ Object ”. Rzeczywista specyfikacja jest raczej bardziej złożona. W każdym razie chodzi o mixiny, a to pytanie dotyczy generyków.
Daniel C. Sobral,
Dzięki za wyjaśnienie - jestem nowicjuszem Scala. Czuję, że teraz jest zły moment, aby wskoczyć do Scali. Wcześniej mogłem nauczyć się zmian w 2.8 z dobrej bazy, później nigdy nie musiałbym znać różnicy!
Scott Morrison,
1
Oto nieco powiązane pytanie dotyczące TypeTags .
pvorb,
2
Podczas pracy scala 2.10.2zobaczyłem to ostrzeżenie: <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?") ^uważam, że twoje pytanie i odpowiedź są bardzo pomocne, ale nie jestem pewien, czy to zaktualizowane ostrzeżenie jest przydatne dla czytelników.
Kevin Meredith

Odpowiedzi:

243

W tej odpowiedzi użyto opcji Manifest-API, która jest przestarzała od wersji Scala 2.10. Więcej odpowiedzi znajdziesz poniżej.

Scala została zdefiniowana za pomocą Type Erasure, ponieważ Java Virtual Machine (JVM), w przeciwieństwie do Javy, nie otrzymywała generycznych. Oznacza to, że w czasie wykonywania istnieje tylko klasa, a nie parametry jej typu. W tym przykładzie JVM wie, że obsługuje a scala.collection.immutable.List, ale nie że ta lista jest sparametryzowana Int.

Na szczęście w Scali jest funkcja umożliwiająca obejście tego. To manifest . Manifest to klasa, której instancje są obiektami reprezentującymi typy. Ponieważ te instancje są obiektami, możesz je przekazywać, przechowywać i ogólnie wywoływać na nich metody. Dzięki obsłudze niejawnych parametrów staje się bardzo potężnym narzędziem. Weźmy na przykład następujący przykład:

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

Podczas przechowywania elementu przechowujemy również „Manifest”. Manifest to klasa, której instancje reprezentują typy Scali. Te obiekty mają więcej informacji niż JVM, co pozwala nam przetestować pełny, sparametryzowany typ.

Zauważ jednak, że a Manifestjest wciąż rozwijającą się funkcją. Jako przykład swoich ograniczeń, obecnie nie wie nic o wariancji i zakłada, że ​​wszystko jest wariantem alternatywnym. Oczekuję, że stanie się bardziej stabilny i solidny, gdy skończy się biblioteka refleksji Scala, która jest obecnie w fazie rozwoju.

Daniel C. Sobral
źródło
3
getSposób może być określony jako for ((om, v) <- _map get key if om <:< m) yield v.asInstanceOf[T].
Aaron Novstrup,
4
@Aaron Bardzo dobra sugestia, ale obawiam się, że może to zaciemnić kod dla osób stosunkowo nowych w Scali. Sam nie miałem dużego doświadczenia ze Scalą, kiedy napisałem ten kod, co było kiedyś, zanim umieściłem go w tym pytaniu / odpowiedzi.
Daniel C. Sobral,
6
@KimStebel Wiesz, że w TypeTagrzeczywistości są one automatycznie używane przy dopasowywaniu wzorców? Fajnie, co?
Daniel C. Sobral
1
Fajne! Może powinieneś dodać to do odpowiedzi.
Kim Stebel
1
Aby odpowiedzieć na moje pytanie powyżej: Tak, kompilator generuje Manifestsam parametr, patrz: stackoverflow.com/a/11495793/694469 „instancja [manifest / type-tag] [...] jest niejawnie tworzona przez kompilator „
KajMagnus,
96

Możesz to zrobić za pomocą TypeTags (jak już wspomina Daniel, ale ja to wyraźnie przeliteruję):

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

Możesz to również zrobić za pomocą ClassTags (co oszczędza Ci konieczności polegania na 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 można stosować, o ile nie oczekuje się, że Asam parametr type będzie typem ogólnym.

Niestety jest to trochę szczegółowe i potrzebujesz adnotacji @unchecked, aby ukryć ostrzeżenie kompilatora. W przyszłości TypeTag może zostać automatycznie włączony przez kompilator do dopasowania wzorca: https://issues.scala-lang.org/browse/SI-6517

tksfz
źródło
2
A co z usuwaniem niepotrzebnych, [List String @unchecked]ponieważ nie dodaje nic do tego wzorca dopasowania (samo użycie case strlist if typeOf[A] =:= typeOf[String] =>spowoduje to, a nawet case _ if typeOf[A] =:= typeOf[String] =>jeśli powiązana zmienna nie jest potrzebna w treści case).
Nader Ghanbari
1
Myślę, że to zadziałałoby w podanym przykładzie, ale myślę, że większość rzeczywistych zastosowań skorzystałaby z tego rodzaju elementów.
tksfz
Czy w powyższych przykładach niezaznaczona część przed stanem wartownika nie wykonuje rzutu? Czy nie dostaniesz wyjątku rzutowania klasą podczas przeglądania dopasowań pierwszego obiektu, który nie może być rzutowany na ciąg?
Toby
Hm nie, wierzę, że nie ma rzutowania przed zastosowaniem wartownika - niezaznaczony bit jest rodzajem braku operacji, dopóki kod po prawej stronie nie =>zostanie wykonany. (A kiedy kod na rhs jest wykonywany, strażnicy zapewniają statyczną gwarancję na rodzaj elementów. Może tam być rzut, ale jest bezpieczny.)
tksfz
Czy to rozwiązanie powoduje znaczne obciążenie środowiska wykonawczego?
stanislav.chetvertkov
65

Możesz użyć Typeableklasy typu od bezkształtnego, aby uzyskać oczekiwany wynik,

Przykładowa sesja 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))

castOperacja nie jest tak precyzyjny wrt usunięcia możliwie biorąc pod uwagę należących do zakresu Typeableprzykłady dostępnych.

Miles Sabin
źródło
14
Należy zauważyć, że operacja „rzutowania” rekurencyjnie przejdzie przez całą kolekcję i jej podkolekcje i sprawdzi, czy wszystkie zaangażowane wartości są odpowiedniego typu. (To l1.cast[List[String]]znaczy z grubsza for (x<-l1) assert(x.isInstanceOf[String]) W przypadku dużych struktur danych lub jeśli rzutowania zdarzają się bardzo często, może to być niedopuszczalne obciążenie ogólne.
Dominique Unruh
16

Wymyśliłem stosunkowo proste rozwiązanie, które wystarczyłoby w sytuacjach o ograniczonym użyciu, zasadniczo owijając sparametryzowane typy, które ucierpiałyby z powodu problemu z usunięciem typu w klasach opakowań, które mogą być użyte w instrukcji match.

case class StringListHolder(list:List[String])

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

Daje to oczekiwany wynik i ogranicza zawartość naszej klasy przypadków do pożądanego typu, Listy ciągów.

Więcej informacji tutaj: http://www.scalafied.com/?p=60

thricejamie
źródło
14

Istnieje sposób na rozwiązanie problemu usuwania typu w Scali. W opcji Pokonywanie wymazywania typu w dopasowaniu 1 i Pokonywanie wymazywania typu w dopasowywaniu 2 (wariancja) znajduje się wyjaśnienie, w jaki sposób zakodować niektóre pomocniki do zawijania typów, w tym wariancji, w celu dopasowania.

oś osiowa
źródło
To nie przezwycięża kasowania typu. W swoim przykładzie robi val x: Any = List (1,2,3); x match {case IntList (l) => println (s "Match $ {l (1)}"); case _ => println (s „No match”)}
zwraca
możesz zobaczyć makra scala 2.10.
Alex
11

Znalazłem nieco lepsze obejście tego ograniczenia skądinąd niesamowitego języka.

W Scali problem usuwania typu nie występuje w przypadku tablic. Myślę, że łatwiej jest to zademonstrować na przykładzie.

Powiedzmy, że mamy listę (Int, String), a następnie poniżej podano ostrzeżenie o usunięciu typu

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

Aby obejść ten problem, najpierw utwórz klasę sprawy:

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

następnie we wzorcu dopasowania wykonaj coś takiego:

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

który wydaje się działać idealnie.

Będzie to wymagało drobnych zmian w kodzie do pracy z tablicami zamiast listami, ale nie powinno to stanowić poważnego problemu.

Zauważ, że użycie case a:Array[(Int, String)]nadal da ostrzeżenie o usunięciu typu, dlatego konieczne jest użycie nowej klasy kontenera (w tym przykładzie IntString).

Jus12
źródło
10
„ograniczenie skądinąd niesamowitego języka” to mniej ograniczenie Scali, a bardziej ograniczenie JVM. Być może Scala mogła zostać zaprojektowana w taki sposób, aby zawierała informacje o typie działające na JVM, ale nie sądzę, aby taki projekt zachowałby interoperacyjność z Javą (tj. Zgodnie z projektem można wywoływać Scalę z Javy).
Carl G
1
Jako kontynuacja, wsparcie dla zmienionych generycznych dla Scali w .NET / CLR jest stałą możliwością.
Carl G
6

Ponieważ Java nie zna faktycznego typu elementu, uznałem, że najbardziej użyteczne jest po prostu użycie List[_]. Potem ostrzeżenie znika, a kod opisuje rzeczywistość - jest to lista czegoś nieznanego.

rained_in
źródło
4

Zastanawiam się, czy jest to odpowiednie obejście:

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

Nie pasuje do „pustej listy”, ale daje błąd kompilacji, a nie ostrzeżenie!

error: type mismatch;
found:     String
requirerd: Int

To z drugiej strony wydaje się działać ....

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

Czy to nie jest nawet lepsze, czy nie rozumiem tutaj tego?

agilesteel
źródło
3
Nie działa z List (1, „a”, „b”), który ma typ List [Dowolny]
sullivan-
1
Chociaż punkt Sullivana jest prawidłowy i istnieją powiązane problemy z dziedziczeniem, nadal uważam to za przydatne.
Seth
0

Chciałem dodać odpowiedź, która uogólnia problem: Jak uzyskać ciąg reprezentujący typ mojej listy w czasie wykonywania

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
źródło
-18

Korzystanie ze wzoru dopasowania dopasowania

    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
źródło
4
Powodem, dla którego ten nie działa, jest isInstanceOfsprawdzenie środowiska wykonawczego na podstawie informacji o typie dostępnych dla JVM. I te informacje o środowisku wykonawczym nie będą zawierać argumentu typu do List(z powodu usunięcia typu).
Dominique Unruh