Kode yang sama sering dibutuhkan untuk banyak tipe, misalnya wadah yang menyimpan teks maupun angka, atau fungsi yang menjumlahkan luas berbagai bentuk. Parameter tipe (type parameter) menyelesaikan kebutuhan tersebut dengan menulis kode satu kali, sedangkan compiler menyempurnakan tipenya untuk setiap penggunaan. Topik ini membahas kelas dan metode generik, batas tipe atas, anotasi kovarians dan kontravarians, serta aturan posisi yang menentukan letak anotasi yang sah. Setiap konsep disertai program utuh yang dikompilasi, termasuk pesan error compiler ketika aturan posisi dilanggar.
Kelas dan metode generik#
Parameter tipe adalah nama tipe yang dideklarasikan dalam tanda kurung siku, misalnya A pada Box[A], lalu diganti dengan tipe nyata ketika kode digunakan. Satu deklarasi kelas generik sudah melayani banyak variasi tipe, sehingga duplikasi kode untuk setiap tipe tidak diperlukan. Compiler Scala juga melakukan inferensi tipe (type inference) atas argumen tipe tersebut, tetapi penulisan tipe secara eksplisit sering membantu keterbacaan pada deklarasi val.
Program berikut mendeklarasikan kelas generik Box[A], mengisinya dengan dua tipe berbeda, lalu menjumlahkan luas bentuk melalui metode generik yang dibahas pada bagian berikutnya.
// kelas generik: parameter tipe A diisi ketika instance dibuat
case class Box[A](value: A)
// hirarki bentuk untuk contoh batas tipe
trait Shape:
def area: Double
case class Square(side: Double) extends Shape:
def area: Double = side * side
case class Circle(radius: Double) extends Shape:
def area: Double = math.Pi * radius * radius
// batas tipe atas: A wajib Shape atau turunannya
def totalArea[A <: Shape](shapes: List[A]): Double =
shapes.map(_.area).sum
@main def totalAreaDemo(): Unit =
val archived: Box[String] = Box("arsip 2026")
val counted: Box[Int] = Box(42)
println(archived)
println(counted)
println(totalArea(List(Square(3), Square(4))))
println(totalArea(List(Square(3), Circle(1))))# menjalankan program total-area.scala
scala-cli run total-area.scalaBox(arsip 2026)
Box(42)
25.0
12.141592653589793Keluaran memperlihatkan dua wadah dengan tipe berbeda dari satu deklarasi yang sama. Nilai archived bertipe Box[String], sedangkan counted bertipe Box[Int], dan keduanya menggunakan tubuh kelas yang sama tanpa salinan kode. Dua baris terakhir dihasilkan oleh metode generik totalArea yang menjadi contoh batas tipe pada bagian berikutnya.
Batas tipe atas dengan <:#
Metode totalArea[A <: Shape] membatasi parameter tipenya dengan batas tipe atas (upper type bound), yaitu notasi <: yang menyatakan bahwa A wajib berupa Shape atau salah satu turunannya. Batas ini bukan sekadar dokumentasi, melainkan syarat agar tubuh metode dapat dikompilasi. Ekspresi _.area di dalam tubuhnya hanya sah apabila compiler tahu bahwa setiap A memiliki anggota area. Apabila batas dihapus, compiler menolak metode itu karena A tanpa batas bisa berupa tipe apa pun, termasuk tipe tanpa anggota area sama sekali.
Batas atas juga memengaruhi cara compiler menyimpulkan argumen tipe koleksi. Daftar List(Square(3), Square(4)) menghasilkan A = Square, sedangkan daftar campuran List(Square(3), Circle(1)) diperlakukan sebagai List[Shape] karena Shape adalah leluhur bersama keduanya. Kedua daftar tetap diterima oleh satu metode totalArea yang sama, dan keluaran 25.0 serta 12.141592653589793 menunjukkan hasil penjumlahan luasnya.
Kovarians dengan anotasi +A#
Kovarians (covariance) adalah sifat yang membuat hubungan subtipenya mengikuti arah parameter tipe. Anotasi +A pada Storage menyatakan bahwa apabila Circle adalah Shape, maka Storage[Circle] juga merupakan Storage[Shape]. Tanpa anotasi apa pun, parameter tipe bersifat invarian dan hubungan itu tidak berlaku. Program berikut menggunakan hierarki bentuk yang sama untuk menunjukkan dua arah varians sekaligus, yaitu wadah penyimpanan yang kovarian dan petugas pemeriksa yang kontravarian.
// hirarki bentuk yang sama dengan contoh sebelumnya
trait Shape:
def area: Double
case class Square(side: Double) extends Shape:
def area: Double = side * side
case class Circle(radius: Double) extends Shape:
def area: Double = math.Pi * radius * radius
// kovarians: Storage[Circle] dapat dipakai sebagai Storage[Shape]
class Storage[+A](val item: A)
// kontravarians: Inspector[Shape] dapat dipakai sebagai Inspector[Circle]
trait Inspector[-A]:
def inspect(item: A): String
val shapeInspector: Inspector[Shape] = new Inspector[Shape]:
def inspect(item: Shape): String = s"Luas: ${item.area}"
@main def variansDemo(): Unit =
// posisi keluaran: wadah yang lebih khusus ditempatkan sebagai wadah umum
val anyStorage: Storage[Shape] = Storage(Circle(1))
println(anyStorage.item.area)
// posisi masukan: pemeriksa yang lebih umum menggantikan pemeriksa khusus
val circleInspector: Inspector[Circle] = shapeInspector
println(circleInspector.inspect(Circle(2)))# menjalankan program varians.scala
scala-cli run varians.scala3.141592653589793
Luas: 12.566370614359172Pada baris pertama program, nilai Storage(Circle(1)) yang bertipe Storage[Circle] ditempatkan langsung pada val bertipe Storage[Shape]. Penempatan itu sah karena A dianotasi kovarian, dan pembacaan anyStorage.item.area aman karena nilai yang dikeluarkan wadah pasti memiliki anggota area. Wadah kovarian memang hanya boleh mengeluarkan nilai, bukan menerima nilai baru, sebagaimana dibatasi aturan posisi.
Kontravarians dengan anotasi -A#
Kontravarians (contravariance) membalik arah hubungan subtipenya. Anotasi -A pada Inspector menyatakan bahwa Inspector[Shape] merupakan subtipe dari Inspector[Circle], yaitu kebalikan dari arah Circle <: Shape. Alasannya dapat ditelusuri dengan mudah: petugas yang sanggup memeriksa sembarang Shape otomatis sanggup memeriksa Circle, sedangkan petugas khusus Circle belum tentu mampu menangani bentuk lain. Baris kedua program di atas memanfaatkan sifat itu, karena shapeInspector yang bertipe Inspector[Shape] diterima pada val bertipe Inspector[Circle] tanpa penyesuaian apa pun.
Kontravarians lazim dijumpai pada tipe yang berperilaku sebagai konsumen, misalnya petugas pemeriksa atau pembanding, sedangkan kovarians lazim pada tipe yang berperilaku sebagai produsen, misalnya sumber data. Kedua anotasi tersebut digunakan compiler sebagai dasar pemeriksaan yang lebih ketat, yaitu aturan posisi yang dibahas pada bagian berikutnya.
Aturan posisi dan error compiler#
Aturan posisi (variance position) menentukan letak parameter tipe yang sah. Parameter tipe kovarian +A hanya boleh muncul pada posisi keluaran, yaitu hasil metode dan val seperti item pada Storage. Parameter tipe kontravarian -A hanya boleh muncul pada posisi masukan, yaitu parameter metode seperti item pada inspect. Parameter tipe tanpa anotasi boleh muncul di mana saja, dengan konsekuensi kehilangan hubungan subtipenya. Aturan ini menjamin keamanan tipe secara menyeluruh, bukan sekadar formalitas sintaksis.
Program berikut sengaja melanggar aturan itu. Metode replace menerima A sebagai parameter, padahal A pada Storage dianotasi kovarian dan parameter metode merupakan posisi masukan.
// Program ini sengaja memuat kesalahan: parameter tipe kovarian dipakai pada posisi masukan
class Storage[+A](val item: A):
def replace(item: A): Storage[A] = Storage(item)
@main def variansRusak(): Unit =
println(Storage("arsip").replace("baru"))# program ini memang gagal dikompilasi
scala-cli run varians-rusak.scalaCompiling project (Scala 3.9.0, JVM (17))
[error] ./varians-rusak.scala:3:15
[error] covariant type A occurs in contravariant position in type A of parameter item
[error] def replace(item: A): Storage[A] = Storage(item)
[error] ^^^^^^^
Error compiling project (Scala 3.9.0, JVM (17))
Compilation failedPesan covariant type A occurs in contravariant position menyatakan pelanggarannya secara harfiah, lengkap dengan baris dan kolom letaknya. Pelarangan ini mencegah kesalahan logika yang sulit dilacak. Apabila replace diperbolehkan, Storage[Circle] dapat ditempatkan sebagai Storage[Shape], kemudian replace(Square(4)) menyelipkan persegi ke dalam penyimpanan lingkaran tanpa satu pun peringatan. Pembacaan item kelak menghasilkan Square yang dianggap Circle, sehingga error baru muncul saat program berjalan. Compiler Scala memotong seluruh rangkaian itu sejak awal.
Ringkasan#
- Parameter tipe ditulis dalam tanda kurung siku dan diganti dengan tipe nyata pada setiap penggunaan, sehingga satu kelas atau satu metode melayani banyak tipe.
- Batas tipe atas
A <: Shapemenjamin anggotaShapetersedia padaAdan memungkinkan daftar campuran diperlakukan sebagaiList[Shape]. - Anotasi
+Amembuat hubungan subtipenya mengikuti arah parameter tipe, sedangkan-Amembaliknya, sehinggaStorage[Circle]merupakanStorage[Shape]danInspector[Shape]merupakanInspector[Circle]. - Parameter tipe kovarian hanya sah pada posisi keluaran dan kontravarian hanya pada posisi masukan, sedangkan pelanggarannya ditolak compiler dengan pesan yang menunjuk letak kesalahannya.
Referensi#
- Generic Classes — Scala Documentation — Mengenalkan kelas generik dengan parameter tipe pada Tour of Scala.
- Variances — Scala Documentation — Menjelaskan anotasi kovarians dan kontravarians beserta posisi yang diizinkan.
- Upper Type Bounds — Scala Documentation
— Membahas batas tipe atas
<:beserta akibatnya pada anggota yang dapat digunakan.