Bagaimana cara mengatasi penghapusan tipe di Scala? Atau, mengapa saya tidak bisa mendapatkan parameter tipe koleksi saya?

370

Ini adalah fakta kehidupan yang menyedihkan di Scala bahwa jika Anda membuat Instansiate Daftar [Int], Anda dapat memverifikasi bahwa instance Anda adalah Daftar, dan Anda dapat memverifikasi bahwa setiap elemen individu itu adalah Int, tetapi bukan bahwa itu adalah Daftar [ Int], karena dapat dengan mudah diverifikasi:

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

Opsi -terperiksa menempatkan kesalahan pada penghapusan tipe:

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

Mengapa begitu, dan bagaimana cara mengatasinya?

Daniel C. Sobral
sumber
Scala 2.8 Beta 1 RC4 baru saja membuat beberapa perubahan pada cara kerja tipe penghapusan. Saya tidak yakin apakah ini secara langsung memengaruhi pertanyaan Anda.
Scott Morrison
1
Itulah yang jenis penghapusan untuk , yang telah berubah. Singkatnya dapat diringkas sebagai " Proposal: Penghapusan" Obyek dengan A "adalah" A ", bukan" Objek ". " Spesifikasi aktual agak lebih kompleks. Ini tentang mixin, bagaimanapun juga, dan pertanyaan ini berkaitan dengan obat generik.
Daniel C. Sobral
Terima kasih atas klarifikasi - Saya seorang pendatang baru scala. Saya merasa saat ini adalah waktu yang buruk untuk melompat ke Scala. Sebelumnya, saya bisa mempelajari perubahan di 2.8 dari basis yang baik, kemudian saya tidak akan pernah tahu bedanya!
Scott Morrison
1
Berikut pertanyaan yang agak terkait tentang TypeTags .
pvorb
2
Sedang berjalan scala 2.10.2, saya melihat peringatan ini: <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?") ^Saya menemukan pertanyaan dan jawaban Anda sangat membantu, tetapi saya tidak yakin apakah peringatan yang diperbarui ini bermanfaat bagi pembaca.
Kevin Meredith

Jawaban:

243

Jawaban ini menggunakan Manifest-API, yang sudah usang pada Scala 2.10. Silakan lihat jawaban di bawah ini untuk solusi terkini.

Scala didefinisikan dengan Tipe Erasure karena Java Virtual Machine (JVM), tidak seperti Java, tidak mendapatkan obat generik. Ini berarti bahwa, pada saat dijalankan, hanya kelas yang ada, bukan parameter tipenya. Dalam contoh, JVM tahu itu menangani scala.collection.immutable.List, tetapi tidak dengan daftar ini parameternya Int.

Untungnya, ada fitur di Scala yang memungkinkan Anda menyiasatinya. Itu Manifes . Manifest adalah kelas yang instansnya adalah objek yang mewakili tipe. Karena instance ini adalah objek, Anda dapat menyebarkannya, menyimpannya, dan biasanya memanggil metode. Dengan dukungan parameter implisit, itu menjadi alat yang sangat kuat. Ambil contoh berikut, misalnya:

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

Saat menyimpan elemen, kami menyimpan "Manifes" juga. Manifest adalah kelas yang instansnya mewakili tipe Scala. Objek-objek ini memiliki informasi lebih banyak daripada JVM, yang memungkinkan kita untuk menguji tipe parameterisasi penuh.

Perhatikan, bagaimanapun, bahwa a Manifestmasih merupakan fitur yang berkembang. Sebagai contoh keterbatasannya, saat ini tidak tahu apa-apa tentang varians, dan menganggap semuanya ko-varian. Saya berharap ini akan menjadi lebih stabil dan kokoh setelah perpustakaan refleksi Scala, saat ini sedang dalam pengembangan, selesai.

