Metode ekstensi

Menambahkan metode pada tipe milik pihak lain tanpa mewarisi kelasnya.

Daftar isi

Metode ekstensi (extension method) adalah metode yang didefinisikan di luar tubuh tipe tujuannya, tetapi dapat dipanggil seolah-olah merupakan anggota asli tipe itu. Mekanisme ini memungkinkan penambahan kemampuan baru pada tipe yang tidak dapat disunting, misalnya tipe koleksi milik pustaka bawaan, tanpa mewarisi kelasnya dan tanpa membungkus nilai aslinya. Topik ini membahas sintaks extension, penulisan beberapa metode dalam satu blok ekstensi, ekstensi generik dengan using, aturan resolusi lingkupnya, serta pembeda dari implicit class yang digunakan Scala 2.

Sintaks dasar metode ekstensi#

Definisi ekstensi diawali kata kunci extension, diikuti parameter penerima dalam tanda kurung, kemudian tubuh metodenya. Parameter penerima, misalnya nilai pada contoh berikut, hanyalah nama untuk nilai yang menerima panggilan, dan Scala tidak menggunakan kata kunci this pada posisi tersebut. Bentuk paling ringkas menempatkan satu metode setelah parameter, sedangkan beberapa metode yang sejenis dapat dikumpulkan dalam satu blok extension dengan indentasi. Kedua bentuk tersebut digunakan program berikut untuk menambahkan statistik dasar pada List[Int], yaitu tipe milik pustaka bawaan yang tubuhnya tidak dapat disunting.

scala ekstensi-dasar.scala
// Ekstensi metode pada List[Int], tipe milik pustaka bawaan
extension (nilai: List[Int])
  def rataRata: Double =
    if nilai.isEmpty then 0.0
    else nilai.sum.toDouble / nilai.size

  def median: Double =
    val urut = nilai.sorted
    val tengah = urut.size / 2
    if urut.isEmpty then 0.0
    else if urut.size % 2 == 1 then urut(tengah).toDouble
    else (urut(tengah - 1) + urut(tengah)) / 2.0

@main def ekstensiDasar(): Unit =
  val nilaiUjian = List(72, 85, 90, 68, 95)
  println("Rata-rata : " + nilaiUjian.rataRata)
  println("Median    : " + nilaiUjian.median)
  println("Daftar kosong, rata-rata: " + List.empty[Int].rataRata)

Jalankan program tersebut.

bash
# menjalankan program ekstensi-dasar.scala
scala-cli run ekstensi-dasar.scala
text Keluaran
Rata-rata : 82.0
Median    : 85.0
Daftar kosong, rata-rata: 0.0

Keluaran memperlihatkan kedua metode dipanggil dengan bentuk titik seperti metode biasa. Compiler mengolah pemanggilan itu sepenuhnya pada waktu kompilasi dan menulis ulang nilaiUjian.rataRata menjadi pemanggilan fungsi biasa dengan nilaiUjian sebagai argumennya. Tidak ada kelas pembungkus atau objek tambahan yang dibuat ketika program berjalan, sehingga biaya ekstensi sama dengan biaya pemanggilan fungsi langsung. Apabila nama yang sama memang sudah menjadi anggota tipe tujuannya, anggota asli itulah yang digunakan, karena compiler mencari anggota asli terlebih dahulu dan baru mempertimbangkan ekstensi apabila anggota itu tidak ada.

Ekstensi generik dengan using#

Ekstensi juga dapat bersifat generik dengan menuliskan parameter tipe sebelum parameter penerima. Bentuknya extension [T](nilai: List[T])(using Ordering[T]), yaitu ekstensi untuk daftar bertipe T apa pun asalkan pengurutan untuk T tersedia di lingkup. Ketersediaan itu diminta melalui klausa using, sehingga compiler menerima pemanggilan untuk tipe yang memiliki nilai Ordering[T] dan menolak pemanggilan untuk tipe yang tidak memilikinya. Pola ini menjadi fondasi type class yang dibangun dengan given dan using, yaitu pola dengan ekstensi yang perilakunya bergantung pada bukti tipe.

scala ekstensi-generik.scala
// Ekstensi generik: T apa pun asalkan bukti Ordering[T] tersedia
extension [T](nilai: List[T])(using Ordering[T])
  def terbesar: T = nilai.max
  def terkecil: T = nilai.min

@main def ekstensiGenerik(): Unit =
  val tinggi = List(170, 158, 182, 165)
  println("Tinggi terbesar: " + tinggi.terbesar)
  println("Tinggi terkecil: " + tinggi.terkecil)

  val kata = List("durian", "apel", "jeruk")
  println("Kata terbesar  : " + kata.terbesar)
  println("Kata terkecil  : " + kata.terkecil)

