Nilai balik nilable

Memilih antara error berbasis union dan raise sebagai keputusan desain API.

Daftar isi

Pencarian data yang tidak selalu menemukan hasil menuntut sebuah API memilih bentuk laporan ketakhadiran. Topik ini membandingkan dua gaya pada Crystal, yaitu nilai balik nilable berupa union yang menuntut penanganan dari pemanggil dan exception yang dilontarkan ketika kontrak dilanggar. Kedua gaya didemonstrasikan pada program katalog yang sama sehingga perbedaan bentuk API, kewajiban pemanggil, dan keluarannya dapat dibandingkan secara langsung. Setelah membaca topik ini, pemilihan gaya untuk API pencarian dapat dilakukan dengan alasan desain yang jelas.

Gaya nilable dengan union#

Gaya pertama menyatakan ketakhadiran sebagai nilai. Tanda tangan def cari(id : Int32) : Barang | Nil menyatakan bahwa hasilnya berupa Barang atau nil. Hash mendukung gaya ini melalui akses dengan tanda tanya, yaitu KATALOG[id]? yang mengembalikan nil ketika kuncinya tidak ada. Tanpa tanda tanya, akses hash yang gagal melontarkan KeyError, sehingga anotasi union dan pilihan metode harus saling menguatkan.

crystal katalog-nilable.cr
# Gaya nilable: nilai balik berupa union Barang | Nil.
# Jalankan dengan: crystal katalog-nilable.cr

struct Barang
  getter id : Int32
  getter nama : String

  def initialize(@id : Int32, @nama : String)
  end
end

KATALOG = {
  1 => Barang.new(1, "Pensil"),
  2 => Barang.new(2, "Buku tulis"),
}

def cari(id : Int32) : Barang | Nil
  KATALOG[id]?
end

def tampilkan(id : Int32)
  barang = cari(id)
  if barang
    puts "ditemukan: #{barang.nama}"
  else
    puts "barang dengan id #{id} tidak ada"
  end
end

tampilkan(1)
tampilkan(2)
tampilkan(9)
bash
# menjalankan program katalog nilable
crystal katalog-nilable.cr
text Keluaran
ditemukan: Pensil
ditemukan: Buku tulis
barang dengan id 9 tidak ada

Pemanggil wajib menangani kedua kemungkinan sebelum properti barang digunakan. Percabangan if barang mengecilkan tipe variabel menjadi Barang di dalam cabangnya, sehingga pemanggilan barang.nama lolos pemeriksaan compiler. Cabang else menangani ketakhadiran secara eksplisit, dan dua permintaan yang ditemukan serta satu permintaan yang tidak ada semuanya melewati jalur yang sama tanpa exception.

Anotasi Barang | Nil pada tanda tangan bukan sekadar dokumentasi. Compiler membaca anotasi tersebut dan menuntut setiap pemanggil mengakui kedua kemungkinan sebelum properti Barang digunakan. Nilai nil juga berbeda dari string kosong atau angka nol karena menyatakan ketakhadiran secara eksplisit, sedangkan string kosong masih merupakan isi yang sah dari sebuah katalog.

Gaya raise untuk pelanggaran kontrak#

Gaya kedua menyatakan keberadaan hasil sebagai kontrak. Tanda tangan def cari!(id : Int32) : Barang menjanjikan Barang selalu dikembalikan, dan tanda seru pada nama metode menjadi penanda bahwa pelanggaran kontrak dilaporkan dengan exception. Ketakhadiran bukan lagi nilai yang mungkin, melainkan keadaan luar biasa yang hanya terjadi ketika asumsi pemanggil keliru.

crystal katalog-raise.cr
# Gaya raise: pelanggaran kontrak dinyatakan dengan exception.
# Jalankan dengan: crystal katalog-raise.cr

struct Barang
  getter id : Int32
  getter nama : String

  def initialize(@id : Int32, @nama : String)
  end
end

class BarangTidakDitemukan < Exception
  getter id : Int32

  def initialize(@id : Int32)
    super("barang dengan id #{@id} tidak ditemukan")
  end
end

KATALOG = {
  1 => Barang.new(1, "Pensil"),
  2 => Barang.new(2, "Buku tulis"),
}

def cari!(id : Int32) : Barang
  KATALOG.fetch(id) { raise BarangTidakDitemukan.new(id) }
end

# pemanggil yang menganggap ketakhadiran sebagai keadaan luar biasa
[1, 9].each do |id|
  begin
    barang = cari!(id)
    puts "diproses: #{barang.nama}"
  rescue e : BarangTidakDitemukan
    puts "dilewati: #{e.message}"
  end
end
bash
# menjalankan program katalog berbasis raise
crystal katalog-raise.cr
text Keluaran
diproses: Pensil
dilewati: barang dengan id 9 tidak ditemukan

