case objects

Objek bervarian sebagai union terdiskriminasi untuk memodelkan tipe data aljabar.

Daftar isi

Tipe data nyata sering berupa pilihan di antara beberapa bentuk, misalnya bentuk geometri yang bisa berupa lingkaran atau persegi. Topik ini membahas case object, yaitu object yang fieldnya bergantung pada varian yang dipilih melalui sebuah enum. Struktur semacam ini dikenal sebagai union terdiskriminasi (discriminated union), dan setelah membacanya, data yang bentuknya bergantian dapat dimodelkan tanpa dipecah ke variabel-variabel yang saling terlepas.

Objek bervarian dengan enum pemilih#

Deklarasinya memasukkan pernyataan case ke dalam bagian type. Field pertama berupa diskriminan, yaitu field bertipe enum yang menentukan varian aktifnya, lalu setiap cabang of menuliskan field yang hanya berlaku untuk varian tersebut. Bentuk ini menjanjikan bahwa field cabang tertentu hanya terbaca apabila diskriminan memang menunjuk cabang itu.

Contoh berikut memodelkan bentuk geometri dengan dua varian. Diskriminan jenis bertipe BentukJenis, cabang bjLingkaran memiliki field radius, dan cabang bjPersegi memiliki field sisi.

nim bentuk_luas.nim
# Objek bervarian untuk bentuk geometri
import std/math

type
  BentukJenis = enum
    bjLingkaran
    bjPersegi

  Bentuk = object
    case jenis: BentukJenis
    of bjLingkaran:
      radius: float
    of bjPersegi:
      sisi: float

proc luas(b: Bentuk): float =
  case b.jenis
  of bjLingkaran:
    PI * b.radius * b.radius
  of bjPersegi:
    b.sisi * b.sisi

var lingkaran = Bentuk(jenis: bjLingkaran, radius: 7.0)
var persegi = Bentuk(jenis: bjPersegi, sisi: 5.0)

echo luas(lingkaran)
echo luas(persegi)
text Keluaran
153.93804002589985
25.0

Field pada varian aktif digunakan seperti field object biasa, sedangkan proc luas menyeleksi perhitungannya melalui case b.jenis. Keluaran pertama berupa PI * 7 * 7 yang dicetak dengan seluruh presisi float-nya, dan keluaran kedua berupa 5 * 5. Karena BentukJenis hanya memiliki dua nilai, kedua cabang of menutup semua kemungkinan dan cabang else tidak diperlukan. Apabila varian baru ditambahkan ke enum, setiap case yang memilih berdasarkan diskriminan itu gagal dikompilasi sampai cabang barunya ditulis, sehingga perubahan model tidak lolos tanpa disadari.

Tata letak memori objek bervarian digambarkan pada diagram berikut: diskriminan selalu tersimpan, sedangkan area cabang hanya menampung field varian yang aktif.

Tata letak objek bervarian BentukObjek bervarian Bentukjenis: BentukJenisdiskriminan, selalu adahanya satu cabang aktif pada satu waktu:bjLingkaran → radius: floatbjPersegi → sisi: floatContoh nilai aktifjenis: bjLingkaranradius: 7.0cabang bjPersegi tidak menyimpan apa pun
Objek bervarian menyimpan diskriminan dan hanya field dari varian aktifnya

Memodelkan tipe data aljabar#

Union terdiskriminasi adalah cara Nim menyusun tipe data aljabar bagian sum type, yaitu tipe yang nilainya berupa satu pilihan di antara beberapa alternatif dengan muatan yang berbeda. Setiap varian membawa data miliknya sendiri, dan label enum-nya menjamin muatan itu tidak tertukar. Pola ini menggantikan kombinasi field opsional yang mudah keliru, misalnya pasangan field nilai dan pesan yang seharusnya diisi bergantian tanpa ada yang mengawasinya.

Contoh berikut memodelkan hasil pembacaan angka dari masukan yang bisa gagal. Varian spSukses membawa nilai berupa int, sedangkan varian spGagal membawa pesan berupa string.

nim hasil_parsing.nim
# Union terdiskriminasi untuk hasil parsing
type
  StatusParsing = enum
    spSukses
    spGagal

  HasilParsing = object
    case status: StatusParsing
    of spSukses:
      nilai: int
    of spGagal:
      pesan: string

proc sukses(n: int): HasilParsing =
  HasilParsing(status: spSukses, nilai: n)

