Pola process berkondisi pada modul ini menuntut penulisan receive dan rekursi berekor secara manual untuk setiap state yang disimpan. Modul Agent menyediakan bentuk siap pakai dari pola itu: sebuah process yang hanya bertugas menyimpan satu nilai dan menjalankan fungsi atas nilai tersebut ketika diminta. Topik ini membahas cara memulai Agent, membaca dan mengubah statenya, serta menilai secara objektif kapan Agent tepat digunakan dan kapan GenServer lebih layak dipilih.
Menyimpan state dengan Agent#
Agent.start_link/1 memulai process baru yang statenya berasal dari hasil sebuah fungsi, misalnya fn -> [] end untuk daftar kosong. Fungsi Agent.start_link/1 mengembalikan {:ok, pid}, karena di dalamnya terdapat process sungguhan yang menjalankan loop berkondisi, persis seperti pola loop yang ditulis secara manual. Selisihnya, seluruh penulisan loop telah dilakukan oleh modul Agent, sehingga yang tersisa hanyalah fungsi-fungsi aksesnya.
Akses state dilakukan dengan tiga fungsi utama. Agent.get/2 menjalankan fungsi pembaca atas state terkini dan mengembalikan hasilnya ke pemanggil. Agent.update/2 menjalankan fungsi perubahan dan menetapkan hasilnya sebagai state baru tanpa mengembalikan nilai. Agent.get_and_update/2 melakukan keduanya dalam satu pemanggilan, yaitu membaca sebagian state sekaligus menetapkan state barunya. Ketiga fungsi itu dieksekusi satu demi satu di dalam process Agent, sehingga tidak ada dua pemanggil yang mengubah state secara bersamaan.
Karena process tidak pernah berbagi memori, state Agent hanya dimiliki oleh process Agent itu sendiri. Hasil Agent.get/2 berupa salinan data yang dikirim melalui pesan, sehingga mengubah salinan itu di process pemanggil tidak pernah memengaruhi state di dalam Agent. Pembaruan hanya terjadi apabila fungsi perubahan dijalankan di process Agent dan hasilnya ditetapkan sebagai state baru. Sifat ini menghilangkan berbagai persoalan akses bersama yang biasa muncul pada memori bersama.
Skrip utuh: daftar belanja pribadi#
Skrip berikut menyimpan daftar belanja pribadi di sebuah Agent. Dua butir ditambahkan satu per satu, isinya dibaca, lalu seluruh isi diganti dengan satu butir baru melalui Agent.update/2 yang mengabaikan daftar lama. Di akhir skrip, Agent dihentikan secara rapi dengan Agent.stop/1:
# keranjang.exs — daftar belanja pribadi yang disimpan Agent
{:ok, keranjang} = Agent.start_link(fn -> [] end)
Agent.update(keranjang, fn daftar -> daftar ++ ["telur"] end)
Agent.update(keranjang, fn daftar -> daftar ++ ["beras"] end)
IO.inspect(Agent.get(keranjang, & &1), label: "isi daftar belanja")
Agent.update(keranjang, fn _daftar_lama -> ["minyak goreng"] end)
IO.inspect(Agent.get(keranjang, & &1), label: "isi setelah diganti")
Agent.stop(keranjang)
IO.puts("agent dihentikan dengan rapi")isi daftar belanja: ["telur", "beras"]
isi setelah diganti: ["minyak goreng"]
agent dihentikan dengan rapiKesederhanaan skrip di atas memang merupakan tujuan Agent. Tidak ada defmodule, tidak ada klausa receive, dan tidak ada pid yang ditangkap secara manual, padahal di baliknya process berkondisi penuh tetap berjalan. Skrip ini dapat dijalankan langsung dari terminal setelah berkasnya disimpan:
# menjalankan skrip daftar belanja
elixir keranjang.exsisi daftar belanja: ["telur", "beras"]
isi setelah diganti: ["minyak goreng"]
agent dihentikan dengan rapiMembaca dan mengubah dalam satu langkah#
Beberapa pengubahan memerlukan nilai lama sebagai dasar perhitungan, misalnya menaikkan penghitung. Menjalankan Agent.get/2 lalu Agent.update/2 berurutan tidak aman secara logika, karena process lain dapat mengubah state di antara keduanya. Agent.get_and_update/2 menutup celah itu dengan menjalankan kedua tugas dalam satu eksekusi di process Agent. Fungsi yang diberikan mengembalikan tuple {nilai_dibaca, state_baru}:
{:ok, penghitung} = Agent.start_link(fn -> 0 end)
nilai_lama = Agent.get_and_update(penghitung, fn angka -> {angka, angka + 1} end)
IO.inspect(nilai_lama, label: "nilai lama")
IO.inspect(Agent.get(penghitung, & &1), label: "nilai baru")nilai lama: 0
nilai baru: 1Nilai lama 0 dan nilai baru 1 pada keluaran menunjukkan kedua hasil itu keluar dari satu eksekusi yang sama. Secara teknis, fungsi tanpa nama yang diberikan merupakan isi pesan itu sendiri: fungsi itu dikirim ke process Agent, dijalankan di sana atas state terkini, lalu tuple hasilnya dipecah menjadi balasan dan state baru. Karena fungsi tersebut dijalankan di process Agent, variabel yang tertangkap olehnya berasal dari sisi pemanggil, sedangkan eksekusinya berlangsung di sisi server.
Selain ketiga fungsi tersebut, Agent.cast/2 mengirim perubahan tanpa menunggu eksekusinya selesai. Pemanggilannya segera mengembalikan :ok, dan fungsi perubahannya dieksekusi oleh process Agent belakangan. Agent.cast/2 berguna apabila pemanggil tidak memerlukan hasilnya, tetapi jaminan bahwa perubahan sudah diterapkan hilang bersama kepastian waktunya.
Penilaian atas Agent#
Agent menyelesaikan satu kebutuhan dengan sangat sedikit kode, yaitu menyimpan state yang diakses dari banyak process. Untuk state sederhana seperti penghitung, konfigurasi kecil, atau daftar yang diperbarui sewaktu-waktu, Agent adalah pilihan yang wajar. Penulisannya lebih pendek daripada loop manual, dan seluruh detail pemilihan pesan telah ditangani.
Keterbatasannya perlu dinyatakan dengan kejelasan yang sama. Pertama, kontrak pesannya bersifat implisit. Antarmuka Agent berupa fungsi tanpa nama yang dikirim ke process server, sehingga tidak ada daftar pesan resmi yang dapat dibaca, diuji, atau dijaga konsistensinya. Semakin banyak jenis akses, semakin sulit melacak fungsi apa saja yang boleh menyentuh state. GenServer menggantinya dengan callback yang bernama dan terdokumentasi.
Kedua, seluruh eksekusi bersifat serial. Karena fungsi pembaca dan pengubah dijalankan di process Agent, satu fungsi yang lambat menunda seluruh pemanggil lain. Pekerjaan berat seperti pengolahan daftar panjang sebaiknya tidak diletakkan dalam Agent. Ketiga, Agent yang berdiri sendiri tidak memiliki pengawas. Exception di dalam fungsi perubahan menghentikan process Agent beserta seluruh statenya, dan tidak ada yang memulai ulang secara otomatis. Perlindungan semacam itu diberikan oleh pohon supervision yang menyertai GenServer. Dengan pertimbangan tersebut, Agent tepat digunakan untuk state sederhana satu jenis, sedangkan GenServer menjadi pilihan ketika kontrak pesan, pekerjaan yang bervariasi, atau pengawasan kegagalan mulai diperlukan.
Ringkasan#
Agent.start_link/1memulai process berkondisi siap pakai yang statenya berasal dari sebuah fungsi awal, dengan akses melaluiAgent.get/2,Agent.update/2, danAgent.get_and_update/2.- Semua fungsi akses dieksekusi di process Agent secara satu demi satu, sehingga state tidak pernah diubah bersamaan, tetapi fungsi yang lambat juga menunda pemanggil lain.
- Agent tidak menyediakan
put, sehingga penggantian seluruh state dilakukan melaluiAgent.update/2yang mengabaikan nilai lama. - Agent tepat digunakan untuk state sederhana, sedangkan GenServer lebih layak ketika kontrak pesan yang jelas, pekerjaan yang bervariasi, atau pengawasan kegagalan diperlukan.
Referensi#
- Modul Agent — Hexdocs Elixir — dokumentasi resmi seluruh fungsi modul Agent.
- Processes — Hexdocs Elixir — panduan process yang menjadi dasar kerja Agent.
- Modul GenServer — Hexdocs Elixir — alternatif Agent dengan kontrak callback yang eksplisit.