Record adalah sintaksis tambahan pada deklarasi data yang memberi nama pada setiap field konstruktor. Dengan nama tersebut, nilai dapat dibangun dengan menyebut field-nya satu per satu, dibaca melalui fungsi aksesor (accessor), dan diperbarui secara parsial. Topik ini membahas ketiga kemampuan tersebut beserta jebakan sintaksis yang paling sering dijumpai, yaitu tabrakan nama aksesor dan konstruksi dengan field yang kurang. Setelah membacanya, deklarasi record dapat ditulis dengan memahami konsekuensi setiap bentuknya.
Sintaksis record#
Deklarasi record menuliskan nama field beserta tipenya di antara kurung kurawal tepat setelah konstruktor. Nilai bertipe tersebut dapat dibangun dengan dua cara, yaitu cara posisional yang mengisi field sesuai urutan dan cara kurung kurawal yang menyebut nama tiap field. Cara kurung kurawal wajib menyebut seluruh field karena field yang tidak disebut tidak memiliki nilai. Program berikut menggunakan kedua cara tersebut pada tipe Buku.
-- Record: konstruktor dengan field bernama
data Buku = Buku
{ judul :: String
, penulis :: String
, tahun :: Int
} deriving (Show, Eq)
main :: IO ()
main = do
let rakBaca = [ Buku { judul = "Kisah Nusantara", penulis = "Citra Dewi", tahun = 2019 }
, Buku "Cerdas Cermat" "Budi Santoso" 2015
]
print (head rakBaca)
print (judul (head rakBaca))
print (map judul rakBaca)
let revisi = (head rakBaca) { tahun = 2021 }
print revisi
print (tahun revisi)
print (revisi == head rakBaca)Buku {judul = "Kisah Nusantara", penulis = "Citra Dewi", tahun = 2019}
"Kisah Nusantara"
["Kisah Nusantara","Cerdas Cermat"]
Buku {judul = "Kisah Nusantara", penulis = "Citra Dewi", tahun = 2021}
2021
FalseBaris pertama keluaran menunjukkan bahwa print menyertakan nama field saat mencetak nilai record, sehingga isi nilainya lebih mudah dibaca daripada bentuk posisional murni. Baris kedua dan ketiga memperlihatkan fungsi aksesor yang mengambil satu field, sedangkan baris keempat dan kelima memperlihatkan pembaruan satu field yang dibahas pada bagian berikutnya. Kedua cara pembangunan nilai menghasilkan nilai yang setara, sehingga pemilihannya bergantung pada keterbacaan, bukan pada semantik.
Aksesor field#
Aksesor adalah fungsi biasa yang tipe kembalinya sama dengan tipe field tersebut. Deklarasi judul pada Buku menghasilkan fungsi judul :: Buku -> String, sehingga aksesor dapat dipetakan, diterapkan secara parsial, dan diteruskan sebagai argumen sebagaimana fungsi lain. Baris map judul rakBaca pada program sebelumnya mengambil judul seluruh buku dalam satu pemetaan, dan sifat ini muncul tanpa sintaksis khusus apa pun.
Struktur bawaan yang paling dekat dengan pola ini adalah tuple, yang menyediakan aksesor posisi berupa fst dan snd. Perintah berikut mengambil unsur pertama sebuah tuple dua unsur.
# fst adalah aksesor posisi pertama pada tuple dua unsur
ghc -e 'fst (7, "tujuh")'7Tuple memang menyediakan aksesor, tetapi aksesor tersebut digunakan bersama oleh seluruh tuple dua unsur di mana pun, sehingga isinya hanya dikenali dari posisinya. Aksesor record memberi nama pada tiap field, dan itulah keunggulan utama yang ditawarkannya. Konsekuensi dari penamaan tersebut menimbulkan jebakan tersendiri yang dibahas pada bagian akhir topik ini.
Pencocokan pola pada record#
Field record juga dapat diikat langsung pada saat pencocokan pola. Cabang seperti Buku { judul = j, tahun = t } menyebut nama field beserta pola pengikatnya, sehingga pembacaan sebagian field tidak membutuhkan fungsi tambahan. Field yang tidak diperlukan cukup ditinggalkan tanpa disebut, dan urutan penyebutannya pun bebas karena pencocokannya berdasar nama, bukan berdasar posisi. Bentuk ini tetap pencocokan pola biasa, sehingga dapat dipadukan dengan cabang lain di dalam satu case.
Keunggulan pencocokan bernama paling terasa pada tipe dengan field bertipe sama. Tipe Buku yang ketiga fieldnya berbeda tipe memang masih dapat dicocokkan secara posisional dengan Buku j p t, tetapi tipe yang ketiga fieldnya sama-sama Int mudah tertukar posisinya saat dibaca. Pencocokan bernama menyebut makna setiap pengikatnya, sehingga kesalahan posisi seperti itu tidak mudah lolos pada peninjauan kode.
Memperbarui nilai#
Sintaksis pembaruan record ditulis dengan menyebut nilai lama, lalu field yang berubah di dalam kurung kurawal, seperti revisi = (head rakBaca) { tahun = 2021 }. Ekspresi semacam ini tidak mengubah nilai lama di tempatnya, melainkan menghasilkan nilai baru yang menyalin seluruh field lain dan menggantikan field yang disebut. Perilaku ini sejalan dengan sifat data yang immutable di Haskell, sehingga nilai lama pada rakBaca tetap utuh setelah pembaruan.
Baris terakhir keluaran program menguji hal itu melalui perbandingan revisi == head rakBaca yang menghasilkan False karena hanya tahunnya yang berbeda. Dengan demikian, pembaruan record menggantikan satu field tanpa menyentuh sisa nilainya, sebagaimana fungsi konversi pada topik pembuka yang menghasilkan nilai baru alih-alih mengubah nilai lama. Pembaruan juga tidak terpengaruh urutan penulisan field saat deklarasi, karena field yang diperbarui disebut dengan namanya, bukan dengan posisinya.
Jebakan sintaksis#
Dua jebakan paling sering dijumpai berasal dari penamaan field dan dari kelengkapan field saat pembangunan nilai. Jebakan pertama diperiksa ketat oleh compiler.
Aksesor berada di ruang nama tingkat modul, sehingga nama umum seperti nama atau tahun mudah menabrak aksesor tipe lain pada modul yang sama. Solusi paling sederhana adalah mengawali nama field dengan konteksnya, misalnya namaPenumpang dan namaKendaraan. Compiler GHC juga menyediakan ekstensi DuplicateRecordFields yang melonggarkan aturan ini, dan tautannya tercantum pada ## Referensi.
Jebakan kedua adalah konstruksi kurung kurawal dengan field yang kurang. Compiler hanya memberi peringatan pada saat kompilasi, sedangkan error baru muncul saat program berjalan ketika field yang tidak diinisialisasi benar-benar dibaca. Bentuk posisional menolak kekurangan field secara langsung karena jumlah argumennya harus pas, sehingga konstruksi kurung kurawal perlu diperiksa ulang jika tipe memiliki banyak field.
Ringkasan#
- Record memberi nama pada setiap field konstruktor, dan nilai dapat dibangun secara posisional maupun dengan menyebut nama field.
- Aksesor adalah fungsi biasa bertipe
Tipe -> TipeField, sehingga dapat dipetakan dan diteruskan sebagaimana fungsi lain. - Pembaruan record menghasilkan nilai baru dengan menggantikan field yang disebut, sedangkan nilai lama tetap utuh.
- Nama aksesor harus unik dalam satu modul, dan konstruksi kurung kurawal dengan field kurang baru berujung error saat berjalan.
Referensi#
- Haskell 2010 Report, bab 4: Declarations and bindings — haskell.org — Bagian 4.2.1 laporan bahasa resmi yang menetapkan sintaksis field bernama pada deklarasi data.
- Duplicate record fields — GHC Users Guide, downloads.haskell.org — Dokumentasi ekstensi compiler yang melonggarkan larangan nama aksesor ganda beserta cara penguraian ambiguitasnya.