Tipe opaque dan alias

Pembungkus tanpa biaya runtime yang menyembunyikan representasi di dalam lingkup modulnya.

Daftar isi

Sistem tipe Scala menyediakan cara untuk memberi nama tipe yang lebih bermakna tanpa harus mendefinisikan kelas baru. Topik ini membahas dua mekanisme untuk keperluan tersebut, yaitu alias tipe (type alias) dan opaque type. Pembahasan dimulai dari alias tipe beserta keterbatasan transparansinya, dilanjutkan dengan pendefinisian opaque type di dalam objek, operasi pembuat dan pengurai, jaminan yang diberlakukan compiler, serta perbandingannya dengan pembungkus berbasis case class.

Alias tipe dengan kata kunci type#

Alias tipe memberi nama baru kepada tipe yang sudah ada melalui kata kunci type. Nama baru tersebut dapat digunakan di mana pun tipe aslinya digunakan, sehingga penandaan maksud seperti satuan pengukuran menjadi lebih mudah dibaca. Pendefinisian ini tidak menghasilkan tipe baru bagi compiler, melainkan hanya nama alternatif yang selalu merujuk pada tipe dasarnya.

scala Contoh: alias tipe transparan
type Meter = Int

val jarak: Meter = 120
val angka: Int   = jarak  // sah, Meter diterima sebagai Int
val lagi:  Meter = 42 + 7 // sah, Int diterima sebagai Meter

Kedua arah penugasan pada contoh di atas diterima compiler tanpa peringatan. Kesetaraan dua arah inilah yang menjadi batas utama alias tipe. Fungsi yang menuntut Meter tetap menerima Int apa pun, dan dua kuantitas dengan satuan berbeda tetap dapat saling dijumlahkan apabila tipe dasarnya sama. Bagi compiler, Meter dan Int memang satu tipe yang sama, sehingga kekeliruan satuan tidak mungkin tertangkap pada pemeriksaan tipe.

Kegunaan alias tetap nyata untuk hal lain, yaitu memendekkan nama tipe yang panjang dan memberi konteks pada tanda tangan (signature) fungsi, misalnya type DaftarPegawai = List[Pegawai]. Alias juga digunakan untuk menyederhanakan tipe parameter yang bertumpuk. Proteksi terhadap kekeliruan semantik bukanlah pekerjaan alias tipe.

Opaque type di dalam objek#

Opaque type dinyatakan dengan kata kunci opaque di depan sebuah alias tipe dan hanya boleh menjadi anggota objek, class, atau trait. Di dalam lingkup tempat tipe itu didefinisikan, sifat transparan tetap berlaku, sehingga representasinya dapat ditulis dan dibaca secara langsung. Di luar lingkup tersebut, compiler memperlakukan tipe itu sebagai tipe abstrak tanpa hubungan apa pun dengan representasinya. Pendekatan ini memberi makna baru pada tipe dasar dengan biaya waktu berjalan yang setara dengan representasi aslinya.

Program berikut menempatkan Suhu sebagai opaque type di dalam objek satuan. Metode apply pada objek pendampingnya menjadi pintu masuk, sedangkan metode ekstensi nilai menjadi pintu keluar.

scala tipe-opaque.scala
// Program demonstrasi tipe opaque untuk satuan suhu.
object satuan:

  // Di dalam objek satuan, Suhu transparan dan identik dengan Double.
  opaque type Suhu = Double

  object Suhu:
    // Pembuat: membungkus Double menjadi Suhu.
    def apply(nilai: Double): Suhu = nilai

    extension (s: Suhu)
      // Pengurai: membuka kembali nilai Double-nya.
      def nilai: Double = s
      def +(lain: Suhu): Suhu = s + lain
      def naik(delta: Double): Suhu = s + delta

end satuan

@main def aplikasi(): Unit =
  import satuan.*

  val pagi  = Suhu(24.0)
  val siang = pagi.naik(6.5)
  println(s"Pagi   : ${pagi.nilai} derajat")
  println(s"Siang  : ${siang.nilai} derajat")
  println(s"Total  : ${(pagi + siang).nilai} derajat")
bash
scala-cli run tipe-opaque.scala
text Keluaran
Pagi   : 24.0 derajat
Siang  : 30.5 derajat
Total  : 54.5 derajat

Keluaran di atas diperoleh dari nilai Double yang melintasi batas objek hanya melalui apply dan nilai. Di dalam objek satuan, ekspresi s + lain pada metode ekstensi merupakan penjumlahan Double biasa karena compiler masih melihat representasinya. Di luar objek, penjumlahan bawaan itu tidak lagi tersedia, dan satu-satunya operasi yang dapat dipanggil adalah yang dinyatakan secara eksplisit oleh satuan. Dengan demikian, aturan tentang nilai yang sah ditentukan oleh pemilik objek, bukan oleh tipe dasarnya.

Operasi pembuat dan pengurai#

Karena lingkup luar tidak mengetahui representasi Suhu, jalan masuk dan keluar nilainya harus disediakan secara eksplisit. Objek pendamping (companion object) Suhu menyediakan apply sebagai pembuat, yaitu metode yang menerima Double dan mengembalikannya sebagai Suhu. Metode ekstensi (extension method) menyediakan arah sebaliknya beserta operasi tambahan. Kombinasi ini membentuk antarmuka lengkap bagi satu tipe di dalam satu objek.