Daniel C. Sobral
sumber
3
The getMetode dapat didefinisikan sebagai for ((om, v) <- _map get key if om <:< m) yield v.asInstanceOf[T].
Aaron Novstrup
4
@ Harun Saran yang sangat bagus, tapi saya khawatir itu mungkin mengaburkan kode untuk orang yang relatif baru ke Scala. Saya sendiri tidak terlalu berpengalaman dengan Scala ketika saya menulis kode itu, yang beberapa saat sebelum saya letakkan di pertanyaan / jawaban ini.
Daniel C. Sobral
6
@ KimStebel Anda tahu bahwa TypeTagsebenarnya secara otomatis digunakan pada pencocokan pola? Keren kan?
Daniel C. Sobral
1
Keren! Mungkin Anda harus menambahkan itu ke jawabannya.
Kim Stebel
1
Untuk menjawab pertanyaan saya sendiri tepat di atas: Ya, kompiler menghasilkan Manifestparam itu sendiri, lihat: stackoverflow.com/a/11495793/694469 "instance [manifest / type-tag] [...] sedang dibuat secara implisit oleh kompiler "
KajMagnus
96

Anda dapat melakukan ini menggunakan TypeTags (seperti yang sudah Daniel sebutkan, tapi saya hanya akan menjabarkannya secara eksplisit):

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

Anda juga dapat melakukan ini menggunakan ClassTags (yang menyelamatkan Anda dari ketergantungan pada 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 dapat digunakan selama Anda tidak mengharapkan parameter tipe Amenjadi tipe generik.

Sayangnya itu sedikit bertele-tele dan Anda membutuhkan penjelasan @eckecked untuk menekan peringatan kompiler. TypeTag dapat dimasukkan ke dalam pencocokan pola secara otomatis oleh kompiler di masa mendatang: https://issues.scala-lang.org/browse/SI-6517

tksfz
sumber
2
Bagaimana dengan menghapus yang tidak perlu [List String @unchecked]karena tidak menambahkan apa pun ke pencocokan pola ini (Hanya menggunakan case strlist if typeOf[A] =:= typeOf[String] =>akan melakukannya, atau bahkan case _ if typeOf[A] =:= typeOf[String] =>jika variabel terikat tidak diperlukan di dalam tubuh case).
Nader Ghanbari
1
Saya kira itu akan bekerja untuk contoh yang diberikan tetapi saya pikir sebagian besar penggunaan nyata akan mendapat manfaat dari memiliki jenis elemen.
tksfz
Dalam contoh di atas, bukankah bagian yang tidak diperiksa di depan kondisi penjaga melakukan pemeran? Tidakkah Anda mendapatkan pengecualian pemeran kelas saat melewati pertandingan pada objek pertama yang tidak dapat dilemparkan ke string?
Toby
Hm tidak, saya percaya tidak ada pemeran sebelum menerapkan penjaga - bit yang tidak dicentang adalah semacam no-op sampai kode di sebelah kanan =>dieksekusi. (Dan ketika kode pada rhs dieksekusi, penjaga memberikan jaminan statis pada jenis elemen. Mungkin ada pemain di sana, tapi aman.)
tksfz
Apakah solusi ini menghasilkan overhead runtime yang signifikan?
stanislav.chetvertkov
65

Anda bisa menggunakan Typeablekelas tipe dari tak berbentuk untuk mendapatkan hasil yang Anda cari,

Contoh sesi 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))

The castoperasi akan menjadi seperti tepat wrt penghapusan mungkin mengingat di-lingkup Typeablekasus yang tersedia.

