Pengecilan union

Mempersempit union type dengan is_a?, responds_to?, dan pengecilan implisit compiler.

Daftar isi

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:

crystal pengecilan-is-a.cr
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])
text Keluaran
(Int32 | String)
String
KRISTAL
(Int32 | String)
Int32
2028

Array 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.

crystal pengecilan-responds.cr
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])
text Keluaran
(Int32 | String)
String
panjang: 7
(Int32 | String)
Int32
angka: 2026

Tipe 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.

crystal pengecilan-nil.cr
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"))
text Keluaran
(String | Nil)
String
JERUK
(String | Nil)
Nil

Keluaran 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?:

crystal pengecilan-ubah.cr
arsip = ["kristal", 2026]
x = arsip[0]

if x.is_a?(String)
  x = x.size
  puts typeof(x)
  puts x + 1
end
text Keluaran
Int32
8

Di 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:

crystal pengecilan-ubah-error.cr
arsip = ["kristal", 2026]
x = arsip[0]

if x.is_a?(String)
  x = x.size
  puts x.upcase
end
text Keluaran
Showing last frame. Use --error-trace for full trace.

In pengecilan-ubah-error.cr:6:10

 6 | puts x.upcase
            ^-----
Error: undefined method 'upcase' for Int32

Compiler 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:

crystal union-error.cr
arsip = ["kristal", 2026]
x = arsip[0]
puts x.upcase
text Keluaran
Showing 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 x pada union yang memuat Nil mengecilkan variabel ke anggota selain Nil di cabang benar dan ke Nil di cabang else.
  • Pengecilan hilang begitu variabel diberi nilai baru di dalam cabang, sehingga metode milik tipe lama dapat ditolak compiler.

Referensi#