Perilaku yang berbeda untuk setiap tipe sering dirangkum dalam satu antarmuka yang sama, misalnya pengubah nilai menjadi teks atau pembanding dua nilai. Pola type class menyusun kebutuhan itu sebagai trait berparameter tipe, kemudian menyediakan implementasinya per tipe melalui given instance. Scala 3 mendukung pola ini dengan parameter konteks (context parameter) yang ditandai dengan kata using, sehingga compiler mencarikan implementasinya tanpa kode penghubung tambahan. Topik ini membangun satu type class lengkap, membahas resolusi otomatis dan summon, lalu menunjukkan pesan error ketika instance yang dibutuhkan tidak ada.
Type class sebagai trait dan instance given#
Trait TextEncoder[A] mendeklarasikan satu kemampuan, yaitu mengubah nilai bertipe A menjadi teks, tanpa menyebut tipe konkret apa pun. Implementasinya diberikan secara terpisah sebagai instance given untuk setiap tipe yang mendukungnya, dan setiap instance merupakan nilai biasa yang dikelola compiler. Pemisahan ini membuat kemampuan dapat diperluas dari luar trait, karena instance baru dapat ditambahkan untuk tipe mana pun tanpa menyentuh deklarasi aslinya. Pola type class berbeda dengan pewarisan karena kemampuan tidak menempel pada tipe datanya. Tipe Int dan String tetap tidak mengetahui keberadaan TextEncoder, sedangkan kemampuannya berada di luar tipe tersebut sebagai instance terpisah. Satu tipe juga dapat memiliki lebih dari satu instance, misalnya encoder teks biasa dan encoder yang mengabaikan huruf besar kecil, dan pemilihan instance cukup dilakukan pada lingkup pemanggilan. Program berikut mendefinisikan type class tersebut beserta tiga instance yang menyertainya.
// type class: kemampuan mengubah nilai menjadi teks diringkas dalam satu trait
trait TextEncoder[A]:
def encode(a: A): String
// instance untuk tipe dasar
given TextEncoder[Int] with
def encode(a: Int): String = a.toString
given TextEncoder[String] with
def encode(a: String): String = "\"" + a + "\""
// instance untuk List[A]: dibangun dari instance yang tersedia untuk A
given listEncoder[A](using enc: TextEncoder[A]): TextEncoder[List[A]] with
def encode(items: List[A]): String =
items.map(enc.encode).mkString("[", ", ", "]")
// parameter konteks: compiler mencarikan instance secara otomatis
def encode[A](value: A)(using enc: TextEncoder[A]): String =
enc.encode(value)
@main def encoderDemo(): Unit =
println(encode(42))
println(encode("arsip"))
// summon mengambil instance type class secara eksplisit
val listEnc = summon[TextEncoder[List[Int]]]
println(listEnc.encode(List(1, 2, 3)))# menjalankan program encoder.scala
scala-cli run encoder.scala42
"arsip"
[1, 2, 3]Dua given pertama melayani Int dan String, sedangkan listEncoder melayani List[A] dengan menuntut instance untuk A melalui using. Mekanisme ini membuat instance tersusun secara rekursif, karena [1, 2, 3] dicetak dengan meminta encoder Int untuk setiap elemennya. Tanda kutip pada "arsip" berasal dari instance TextEncoder[String] yang membungkus teksnya, sehingga keluarannya menyerupai data berformat tanpa kode serialisasi manual.
Parameter konteks dengan using#
Parameter konteks ditandai dengan kata using dan diletakkan pada daftar parameter tersendiri. Pada fungsi encode, daftar (using enc: TextEncoder[A]) tidak pernah diisi oleh pemanggil, karena compiler mencarikan instance given yang cocok dari lingkup yang tersedia. Pemanggilan encode(42) dengan demikian setara dengan menyertakan summon[TextEncoder[Int]] secara manual, tetapi tanpa kebisingan kode tambahan. Parameter konteks tetap dapat diberikan secara eksplisit apabila dibutuhkan, misalnya untuk memilih instance tertentu di atas pilihan bawaan compiler, sehingga kendali penuh tetap tersedia. Mekanisme inilah yang menggantikan kata kunci implicit pada Scala 3.
Resolusi instance mengikuti aturan yang dapat diprediksi. Compiler mencari instance dari lingkup lokal, lalu dari objek companion tipe yang bersangkutan, dan pencarian dinyatakan gagal apabila tidak ada yang cocok. Karena listEncoder menuntut TextEncoder[A], pencarian untuk List[Int] memicu pencarian lanjutan untuk Int yang langsung ditemukan. Rantai pencarian seperti ini menjadi dasar banyak pustaka Scala, misalnya encoder JSON dan parser yang disusun dari tipe-tipe kecilnya.
summon untuk pengambilan eksplisit#
Fungsi summon[T] mengambil instance type class secara eksplisit dan menghasilkan nilai bertipe T. Fungsi ini berguna ketika kode perlu menangani instance secara langsung, misalnya untuk menyimpannya dalam struktur data atau mendelegasikan pencarian dari dalam fungsi generik. Pada program di atas, summon[TextEncoder[List[Int]]] diambil ke dalam val terlebih dahulu, kemudian dipanggil tanpa pencarian ulang. Penyimpanan instance dalam val juga memisahkan pencarian dari penggunaan, sehingga kode yang mengulang pemanggilan tidak menambah beban pencarian pada setiap barisnya.
summon[TextEncoder[Int]].encode(7)Kegagalan summon juga bersifat informatif. Apabila tidak ada instance untuk tipe yang diminta, compiler menolak ekspresi itu dengan pesan yang sama seperti pencarian using yang gagal, sehingga keduanya dapat didiagnosis dengan cara yang serupa. Sifat ini menjadikan summon alat pemeriksa yang praktis, karena keberadaan instance dapat diuji langsung pada waktu kompilasi.
Error ketika instance tidak ada#
Pencarian instance adalah operasi yang dapat gagal pada waktu kompilasi. Apabila encode dipanggil dengan tipe yang belum memiliki instance given, compiler menolaknya dan menyebut tipe instance yang tidak ditemukan beserta parameter yang memintanya. Program berikut sengaja memanggil encode untuk Boolean, padahal belum ada instance untuk tipe itu.
// Program ini sengaja memuat kesalahan: tidak ada instance type class untuk Boolean
trait TextEncoder[A]:
def encode(a: A): String
def encode[A](value: A)(using enc: TextEncoder[A]): String =
enc.encode(value)
@main def encoderRusak(): Unit =
println(encode(true))# program ini memang gagal dikompilasi
scala-cli run encoder-rusak.scalaCompiling project (Scala 3.9.0, JVM (17))
[error] ./encoder-rusak.scala:9:23
[error] No given instance of type TextEncoder[Boolean] was found for parameter enc of method encode
[error] println(encode(true))
[error] ^
Error compiling project (Scala 3.9.0, JVM (17))
Compilation failedPesan No given instance of type TextEncoder[Boolean] was found menunjuk langsung pada kebutuhan yang belum terpenuhi, dan letaknya tertulis pada baris serta kolom pemanggilan. Kegagalan seperti ini terjadi sebelum program berjalan, sehingga tipe yang belum didukung tidak menyisakan error di tengah operasi. Perbaikannya sesederhana menambahkan instance yang diminta, dan instance tersebut langsung tersedia untuk seluruh kode yang menggunakan type class ini. Lokasi penempatan instance menentukan jangkauannya, karena instance yang dideklarasikan pada lingkup yang lebih luas tetap ditemukan compiler selama lingkup itu mencakup pemanggilannya.
given TextEncoder[Boolean] with
def encode(a: Boolean): String = if a then "benar" else "salah"Ringkasan#
- Type class ditulis sebagai trait berparameter tipe, sedangkan implementasinya diberikan per tipe melalui instance
givenyang dapat diperluas tanpa menyentuh deklarasi aslinya. - Parameter konteks
usingmenyerahkan pencarian instance kepada compiler, dan instance untuk tipe majemuk dapat disusun dari instance tipe penyusunnya. - Fungsi
summon[T]mengambil instance secara eksplisit dan berguna ketika instance perlu disimpan atau didelegasikan. - Instance yang tidak ditemukan menghasilkan error kompilasi
No given instance of type ..., bukan error saat program berjalan.
Referensi#
- Context Parameters — Scala 3 Book
— Bab resmi yang memperkenalkan parameter konteks
usingbeserta resolusinya. - Given Instances — Scala 3 Reference
— Dokumentasi resmi bentuk deklarasi
givendan tempat pencariannya.