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.
// 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.
# menjalankan program ekstensi-dasar.scala
scala-cli run ekstensi-dasar.scalaRata-rata : 82.0
Median : 85.0
Daftar kosong, rata-rata: 0.0Keluaran 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.
// 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.
# menjalankan program ekstensi-generik.scala
scala-cli run ekstensi-generik.scalaTinggi terbesar: 182
Tinggi terkecil: 158
Kata terbesar : jeruk
Kata terkecil : apelProgram 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.
object Statistik:
extension (nilai: List[Int])
def rataRata: Double =
if nilai.isEmpty then 0.0
else nilai.sum.toDouble / nilai.size
import Statistik.rataRataTanpa 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.
| Aspek | implicit class (Scala 2) | extension (Scala 3) |
|---|---|---|
| Bentuk definisi | kelas dengan kata kunci implicit | kata kunci extension dengan parameter penerima |
| Metode dalam satu definisi | satu metode utama per kelas | beberapa metode dalam satu blok |
| Biaya eksekusi | objek pembungkus dibuat pada tiap pemanggilan | tanpa objek pembungkus |
| Dukungan generik | parameter tipe pada kelas | parameter tipe dan klausa using pada ekstensi |
implicit class StatistikInt(nilai: List[Int]):
def rataRata: Double =
if nilai.isEmpty then 0.0
else nilai.sum.toDouble / nilai.sizePada 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
extensiondapat memuat beberapa metode, dan parameter tipe beserta klausausingmenjadikannya generik dengan syarat bukti sepertiOrdering[T]. - Compiler mencari ekstensi dari lingkup leksikal, impor, dan nilai
givenyang tersedia, sehingga ekstensi di dalam objek harus diimpor lebih dahulu. - Ekstensi menggantikan
implicit classScala 2 dengan definisi yang lebih langsung dan tanpa pembuatan objek pembungkus pada tiap pemanggilan.
Referensi#
- Extension Methods — Scala 3 Reference — Aturan sintaks, terjemahan pemanggilan, dan urutan resolusi ekstensi pada referensi resmi Scala 3.
- Extension Methods — Scala 3 Book — Pengenalan ekstensi pada buku resmi Scala 3, termasuk pembandingnya dengan implicit class Scala 2.
- Implicit Conversions — Scala 3 Reference — Konversi implisit yang menjadi dasar kerja implicit class pada Scala 2.