Pemisahan peran antara tipe dan besaran terlihat pada metode naik. Perubahan suhu bukanlah suhu, sehingga parameternya sengaja dinyatakan sebagai Double, sedangkan hasilnya dikembalikan sebagai Suhu. Sebaliknya, metode + menuntut Suhu pada kedua operannya, sehingga suhu tidak dapat dijumlahkan dengan besaran lain secara diam-diam. Dengan pembagian seperti ini, compiler dapat menolak pencampuran satuan sebelum program dijalankan.

scala Contoh: pembuatan dan penguraian nilai Suhu
val pagi = Suhu(24.0)   // pembuat: Double masuk sebagai Suhu
val baca = pagi.nilai   // pengurai: Suhu keluar sebagai Double

Nilai yang keluar melalui nilai berupa Double biasa, sehingga kode penelepon bebas memperlakukannya sebagai angka setelah batas objek dilalui. Apabila pembuatan nilai perlu dibatasi, misalnya menolak suhu di bawah nol mutlak, logika pemeriksaan cukup ditulis di dalam apply. Seluruh pengguna di luar objek otomatis mengikuti aturan tersebut karena tidak ada jalur lain untuk membuat Suhu.

Jaminan lingkup dan pesan error compiler#

Jaminan kerahasiaan representasi diberlakukan compiler sebelum program berjalan, bukan oleh pemeriksaan pada waktu berjalan. Apabila kode di luar objek satuan memperlakukan Suhu seolah-olah Double, compiler menolak programnya. Berikut adalah berkas terpisah yang salah beserta pesan asli yang dihasilkan compiler ketika berkas tersebut dikompilasi.

scala Contoh: penggunaan di luar lingkup objek
object satuan:
  opaque type Suhu = Double
  object Suhu:
    def apply(nilai: Double): Suhu = nilai

object uji:
  val dingin: Double = satuan.Suhu(24.0)
text Pesan error
[error] ./tipe-salah.scala:7:24
[error] Found:    satuan.Suhu
[error] Required: Double
[error]   val dingin: Double = satuan.Suhu(24.0)
[error]                        ^^^^^^^^^^^^^^^^^
Error compiling project (Scala 3.9.0, JVM (17))
Compilation failed

Pesan Found: satuan.Suhu dan Required: Double menunjukkan bahwa di luar objek, compiler tidak lagi mengenal kesetaraan yang hanya berlaku di dalam satuan. Penolakan berlaku pada kedua arah, yaitu pemberian Suhu pada posisi Double maupun pemberian Double mentah pada posisi Suhu tanpa melalui apply. Jaminan ini bersifat statis, sehingga program yang sudah benar tidak dikenakan biaya pemeriksaan apa pun pada waktu berjalan.

Perbandingan dengan case class pembungkus#

Cara konvensional untuk memberi identitas pada kumpulan nilai adalah case class, misalnya final case class Suhu(nilai: Double). Pendekatan ini menghasilkan tipe yang benar-benar baru, lengkap dengan field, pencocokan pola, dan kesetaraan struktural. Biayanya muncul pada waktu berjalan, karena setiap nilai memerlukan satu instance objek pembungkus yang dialokasikan secara tersendiri.

scala Contoh: pembungkus case class
final case class Suhu(nilai: Double)

val a = Suhu(24.0)
val b = Suhu(24.0)
// a == b bernilai true karena kesetaraan struktural,
// tetapi a dan b adalah dua instance yang berbeda.

Tabel berikut merangkum perbedaan kedua pendekatan pada aspek yang paling berpengaruh.

Aspekopaque typecase class pembungkus
Alokasi pada waktu berjalantidak ada, nilai tetap berupa Doublesatu instance baru untuk setiap nilai
Nama tipe pada JVMDouble tanpa nama barukelas Suhu yang berdiri sendiri
Akses representasihanya melalui anggota objekfield nilai dapat diakses dari mana pun
Pencocokan pola terhadap representasitidak tersedia di luar lingkuptersedia

Pilihan antara keduanya ditentukan oleh kebutuhan. Case class dipilih apabila representasi memang bagian dari antarmuka publik, misalnya untuk serialisasi (serialization) dan pencocokan pola. Opaque type dipilih apabila makna barunya penting tetapi representasinya harus menjadi urusan internal modul, sebagaimana Meter, pegangan ID pegawai, atau KodePos yang semuanya bertumpu pada representasi sederhana.

Ringkasan#

  • Alias tipe (type) memberi nama alternatif yang transparan, sehingga tidak memberikan proteksi terhadap kekeliruan satuan.
  • Opaque type menyembunyikan representasinya di luar objeknya, sedangkan di dalam lingkupnya representasi tetap dapat ditulis langsung.
  • Pintu masuk dan keluar nilai dinyatakan secara eksplisit melalui apply dan metode ekstensi pada objek pendamping.
  • Compiler menolak penggunaan opaque sebagai tipe dasarnya di luar lingkupnya, dan nilai yang beredar pada waktu berjalan tidak dibungkus objek baru.

Referensi#