Generics adalah mekanisme penulisan class dan metode yang bekerja terhadap banyak tipe dengan menjadikan perbedaan tipe sebagai parameter. Crystal menggunakan mekanisme ini pada struktur data di standard library, mulai dari array hingga Hash dan NamedTuple. Topik ini membahas generics bawaan, class generik buatan sendiri, instansiasi dengan inferensi tipe, metode generik, dan batasan compiler pada kode generik. Setelah membaca topik ini, tanda tangan tipe generik pada dokumentasi API dapat dibaca dan class generik sederhana dapat ditulis.
Generik pada standard library#
Class generik (generic class) adalah class yang menerima tipe sebagai argumen, ditulis dalam tanda kurung setelah nama class-nya. Array, Hash, dan NamedTuple adalah contohnya pada standard library. Array menyimpan elemen dengan satu parameter tipe yang lazim ditulis T, sedangkan Hash memerlukan dua parameter, yaitu K untuk tipe kunci dan V untuk tipe nilai. Nama lengkap hasil instansiasi terlihat dengan memanggil typeof terhadap nilainya.
angka = [1, 2, 3]
kamus = {"satu" => 1, "dua" => 2}
catatan = {judul: "laporan", halaman: 12}
puts typeof(angka)
puts typeof(kamus)
puts typeof(catatan)Array(Int32)
Hash(String, Int32)
NamedTuple(judul: String, halaman: Int32)Keluaran tersebut menunjukkan tipe lengkap dari ketiga nilai. Array literal [1, 2, 3] menghasilkan Array(Int32) karena semua elemennya bertipe Int32, sedangkan kamus dengan kunci string dan nilai bilangan bulat menghasilkan Hash(String, Int32). NamedTuple bahkan mencantumkan nama dan tipe setiap kolomnya. Parameter tipe membuat satu definisi class tetap aman tipe untuk banyak kombinasi tipe, dan nama lengkap inilah yang digunakan dokumentasi API untuk menjelaskan setiap class generik.
Class generik buatan sendiri#
Class generik buatan sendiri ditulis dengan menambahkan parameter tipe pada deklarasi class-nya. Definisi berikut membuat class Kotak yang menyimpan satu nilai bertipe T. Parameter tipe T digunakan pada empat tempat, yaitu tipe variabel instance @isi, tipe argumen initialize, tipe kembalian metode isi, dan tipe argumen metode isi=.
# Kotak menyimpan satu nilai bertipe T
class Kotak(T)
def initialize(@isi : T)
end
def isi : T
@isi
end
def isi=(isi_baru : T)
@isi = isi_baru
end
end
kotak = Kotak(Int32).new(42)
puts typeof(kotak)
puts kotak.isi
kotak.isi = 100
puts kotak.isi
kotak_teks = Kotak(String).new("catatan")
puts typeof(kotak_teks)
puts kotak_teks.isiKotak(Int32)
42
100
Kotak(String)
catatanPemanggilan Kotak(Int32).new(42) mengikat T ke Int32, sedangkan Kotak(String).new("catatan") mengikat T ke String. Keluaran typeof memperlihatkan bahwa keduanya menghasilkan tipe konkret yang berbeda, yaitu Kotak(Int32) dan Kotak(String). Karena setter isi= menerima argumen bertipe T, penugasan kotak.isi = "teks" pada Kotak(Int32) ditolak compiler dengan pesan ketidakcocokan tipe. Satu definisi class dengan demikian melayani banyak tipe tanpa mengorbankan pemeriksaan tipe.
Instansiasi dengan inferensi tipe#
Argumen tipe tidak selalu harus ditulis secara eksplisit. Apabila initialize menerima argumen, compiler menyimpulkan parameternya dari tipe argumen yang dikirim, mekanisme yang disebut inferensi tipe (type inference). Pemanggilan Kotak.new(42) pada program berikut menghasilkan class yang sama dengan Kotak(Int32).new(42).
class Kotak(T)
def initialize(@isi : T)
end
def isi : T
@isi
end
end
kotak = Kotak.new(42)
puts typeof(kotak)Kotak(Int32)Keluaran typeof menegaskan bahwa hasil inferensinya adalah Kotak(Int32). Bentuk eksplisit dan bentuk inferensi menghasilkan tipe yang sama, sehingga pemilihannya adalah perkara kejelasan kode. Bentuk eksplisit lazim digunakan ketika kode perlu menegaskan tipe yang diharapkan, sedangkan bentuk inferensi membuat baris pemanggilan lebih pendek. Apabila argumen yang dikirim tidak cocok dengan tipe yang ditulis eksplisit, compiler menolaknya sebagaimana ditunjukkan pada bagian Kesalahan umum.
Metode generik dengan forall#
Metode juga dapat memiliki parameter tipe sendiri yang berlaku per pemanggilan. Parameternya dideklarasikan dengan kata kunci forall yang ditulis setelah tanda tangan metode, seperti forall T pada definisi berikut. Pembatas Array(T) mengikat T ke tipe elemen array yang dikirim, sehingga satu definisi melayani array dengan tipe elemen apa pun. Tipe kembalian metode juga boleh merujuk T yang sama.
def ambil_pertama(xs : Array(T)) : T forall T
xs.first
end
puts ambil_pertama([3, 8, 5])
puts typeof(ambil_pertama([3, 8, 5]))
puts ambil_pertama(["tiga", "delapan"])
puts typeof(ambil_pertama(["tiga", "delapan"]))3
Int32
tiga
StringKeluaran typeof menunjukkan bahwa T terikat ke Int32 pada pemanggilan pertama dan ke String pada pemanggilan kedua. Nilai kembalian mengikuti tipe elemen array tanpa konversi apa pun. Penulisan forall tidak boleh dilewati karena compiler menolak parameter tipe yang tidak dideklarasikan dengan pesan undefined constant T, sebagaimana ditunjukkan pada bagian Kesalahan umum.
Batasan compiler pada parameter tipe#
Kode di dalam class generik dikompilasi untuk setiap tipe yang menginstansiasinya. Akibatnya, setiap metode dan operator yang dipanggil terhadap nilai bertipe T wajib tersedia pada semua tipe pemakaiannya. Definisi berikut menambahkan metode jumlah yang menghitung @isi + 1. Metode ini sah untuk Kotak(Int32), tetapi compiler menolaknya ketika class diinstansiasi dengan String karena String tidak mendefinisikan operator + untuk argumen Int32.
class Kotak(T)
def initialize(@isi : T)
end
def jumlah : T
@isi + 1
end
end
kotak = Kotak(String).new("catatan")
puts kotak.jumlahShowing last frame. Use --error-trace for full trace.
In error_jumlah.cr:6:12
6 | @isi + 1
^
Error: expected argument #1 to 'String#+' to be Char or String, not Int32
Overloads are:
- String#+(other : self)
- String#+(char : Char)Pesan error tersebut berasal dari pemeriksaan overload String#+, yaitu operator + milik String, yang tidak menerima argumen Int32. Kompilasi hanya berhasil apabila pemanggilan semacam itu dihapus atau tipe argumen metode dibatasi ke tipe yang mendukungnya. Nama berkas pada baris pertama pesan mengikuti nama berkas yang dikompilasi, sehingga baris tersebut boleh berbeda pada lingkungan lain.
Kesalahan umum#
Dua kesalahan berikut lazim muncul ketika class dan metode generik pertama kali ditulis, dan keduanya terdeteksi compiler sebelum program dijalankan. Masing-masing disertai penyebab dan perbaikannya.
Kesalahan pertama adalah menambahkan anotasi tipe kembalian : T tanpa mendeklarasikan parameter tipe dengan forall. Penyebabnya, compiler belum menganggap T sebagai parameter tipe metode, sehingga T diperlakukan sebagai konstanta yang tidak ada. Perbaikannya, tambahkan forall T setelah tanda tangan metode sebagaimana pada bagian Metode generik dengan forall.
def ambil_pertama(xs : Array(T)) : T
xs.first
end
puts ambil_pertama([3, 8, 5])Showing last frame. Use --error-trace for full trace.
In error_metode.cr:1:30
1 | def ambil_pertama(xs : Array(T)) : T
^
Error: undefined constant TKesalahan kedua adalah mengirim argumen yang tidak cocok dengan parameter tipe yang ditulis eksplisit. Pada pemanggilan berikut, Kotak(Int32) menuntut argumen Int32, sedangkan yang dikirim adalah String. Compiler menolaknya sebelum program dijalankan. Perbaikannya, samakan tipe argumen dengan parameter tipe yang ditulis, atau hilangkan penulisan eksplisitnya agar inferensi tipe yang menentukan.
class Kotak(T)
def initialize(@isi : T)
end
end
kotak = Kotak(Int32).new("teks")
puts kotak.isiShowing last frame. Use --error-trace for full trace.
In error_instansiasi.cr:6:26
6 | kotak = Kotak(Int32).new("teks")
^
Error: expected argument #1 to 'Kotak(Int32).new' to be Int32, not String
Overloads are:
- Kotak(T).new(isi : T)Ringkasan#
Array,Hash, danNamedTupleadalah class generik bawaan yang parameter tipenya terlihat pada nama lengkapnya, misalnyaArray(Int32)danHash(String, Int32).- Class generik buatan sendiri dideklarasikan dengan parameter tipe seperti
class Kotak(T), dan setiap instansiasi menghasilkan tipe konkret tersendiri sepertiKotak(Int32). - Argumen tipe dapat disimpulkan compiler dari argumen
initialize, sedangkan metode generik mendeklarasikan parameternya denganforall. - Kode generik diperiksa compiler untuk setiap tipe yang menginstansiasinya, sehingga metode yang menggunakan operator tertentu hanya sah untuk tipe yang mendukung operator tersebut.
Referensi#
- Dokumentasi bahasa Crystal 1.22 — crystal-lang.org — Rujukan sintaks dan semantik bahasa, termasuk bab generics.
- Referensi API Crystal 1.22.1 — crystal-lang.org
— Dokumentasi API standard library, termasuk
Array,Hash, danNamedTuple.