Class BarangTidakDitemukan membawa id yang tidak ditemukan sehingga pesannya spesifik. Exception khusus semacam ini dapat membawa konteks kegagalan sebagai properti, sehingga pemanggil yang menangkapnya langsung memperoleh penyebabnya tanpa pemeriksaan ulang. Metode fetch pada hash menerima blok yang dijalankan hanya ketika kunci tidak ada, sehingga jalur berhasil tidak membuat exception sama sekali. Pemanggil menangkapnya dengan begin dan rescue ketika ketakhadiran perlu dikelola, atau membiarkan exception naik ke atas ketika kegagalan memang harus menghentikan proses. Pilihan antara menangkap dan membiarkan naik bergantung pada lapisan kode, misalnya lapisan antarmuka menangkap untuk menampilkan pesan, sedangkan lapisan inti membiarkan exception naik.

Compiler menegakkan penanganan nil#

Keunggulan gaya nilable adalah penegakan pada waktu kompilasi. Berkas pengecil-gagal.cr berikut sengaja menggunakan hasil cari tanpa pemeriksaan, dan program itu tidak pernah selesai dikompilasi.

crystal pengecil-gagal.cr
# Sengaja tidak dapat dikompilasi: hasil cari dipakai tanpa menangani nil.
struct Barang
  getter nama : String

  def initialize(@nama : String)
  end
end

def cari(id : Int32) : Barang | Nil
  id == 1 ? Barang.new("Pensil") : nil
end

barang = cari(9)
puts barang.nama
bash
# compiler menolak program yang mengabaikan nilai nil
crystal run --no-color --error-trace pengecil-gagal.cr
text Keluaran
In pengecil-gagal.cr:14:13

 14 | puts barang.nama
                  ^---
Error: undefined method 'nama' for Nil (compile-time type is (Barang | Nil))

Nil trace:

  pengecil-gagal.cr:14

    puts barang.nama


  pengecil-gagal.cr:13

    barang = cari(9)


  pengecil-gagal.cr:13

    barang = cari(9)
             ^~~~

  pengecil-gagal.cr:9

    def cari(id : Int32) : Barang | Nil
        ^~~~

  pengecil-gagal.cr:10

      id == 1 ? Barang.new("Pensil") : nil


  pengecil-gagal.cr:10

      id == 1 ? Barang.new("Pensil") : nil

Pesan undefined method 'nama' for Nil muncul karena compiler memeriksa semua kemungkinan tipe dari union, termasuk Nil yang tidak memiliki metode nama. Bagian Nil trace menelusuri asal nilai tersebut hingga pemanggilan cari(9) yang dapat mengembalikan nil. Pesan penolakan juga menyebut posisi baris dan kolom, sehingga lokasi yang perlu diperbaiki langsung terlihat. Penolakan terjadi sebelum program berjalan, sehingga kelalaian penanganan tidak mungkin lolos ke runtime.

Pemeriksaan semacam ini tidak menambah biaya apa pun saat program berjalan karena seluruhnya selesai pada waktu kompilasi. Konsekuensinya, keputusan menggunakan tanda tangan nilable menuntut kedisiplinan dari seluruh pemanggil di dalam kode. Ganti rugi itu seimbang dengan keuntungannya, yaitu kelalaian penanganan nil terdeteksi lebih awal dan tidak pernah mencapai pengguna program.

Memilih gaya untuk API#

AspekNilai balik nilableException
Makna ketakhadirankeadaan yang wajar dan sering terjadipelanggaran kontrak yang luar biasa
Beban penanganandi pemanggil, ditegakkan compilerdi pemanggil, hanya apabila diperlukan
Alur programpercabangan if dan else biasastruktur begin, rescue, dan ensure
Komposisidapat dirangkai dengan try dan operator ||perlu blok rescue pada setiap panggilan
Contoh penggunaanpencarian katalog berdasarkan idpemuatan konfigurasi wajib saat program dimulai

Kaidah praktisnya: gunakan nilai balik nilable ketika ketakhadiran adalah jawaban yang sah atas pertanyaan pemanggil, dan gunakan exception ketika kegagalan berarti asumsi program telah rusak. Pustaka bawaan Crystal mengikuti kaidah yang sama, yaitu akses hash dengan tanda tanya mengembalikan nil untuk kunci yang tidak ada, sedangkan akses tanpa tanda tanya melontarkan KeyError. Kedua gaya juga dapat digunakan berdampingan pada satu kelompok API, sebagaimana hash menyediakan akses nilable sekaligus metode fetch dengan blok pelontar. Konsistensi satu gaya pada seluruh API membuat pemanggil tidak perlu menebak bentuk laporannya.

Ringkasan#

  • Nilai balik nilable, misalnya Barang | Nil, menempatkan ketakhadiran sebagai nilai yang penanganannya ditegakkan compiler sebelum program berjalan.
  • Gaya raise menjanjikan hasil non-nil dan melontarkan exception ketika kontrak dilanggar, sehingga ketakhadiran menjadi keadaan luar biasa.
  • Percabangan if pada nilai nilable mengecilkan tipe di dalam cabangnya, sedangkan gaya raise dikelola dengan begin dan rescue.
  • Nilai balik nilable tepat untuk ketakhadiran yang wajar, dan raise tepat untuk pelanggaran kontrak yang seharusnya tidak terjadi.

Referensi#