proc gagal(pesan: string): HasilParsing =
  HasilParsing(status: spGagal, pesan: pesan)

proc tampilkan(h: HasilParsing): string =
  case h.status
  of spSukses:
    "nilai: " & $h.nilai
  of spGagal:
    "gagal: " & h.pesan

echo tampilkan(sukses(42))
echo tampilkan(gagal("bukan angka"))
text Keluaran
nilai: 42
gagal: bukan angka

Kedua proc pembangun, sukses dan gagal, menjamin bahwa diskriminan dan fieldnya selalu konsisten. Proc tampilkan membaca hasil hanya melalui case h.status, sehingga tidak ada jalur yang membaca field milik varian lain. Menambahkan varian ketiga, misalnya spKosong untuk masukan yang kosong, menuntut cabang baru pada setiap case yang memilih berdasarkan diskriminan, dan kompilator mengingatkan sampai semuanya lengkap.

Keunggulan lainnya terasa ketika model berkembang. Dokumentasi tutorial Nim bagian kedua mencatat bahwa hierarki object sering kali berlebihan untuk kebutuhan varian sederhana, sedangkan case object tidak menuntut konversi antar tipe karena semua varian tetap satu tipe yang sama. Satu seq[Bentuk] dapat menampung lingkaran dan persegi sekaligus, dan setiap elemennya diolah dengan pola case yang sama.

Akses varian yang salah dan FieldDefect#

Kompilator tidak menolak akses ke field varian yang tidak aktif, sehingga kesalahan berikut baru terlihat saat program dijalankan.

nim Contoh: akses varian yang tidak aktif
# tidak-dijalankan
# membaca field milik cabang lain memicu FieldDefect saat runtime
var b = Bentuk(jenis: bjLingkaran, radius: 7.0)
echo b.sisi

Program di atas dikompilasi tanpa keluhan, tetapi saat dijalankan Nim memicu pengecualian FieldDefect dengan pesan field 'sisi' is not accessible for type 'Bentuk' using 'jenis = bjLingkaran'. Pesan itu menyebutkan field yang diakses, tipe object-nya, dan nilai diskriminan saat itu, sehingga penyebabnya mudah dilacak. Aturan yang sama berlaku untuk penugasan diskriminan, yaitu memberi b.jenis = bjPersegi pada objek yang varian aktifnya lingkaran, karena perubahan itu mengganti cabang yang sedang aktif.

Kebiasaan yang menjaga kode tetap aman adalah membaca case object selalu melalui case pada diskriminan, sebagaimana proc luas dan tampilkan pada contoh di atas. Pola ini memindahkan pemeriksaan varian dari kewaspadaan penulis kode ke tugas kompilator. Konsumsi data yang terikat cabang menjadi eksplisit, dan pembaca berikutnya dapat melihat batasnya tanpa menelusuri seluruh proc.

Kesalahan umum#

Kesalahan pertama berupa mengisi konstruktor dengan field milik cabang yang tidak cocok. Konstruksi Bentuk(jenis: bjPersegi, radius: 7.0) ditolak saat kompilasi dengan pesan a case selecting discriminator 'jenis' with value 'bjPersegi' appears in the object construction, but the field(s) 'radius' are in conflict with this value, sehingga ketidaksesuaian label dan muatan tertangkap lebih awal. Perbaikannya menyamakan kelompok field dengan nilai diskriminan pada konstruktor yang sama.

Kesalahan kedua berupa lupa menambah cabang pada proc lama setelah varian baru dideklarasikan. Kompilator menolak setiap case yang belum menutup seluruh nilai diskriminan, dan pesannya menunjuk nilai yang belum tertangani. Perbaikannya menulis cabang untuk varian tersebut, karena jalan pintas berupa else yang membungkam pemeriksaan justru menghilangkan jaminan utama union terdiskriminasi.

Ringkasan#

  • Case object menempatkan pernyataan case di dalam bagian type, dengan enum sebagai diskriminan dan field milik tiap varian di cabang of-nya.
  • Struktur ini menyusun union terdiskriminasi, yaitu tipe data aljabar bagian sum type yang label dan muatannya tidak dapat tertukar.
  • Akses ke field varian yang tidak aktif memicu FieldDefect saat runtime, dan penugasan diskriminan dibatasi agar cabang aktif tidak berubah diam-diam.
  • Menambah varian baru membuat setiap case yang cabangnya belum lengkap gagal dikompilasi, sehingga perubahan model terpandu oleh kompilator.

Referensi#