Model State

Memahami model nilai versus identitas serta waktu sebagai rangkaian nilai beruntun.

Daftar isi

Topik ini membahas model state pada Clojure, yaitu pemisahan antara value yang tidak dapat diubah dan identity yang menunjuk nilai berbeda dari waktu ke waktu. Pembahasan mencakup sifat immutable struktur data, peran identitas sebagai penunjuk, dan konsekuensi model ini pada program yang berjalan secara konkuren. Setelah topik ini selesai, keputusan memilih mekanisme state dapat diambil dengan dasar yang jelas.

Nilai bersifat immutable#

Seluruh struktur data inti Clojure berupa value, yaitu nilai data yang tidak dapat diubah setelah dibuat (immutable). Fungsi yang tampak seperti pengubahan tidak menyentuh nilai aslinya, melainkan mengembalikan nilai baru yang sudah berubah. Fungsi assoc pada map adalah contoh utamanya, dan nilai lama tetap utuh serta tetap sah digunakan bagian kode lain. Perilaku ini terlihat pada contoh berikut, ketika satu buku di gudang dipinjam sehingga stoknya turun satu.

clojure Contoh: assoc menghasilkan nilai baru
(def stok-awal {:judul "Sistem Operasi" :jumlah 12})
(def stok-setelah-pinjam (assoc stok-awal :jumlah 11))

(println "Nilai lama:")
(prn stok-awal)
(println "Nilai baru:")
(prn stok-setelah-pinjam)
text Keluaran
Nilai lama:
{:judul "Sistem Operasi", :jumlah 12}
Nilai baru:
{:judul "Sistem Operasi", :jumlah 11}

Nilai lama dan nilai baru berdampingan tanpa saling mengganggu. Pada bahasa yang berorientasi mutasi, pengubahan biasanya menimpa data yang sama, sedangkan assoc menyisakan dua nilai terpisah yang masing-masing dapat dibaca kapan pun. Pola yang sama berlaku untuk update, conj, dan dissoc, sehingga tidak ada operasi pengubah yang dapat merusak data yang sudah ada. Karena tidak ada nilai yang berubah, tidak ada risiko data berganti tanpa sepengetahuan pembaca.

Identitas sebagai penunjuk ke nilai#

Nilai yang immutable tidak berarti program tanpa perubahan. Perubahan pada Clojure dinyatakan lewat identity, yaitu tempat bernama yang menunjuk nilai berbeda pada waktu yang berbeda. Var yang dibuat dengan def adalah identitas paling sederhana, sedangkan atom adalah identitas yang nilainya memang dimaksudkan untuk diganti selama program berjalan dan dibahas pada topik tersendiri di modul ini. Identitas tidak memuat data, tetapi hanya menunjuk, sehingga dua identitas boleh menunjuk nilai yang sama tanpa risiko saling mengganggu.

Contoh berikut memeriksa dua katalog gudang yang berasal dari nilai yang sama.

clojure Contoh: dua var berasal dari nilai yang sama
(def katalog-a {:gedung "Utama" :judul 320})
(def katalog-b (assoc katalog-a :judul 321))

(println "Katalog A:" (:judul katalog-a))
(println "Katalog B:" (:judul katalog-b))
(println "Nilai sama:" (= katalog-a katalog-b))
(println "Identitas sama:" (identical? katalog-a katalog-b))
text Keluaran
Katalog A: 320
Katalog B: 321
Nilai sama: false
Identitas sama: false

Kedua katalog berbagi sejarah yang sama, tetapi assoc menghasilkan nilai baru sehingga isinya berbeda. Fungsi = menguji kesetaraan isi, sedangkan identical? menguji kesamaan objek di memori, dan keduanya bernilai false pada contoh tersebut. Di balik layar, bagian besar data yang sama tetap digunakan bersama karena struktur data persisten (persistent data structure) berbagi bagian yang tidak berubah antar nilai turunannya.

Diagram berikut merangkum hubungan itu: identitas stok menunjuk satu nilai pada setiap saat, sedangkan rangkaian nilai dari waktu ke waktu membentuk sejarahnya.

Identitas menunjuk satu nilai pada setiap saat{:stok 12}waktu t0 (lampau){:stok 11}waktu t1 (saat ini){:stok 10}waktu t2 (berikutnya)assocassocidentitas stok
Identitas menunjuk satu nilai pada setiap saat, sedangkan rangkaian nilai dari waktu ke waktu membentuk sejarah state

Waktu sebagai rangkaian nilai beruntun#

