Union type dan nil

Nil sebagai anggota union biasa yang ditegakkan saat compile dan cara bekerja dengannya.

Daftar isi

Banyak operasi tidak selalu menghasilkan nilai, misalnya pencarian kunci hash yang ternyata tidak ada. Crystal memodelkan ketiadaan nilai sebagai tipe Nil dan menggabungkannya dengan tipe kemungkinan lain menjadi union type, yaitu tipe gabungan yang ditulis dengan tanda |. Topik ini membahas bentuk union yang memuat Nil, bukti bahwa compiler menegakkan union tersebut saat compile, dan pengecilan tipe yang membuat pemeriksaannya tetap ringkas.

Nil sebagai tipe#

Nil adalah tipe yang hanya memiliki satu nilai, yaitu nil, dan nil ditulis sebagai kata kunci yang menyatakan ketiadaan nilai. Variabel yang diisi nil bertipe Nil, dan metode nil? mengembalikan true untuk nilai tersebut serta false untuk nilai lain, sehingga pemeriksaan eksplisit dapat dilakukan di mana pun.

crystal Contoh: nilai nil dan tipenya
kosong = nil
p typeof(kosong)
p kosong.nil?
text Keluaran
Nil
true

Tipe yang dilaporkan typeof adalah Nil, bukan tipe lain yang mirip, karena nil memang satu-satunya nilai milik tipe itu. Metode nil? pada contoh mengembalikan true sesuai harapan, dan pemanggilan yang sama pada nilai apa pun selain nil mengembalikan false. Tipe Nil juga menjadi tipe balik metode yang tidak membawa nilai kembali, sehingga variabel yang menampung hasil pemanggilan seperti puts bertipe Nil juga.

Union yang memuat Nil#

Union type menggabungkan beberapa kemungkinan tipe menjadi satu, dan bentuknya muncul sendiri ketika compiler menyimpulkan sebuah nilai yang asalnya lebih dari satu tipe. Ekspresi ternary yang satu cabangnya Int32 dan cabang lainnya nil menghasilkan gabungan keduanya, yaitu Int32 | Nil. Program berikut menunjukkan penyimpulan tersebut pada dua sumber yang umum.

crystal union-dasar.cr
# Dua sumber union yang umum

kondisi = 7 > 3
x = kondisi ? 3 : nil
p typeof(x)
p x

kamus = {"buku" => 7}
hasil = kamus["pensil"]?
p typeof(hasil)
p hasil

Jalankan berkasnya dengan perintah berikut.

bash
# jalankan program union-dasar.cr
crystal run union-dasar.cr
text Keluaran
(Int32 | Nil)
3
(Int32 | Nil)
nil

Variabel kondisi hanya diketahui compiler sebagai Bool, sehingga kedua cabang ternary tetap dihitung dan tipenya digabung menjadi Int32 | Nil. Pemanggilan kamus["pensil"]? menghasilkan union yang sama karena kunci yang dicari memang mungkin tidak ada, dan pada contoh ini hasilnya memang nil. Pola yang sama berlaku pada kontainer yang isinya tidak seragam, karena Nil ikut menjadi anggota tipenya tanpa perlakuan khusus.

crystal Contoh: Nil bergabung pada tipe array
baris = [1, nil, "dua"]
p typeof(baris)
p baris[1]
text Keluaran
Array(Int32 | String | Nil)
nil

Tipe Array(Int32 | String | Nil) menyatakan bahwa setiap elemen dapat berupa Int32, String, atau Nil. Nil dengan demikian menjadi anggota union yang setara dengan tipe lainnya, bukan penanda khusus yang diperlakukan berbeda oleh sistem tipe.

Compiler menegakkan union saat compile#

Variabel bertipe Int32 | Nil tidak boleh langsung digunakan pada operasi yang hanya dimiliki Int32, karena tidak semua anggota union menjamin operasi itu tersedia. Penegakan ini terjadi saat compile, sehingga program yang melanggar tidak pernah dijalankan. Program berikut memanggil penjumlahan pada x yang tipenya masih union.

crystal gagal-nil.cr
# Program ini ditolak compiler

kondisi = 7 > 3
x = kondisi ? 3 : nil
p x + 1