Miles Sabin
sumber
14
Perlu dicatat bahwa operasi "pemain" akan secara rekursif melalui seluruh koleksi dan subkoleksi dan memeriksa apakah semua nilai yang terlibat adalah dari jenis yang tepat. (Yaitu, l1.cast[List[String]]kira-kira for (x<-l1) assert(x.isInstanceOf[String]) Untuk struktur data besar atau jika gips sering terjadi, ini mungkin overhead yang tidak dapat diterima.
Dominique Unruh
16

Saya datang dengan solusi yang relatif sederhana yang akan mencukupi dalam situasi penggunaan terbatas, pada dasarnya membungkus tipe parameter yang akan menderita dari masalah penghapusan tipe dalam kelas wrapper yang dapat digunakan dalam pernyataan pertandingan.

case class StringListHolder(list:List[String])

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

Ini memiliki output yang diharapkan dan membatasi konten kelas kasus kami ke tipe yang diinginkan, Daftar String.

Lebih detail di sini: http://www.scalafied.com/?p=60

tiga kali lipat jamie
sumber
14

Ada cara untuk mengatasi masalah penghapusan tipe di Scala. Dalam Mengatasi Jenis Penghapusan dalam pencocokan 1 dan Mengatasi Penghapusan Jenis dalam Pencocokan 2 (Varians) adalah beberapa penjelasan tentang cara membuat kode beberapa pembantu untuk membungkus jenis, termasuk Variance, untuk pencocokan.

axaluss
sumber
Ini tidak mengatasi penghapusan tipe. Dalam contohnya, melakukan val x: Any = Daftar (1,2,3); x match {case IntList (l) => println (s "Match $ {l (1)}"); case _ => println (s "No match")} menghasilkan "No match"
user48956
Anda bisa melihat makro scala 2.10.
Alex
11

Saya menemukan solusi yang sedikit lebih baik untuk pembatasan bahasa yang tidak biasa ini.

Dalam Scala, masalah penghapusan tipe tidak terjadi dengan array. Saya pikir lebih mudah untuk menunjukkan ini dengan sebuah contoh.

Katakanlah kita memiliki daftar (Int, String), lalu yang berikut ini memberikan peringatan penghapusan tipe

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

Untuk mengatasinya, pertama-tama buat kelas kasus:

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

maka dalam pencocokan pola lakukan sesuatu seperti:

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

yang tampaknya bekerja dengan sempurna.

Ini akan membutuhkan perubahan kecil dalam kode Anda untuk bekerja dengan array, bukan daftar, tetapi seharusnya tidak menjadi masalah besar.

Perhatikan bahwa menggunakan case a:Array[(Int, String)]masih akan memberikan peringatan penghapusan tipe, sehingga perlu menggunakan kelas kontainer baru (dalam contoh ini, IntString).

Jus12
sumber
10
"batasan bahasa yang tidak biasa" itu bukan batasan Scala dan lebih merupakan batasan JVM. Mungkin Scala bisa dirancang untuk memasukkan informasi tipe saat dijalankan pada JVM, tapi saya tidak berpikir desain seperti itu akan menjaga interoperabilitas dengan Java (yaitu, seperti yang dirancang, Anda dapat memanggil Scala dari Jawa.)
Carl G
1
Sebagai tindak lanjut, dukungan untuk generik reified untuk Scala di .NET / CLR adalah kemungkinan yang berkelanjutan.
Carl G
6

Karena Java tidak tahu tipe elemen yang sebenarnya, saya merasa paling bermanfaat untuk digunakan saja List[_]. Lalu peringatan hilang dan kode menggambarkan realitas - itu adalah daftar sesuatu yang tidak diketahui.

rained_in
sumber
4

Saya ingin tahu apakah ini solusi yang cocok:

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

Itu tidak cocok dengan kasus "daftar kosong", tetapi memberikan kesalahan kompilasi, bukan peringatan!

error: type mismatch;
found:     String
requirerd: Int

Ini di sisi lain tampaknya berhasil ....

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

Bukankah ini agak lebih baik atau apakah saya kehilangan poin di sini?

agilesteel
sumber
3
Tidak bekerja dengan List (1, "a", "b"), yang memiliki tipe List [Any]
sullivan-
1
Meskipun poin sullivan benar dan ada masalah terkait dengan warisan, saya masih menemukan ini berguna.
Seth
0

Saya ingin menambahkan jawaban yang membuat masalah: Bagaimana cara mendapatkan representasi String dari tipe daftar saya saat runtime

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

Menggunakan penjaga pola pertandingan

    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
sumber
4
Alasan mengapa yang ini tidak akan berfungsi adalah yang isInstanceOfmelakukan pemeriksaan runtime berdasarkan pada tipe informasi yang tersedia untuk JVM. Dan informasi runtime itu tidak akan berisi argumen type to List(karena tipe erasure).
Dominique Unruh