Model nilai dan identitas memberi arti khusus pada kata state. State suatu entitas pada satu saat adalah satu value yang ditunjuk identitasnya, sedangkan sejarah entitas tersebut adalah rangkaian nilai berurutan dari waktu ke waktu. Perubahan tidak merusak keadaan lampau, melainkan menambah nilai baru pada rangkaian itu. Pembaca yang memegang referensi ke nilai lama tetap melihat data yang konsisten karena nilai lama tidak pernah berubah.

AspekModel mutableModel nilai dan identitas
Bentuk perubahanData yang sama ditimpaNilai baru dibuat, identitas menunjuknya
Nilai lama setelah perubahanHilangTetap utuh dan dapat dibaca
Pembaca bersamaanPerlu kunci agar konsistenAman tanpa kunci

Perbedaan pada baris terakhir tabel menjadi dasar seluruh mekanisme konkurensi Clojure. Model mutable menuntut penguncian pada setiap pembacaan dan penulisan, sedangkan model nilai dan identitas hanya menuntut koordinasi pada saat identitas dipindahkan ke nilai berikutnya. Karena itu, pemilihan mekanisme state pada Clojure selalu dimulai dari kebutuhan koordinasi, bukan dari mutasi data.

Konsekuensi pada konkurensi#

Nilai yang immutable dapat dibagikan kepada thread mana pun tanpa perlindungan. Thread adalah satuan eksekusi yang berjalan mandiri, dan dua thread yang membaca nilai yang sama selalu melihat isi yang persis sama. Tidak ada thread yang dapat merusak nilai milik thread lain karena operasi apa pun yang tampak seperti pengubahan selalu menghasilkan nilai baru. Ilustrasi berikut menunjukkan dua thread yang membaca identitas yang sama pada saat yang bersamaan.

clojure Ilustrasi: dua thread membaca nilai bersama
; tidak-dijalankan
; Ilustrasi konseptual, bukan program utuh: dua thread membaca identitas
; yang sama pada saat yang bersamaan. Nilainya immutable, sehingga tidak
; ada thread yang dapat merusak nilai milik thread lain. Urutan cetak
; antar thread tidak dapat ditentukan, tetapi isi keluarannya selalu benar.
(let [nilai-bersama {:sensor "suhu" :nilai 28}]
  (future (println "Thread 1:" (:nilai nilai-bersama)))
  (future (println "Thread 2:" (:nilai nilai-bersama))))

Urutan cetak antar thread memang tidak dapat ditentukan, tetapi isi setiap baris selalu benar. Pembacaan yang aman bukan berarti program bebas dari race condition, yaitu hasil program yang bergantung pada urutan eksekusi antar thread. Sisa pekerjaan koordinasi berpindah ke identitas, yaitu pada saat nilai yang ditunjuk perlu diganti. Clojure menyediakan atom, ref, dan agent sebagai mekanisme penggantian identitas dengan tingkat koordinasi yang berbeda, sedangkan fondasi ketiganya adalah nilai immutable yang sama.

Kesalahan umum#

Mengira assoc mengubah nilai aslinya. Program yang berharap stok-awal ikut berubah akan menemukan nilai lama tetap sama, karena assoc mengembalikan nilai baru dan membuangnya bila hasilnya tidak diikat. Perbaikannya dengan menampung keluaran operasi pengubah pada var atau identitas lain, sebagaimana stok-setelah-pinjam pada contoh pertama.

Menyamakan kesetaraan isi dengan kesamaan identitas. Fungsi = menguji isi dua nilai, sedangkan identical? menguji objek yang sama di memori, sehingga dua map yang isinya sama dapat saja berupa objek yang berbeda. Perbaikannya dengan memilih pembanding sesuai maksud, yaitu = untuk logika program dan identical? untuk pemeriksaan pada tingkat memori.

Menyamakan state dengan wadah data yang ditimpa. Pada model ini, state adalah nilai yang ditunjuk identitas pada satu saat, bukan wadah yang isinya berubah. Program yang meniru gaya mutable dengan banyak reset! pada satu atom biasanya menyembunyikan struktur yang seharusnya dinyatakan sebagai nilai turunan yang jelas.

Ringkasan#

  • Nilai pada Clojure bersifat immutable, sehingga operasi seperti assoc menghasilkan nilai baru tanpa mengubah nilai aslinya.
  • Identity adalah penunjuk yang dapat menunjuk nilai berbeda dari waktu ke waktu, sedangkan state adalah nilai yang ditunjuk pada satu saat.
  • Struktur data persisten berbagi bagian yang tidak berubah antar nilai turunannya, sehingga nilai baru dibuat dengan efisien.
  • Nilai yang dibagikan antar thread aman tanpa kunci, dan koordinasi hanya diperlukan pada saat identitas diganti.

Referensi#