Jalankan program tersebut.

bash
# menjalankan program ekstensi-generik.scala
scala-cli run ekstensi-generik.scala
text Keluaran
Tinggi terbesar: 182
Tinggi terkecil: 158
Kata terbesar  : jeruk
Kata terkecil  : apel

Program di atas memanggil terbesar dan terkecil pada daftar Int sekaligus daftar String tanpa menuliskan versi tersendiri untuk tiap tipe. Yang berubah hanyalah nilai Ordering yang disiapkan compiler untuk setiap pemanggilan, sehingga satu definisi melayani semua tipe yang terurut. Apabila daftar tersebut diganti dengan tipe yang tidak memiliki Ordering, misalnya daftar case class biasa, error muncul pada waktu kompilasi dan bukan pada waktu program berjalan.

Lingkup resolusi ekstensi#

Compiler mencari ekstensi pada beberapa tempat secara berurutan. Pencarian dimulai dari lingkup leksikal (lexical scope) pemanggilan, yaitu ekstensi yang ditulis pada tingkat atas berkas yang sama. Tempat berikutnya adalah impor, sehingga ekstensi yang berada di dalam objek menjadi terlihat setelah ekstensinya diimpor. Tempat terakhir adalah nilai given yang tersedia di lingkup, dan jalur inilah yang digunakan ekstensi generik pada bagian sebelumnya. Bentuk pencarian kedua tersebut ditunjukkan fragmen berikut.

scala Contoh: ekstensi di dalam objek dan impor eksplisitnya
object Statistik:
  extension (nilai: List[Int])
    def rataRata: Double =
      if nilai.isEmpty then 0.0
      else nilai.sum.toDouble / nilai.size

import Statistik.rataRata

Tanpa baris impor, pemanggilan angka.rataRata dari luar objek menggagalkan kompilasi dengan pesan value rataRata is not a member of List[Int] yang menunjuk ke baris pemanggilannya. Perbaikannya berupa impor ekstensi itu, baik per metode sebagaimana contoh maupun untuk seluruh isi objek dengan import Statistik.*. Karena ekstensi hanya ditemukan melalui lingkup, kehati-hatian diperlukan ketika beberapa sumber impor dibawa sekaligus.

Pembeda dari implicit class#

Scala 2 menyelesaikan kebutuhan yang sama dengan implicit class, yaitu kelas biasa yang diberi kata kunci implicit sehingga compiler menyisipkan konversi ke pembungkusnya secara otomatis. Ekstensi menggantikan pola tersebut dengan definisi yang lebih langsung dan tanpa kelas pembungkus sama sekali. Perbedaan keduanya dirangkum pada tabel berikut.

Aspekimplicit class (Scala 2)extension (Scala 3)
Bentuk definisikelas dengan kata kunci implicitkata kunci extension dengan parameter penerima
Metode dalam satu definisisatu metode utama per kelasbeberapa metode dalam satu blok
Biaya eksekusiobjek pembungkus dibuat pada tiap pemanggilantanpa objek pembungkus
Dukungan generikparameter tipe pada kelasparameter tipe dan klausa using pada ekstensi
scala Contoh: implicit class pada Scala 2
implicit class StatistikInt(nilai: List[Int]):
  def rataRata: Double =
    if nilai.isEmpty then 0.0
    else nilai.sum.toDouble / nilai.size

Pada bentuk Scala 2 di atas, setiap pemanggilan rataRata menyembunyikan pembuatan satu objek StatistikInt baru, sedangkan bentuk extension memanggil fungsinya secara langsung. Pesan error compiler juga lebih jelas pada ekstensi, karena compiler menyebut kemungkinan metode ekstensi beserta impor yang dapat memperbaikinya apabila ada. Bagi kode Scala 2 yang masih menggunakan implicit class, migrasinya lazimnya berupa penggantian kelas pembungkus dengan satu blok extension yang berisi metode-metode yang sama.

Ringkasan#

  • Metode ekstensi didefinisikan dengan extension (nilai: T) def ... dan dipanggil seperti metode asli tipe T, tanpa mewarisi kelasnya dan tanpa kelas pembungkus.
  • Satu blok extension dapat memuat beberapa metode, dan parameter tipe beserta klausa using menjadikannya generik dengan syarat bukti seperti Ordering[T].
  • Compiler mencari ekstensi dari lingkup leksikal, impor, dan nilai given yang tersedia, sehingga ekstensi di dalam objek harus diimpor lebih dahulu.
  • Ekstensi menggantikan implicit class Scala 2 dengan definisi yang lebih langsung dan tanpa pembuatan objek pembungkus pada tiap pemanggilan.

Referensi#