Tipe union (union type) mengizinkan satu variabel menyimpan nilai dari beberapa tipe, tetapi compiler hanya menerima pemanggilan metode yang dimiliki semua anggota union tersebut. Agar metode milik satu anggota tetap dapat dipanggil, compiler melakukan pengecilan tipe (type narrowing), yaitu penyempitan tipe variabel di dalam cabang tertentu berdasarkan pemeriksaan yang ditulis dalam program. Topik ini membahas pengecilan melalui is_a?, responds_to?, dan aturan implisit untuk union yang memuat Nil, beserta batas pengecilan ketika variabel diberi nilai baru.
Pengecilan dengan is_a?#
Metode is_a? memeriksa apakah sebuah nilai merupakan instance dari tipe tertentu. Compiler memanfaatkan pemeriksaan itu untuk mengecilkan tipe variabel: di dalam cabang benar, variabel diperlakukan sebagai tipe yang diuji, sedangkan cabang else menyisakan anggota union yang tersisa. Pengecilan terjadi saat kompilasi dan tidak mengubah nilai apa pun.
typeof melaporkan tipe variabel pada titik pemanggilannya menurut compiler, sehingga berguna untuk melihat hasil pengecilan. Program berikut memeriksa dua elemen dari sebuah array campuran:
def tampilkan(x : Int32 | String)
puts typeof(x)
if x.is_a?(String)
puts typeof(x)
puts x.upcase
else
puts typeof(x)
puts x + 2
end
end
arsip = ["kristal", 2026]
tampilkan(arsip[0])
tampilkan(arsip[1])(Int32 | String)
String
KRISTAL
(Int32 | String)
Int32
2028Array arsip bertipe Array(String | Int32), sehingga hasil indeksnya adalah union (Int32 | String) pada kedua pemanggilan. Keluaran typeof menuliskan union di dalam tanda kurung. Pemanggilan pertama membawa nilai "kristal", sehingga cabang is_a?(String) dieksekusi dan tipe x di dalamnya menjadi String, yang membuat pemanggilan upcase diterima compiler. Pemanggilan kedua membawa 2026, sehingga cabang else dieksekusi dan tipe x menjadi Int32, yang membuat operasi x + 2 dapat dihitung. Pengecilan tidak mengubah nilai apa pun, hanya cara compiler memandang variabel pada titik itu.
Pengecilan dengan responds_to?#
Pemeriksaan tipe tidak selalu perlu menyebut tipe secara eksplisit. Metode responds_to? mengecilkan union berdasarkan keberadaan metode: cabang benar menyisakan anggota yang memiliki metode yang diuji, sedangkan cabang else menyisakan anggota yang tidak memilikinya. Pendekatan ini berguna apabila pembeda yang penting adalah kemampuan nilai, bukan tipe persisnya.
def tampilkan(y : Int32 | String)
puts typeof(y)
if y.responds_to?(:size)
puts typeof(y)
puts "panjang: #{y.size}"
else
puts typeof(y)
puts "angka: #{y}"
end
end
arsip = ["kristal", 2026]
tampilkan(arsip[0])
tampilkan(arsip[1])(Int32 | String)
String
panjang: 7
(Int32 | String)
Int32
angka: 2026Tipe y di cabang benar menjadi String karena hanya String yang memiliki metode size di dalam union tersebut, sedangkan cabang else menyisakan Int32. Apabila lebih dari satu anggota memiliki metode yang diuji, compiler menyisakan seluruh anggota tersebut di cabang benar dan pemanggilan metode itu tetap sah untuk semuanya. Karena yang diperiksa adalah keberadaan metode, pengecilan ini tetap bekerja pada union yang anggotanya tidak berasal dari hierarki tipe yang sama.
Pengecilan implisit pada union dengan Nil#
Union yang memuat Nil mendapat pengecilan tanpa pemeriksaan tambahan. Apabila variabel digunakan sebagai kondisi if, compiler mengecilkan variabel menjadi anggota selain Nil pada cabang benar, dan menjadi Nil pada cabang else. Aturan ini mengikuti kebenaran nilai (truthiness), yaitu perlakuan compiler terhadap nilai yang dianggap kosong: Nil dan false dianggap tidak benar, sedangkan nilai lainnya dianggap benar.
def cari(kata : Array(String), target : String) : String | Nil
kata.find { |k| k == target }
end
def tampilkan(hasil : String | Nil)
puts typeof(hasil)
if hasil
puts typeof(hasil)
puts hasil.upcase
else
puts typeof(hasil)
end
end
tampilkan(cari(["apel", "jeruk", "mangga"], "jeruk"))
tampilkan(cari(["apel", "jeruk", "mangga"], "pir"))(String | Nil)
String
JERUK
(String | Nil)
NilKeluaran memperlihatkan tipe hasil sebelum dan sesudah pengecilan untuk dua kasus. Kata jeruk ditemukan, sehingga cabang benar menampung String dan upcase dipanggil. Kata pir tidak ada dalam array, sehingga cabang else menampung Nil dan tidak ada metode yang dipanggil.
Pengecilan yang hilang saat variabel diubah ulang#
Pengecilan melekat pada posisi variabel di dalam alur program, bukan pada namanya untuk selamanya. Apabila variabel diberi nilai baru di dalam cabang, compiler menilai variabel dari tipe nilai barunya, dan pengecilan yang berlaku sebelum penugasan tidak digunakan lagi. Program berikut menunjukkan pergantian tipe tersebut di dalam cabang is_a?:
arsip = ["kristal", 2026]
x = arsip[0]
if x.is_a?(String)
x = x.size
puts typeof(x)
puts x + 1
endInt32
8Di dalam cabang, x semula String hasil pengecilan. Setelah penugasan x = x.size, tipe x menjadi Int32, sehingga typeof melaporkan Int32 dan penjumlahan x + 1 diterima. Sifat yang sama membuat pemanggilan metode milik tipe lama ditolak setelah penugasan:
arsip = ["kristal", 2026]
x = arsip[0]
if x.is_a?(String)
x = x.size
puts x.upcase
endShowing last frame. Use --error-trace for full trace.
In pengecilan-ubah-error.cr:6:10
6 | puts x.upcase
^-----
Error: undefined method 'upcase' for Int32Compiler menolak program tersebut karena pada baris keenam tipe x sudah Int32, sedangkan upcase hanya dimiliki String. Pesan error menyebut Int32 tanpa konteks union karena tipe x pada titik itu memang sudah bukan union. Untuk menyimpan nilai baru dengan tipe berbeda, gunakan variabel terpisah agar pengecilan pada variabel lama tetap berlaku.
Kesalahan umum#
Kesalahan pertama adalah memanggil metode yang tidak dimiliki semua anggota union. Compiler menolaknya saat kompilasi, sebelum program berjalan:
arsip = ["kristal", 2026]
x = arsip[0]
puts x.upcaseShowing last frame. Use --error-trace for full trace.
In union-error.cr:3:8
3 | puts x.upcase
^-----
Error: undefined method 'upcase' for Int32 (compile-time type is (Int32 | String))Pesan error menyebut Int32 sebagai anggota yang tidak memiliki metode upcase, ditambah catatan bahwa tipe saat kompilasi adalah union (Int32 | String). Catatan union itu tidak muncul lagi apabila tipe variabel pada titik yang sama sudah bukan union, misalnya setelah variabel diberi nilai baru. Perbaikannya adalah mengecilkan union terlebih dahulu dengan is_a? atau responds_to? sebelum metode khusus dipanggil, sebagaimana ditunjukkan pada bagian-bagian sebelumnya.
Kesalahan kedua adalah menganggap pengecilan bertahan setelah isi variabel berubah. Penyebabnya, pengecilan mengikuti alur data, sehingga setiap penugasan mengganti tipe variabel dengan tipe nilai barunya. Perbaikannya, simpan nilai baru pada variabel lain, atau tulis kembali pemeriksaan is_a? setelah penugasan apabila pemeriksaan itu memang diperlukan kembali.
Ringkasan#
is_a?mengecilkan union di dalam cabangnya: cabang benar bertipe yang diuji, sedangkan cabang else menyisakan anggota lainnya.responds_to?mengecilkan union berdasarkan keberadaan metode, tanpa menyebut tipe secara eksplisit.if xpada union yang memuatNilmengecilkan variabel ke anggota selainNildi cabang benar dan keNildi cabang else.- Pengecilan hilang begitu variabel diberi nilai baru di dalam cabang, sehingga metode milik tipe lama dapat ditolak compiler.
Referensi#
- Referensi bahasa Crystal 1.22 — Crystal — Dokumentasi resmi sintaks dan semantik bahasa, termasuk aturan pengecilan tipe pada percabangan.
- Dokumentasi API Crystal 1.22.1 — Crystal
— Rujukan kelas dan metode pustaka standar, termasuk
Array#findyang bertipe kembalianT | Nil.