Jalankan berkasnya dengan perintah berikut, lalu amati pesan errornya.

bash
# jalankan program gagal-nil.cr
crystal run gagal-nil.cr
text Keluaran
Showing last frame. Use --error-trace for full trace.

In gagal-nil.cr:5:5

 5 | p x + 1
         ^
Error: undefined method '+' for Nil (compile-time type is (Int32 | Nil))

Pesan error menyebut compile-time type is (Int32 | Nil), yang berarti compiler mengetahui tipe lengkap variabel tersebut dan menolak pemanggilan karena anggota Nil tidak memiliki metode +. Pada bahasa yang pemeriksaannya ditunda sampai program berjalan, kegagalan semacam ini baru terlihat saat eksekusi, sedangkan compiler Crystal menangkapnya sebelum program sempat dijalankan.

Pengecilan tipe dengan if#

Pemeriksaan kebenaran if x sekaligus mengecilkan tipe x di dalam bloknya, teknik yang disebut pengecilan tipe (type narrowing). Apabila kondisinya benar, x pasti bukan nil, sehingga tipenya tinggal Int32 dan operasi apa pun milik Int32 menjadi sah. Apabila kondisinya salah, x pasti nil.

crystal pengecilan.cr
# if x mengecilkan tipe x di dalam bloknya

kondisi = 7 > 3
x = kondisi ? 3 : nil

if x
  p x + 1
else
  p "x bernilai nil"
end
text Keluaran
4

Di dalam blok if, penjumlahan x + 1 diterima tanpa konversi tipe apa pun karena compiler telah mengecilkan tipenya menjadi Int32. Kondisi pada contoh ini benar, sehingga hasilnya 4, tetapi mekanisme yang sama berlaku apabila kondisinya salah dan cabang else yang berjalan.

crystal Contoh: tipe x pada cabang else
kondisi = 3 > 5
x = kondisi ? 3 : nil

if x
  p x + 1
else
  p typeof(x)
end
text Keluaran
Nil

Cabang else melaporkan Nil karena di sanalah satu-satunya kemungkinan x. Bentuk unless x menerapkan pengecilan yang sama dengan urutan terbalik, yaitu tubuhnya mengecilkan x menjadi Nil dan cabang else-nya mengecilkan x menjadi Int32. Pengecilan juga bekerja pada bentuk postfix, sehingga pemeriksaan dan operasinya dapat ditulis satu baris.

crystal Contoh: pengecilan pada if postfix
kondisi = 7 > 3
x = kondisi ? 3 : nil

p x + 1 if x
text Keluaran
4

Kesalahan umum#

Mengoperasikan hasil yang mungkin nil tanpa pemeriksaan. Kebiasaan ini ditolak compiler sebagaimana ditunjukkan pada bagian Compiler menegakkan union saat compile. Selain memeriksa dengan if atau unless, nilai cadangan dapat disediakan dengan operator ||, yaitu nilai kedua digunakan apabila nilai pertama bernilai nil atau false.

crystal Contoh: nilai cadangan dengan ||
kamus = {"buku" => 7}
stok = kamus["pensil"]? || 0
p stok
text Keluaran
0

Mengira pengecilan berlaku di luar blok pemeriksaannya. Pengecilan tipe mengikuti alur program, sehingga di luar blok if yang mengecilkannya, tipe x kembali menjadi Int32 | Nil. Pindahkan operasi yang bergantung pada pengecilan itu ke dalam blok if, atau tampung hasil operasinya ke variabel baru di dalam blok.

Ringkasan#

  • Nil adalah tipe dengan satu nilai, yaitu nil, dan menjadi anggota union yang setara dengan tipe lainnya.
  • Ekspresi yang cabangnya memuat nil menghasilkan union seperti Int32 | Nil, dan []? mengembalikan union serupa karena kuncinya mungkin tidak ada.
  • Compiler menolak pemanggilan metode yang tidak dimiliki seluruh anggota union, sehingga kekurangan nilai tertangkap saat compile dan bukan saat program berjalan.
  • if x mengecilkan tipe di dalam bloknya, bentuk postfix menerapkan pengecilan yang sama, sedangkan || menyediakan nilai cadangan apabila nil ditemukan.

Referensi#