Tuple dan record

Data berlabel imutabel dengan kesetaraan struktural untuk memodelkan nilai gabungan sederhana.

Daftar isi

Tuple dan record adalah dua tipe gabungan dasar pada F# untuk mengelompokkan beberapa nilai menjadi satu nilai gabungan. Topik ini membahas cara mendefinisikan keduanya, membedah isinya, kesetaraan struktural, serta pembuatan salinan melalui ekspresi copy-and-update. Di bagian akhir tersaji kriteria ringkas untuk memilih antara tuple dan record saat memodelkan data.

Tuple untuk gabungan nilai sementara#

Tuple menggabungkan beberapa nilai yang boleh bertipe berbeda menjadi satu nilai dengan urutan yang tetap. Penulisannya berupa nilai-nilai yang dipisahkan koma di dalam tanda kurung. Tuple tidak memiliki nama field, sehingga isinya hanya dapat diakses melalui posisi, baik dengan fungsi fst dan snd untuk pasangan berisi dua elemen maupun dengan pembongkaran pola (destructuring).

Berkas berikut membuat tuple posisi dua dimensi, membongkar isinya ke dalam dua nama, kemudian mencetak tuple tiga elemen.

fsharp koordinat.fsx
// Tuple posisi dua dimensi beserta pembongkaran nilainya.
let posisi = (3, 4)
let (x, y) = posisi
printfn "x = %d dan y = %d" x y
printfn "elemen pertama: %d" (fst posisi)
let titik3 = (1.5, 2.5, 3.5)
printfn "%A" titik3
text Keluaran
x = 3 dan y = 4
elemen pertama: 3
(1.5, 2.5, 3.5)

Baris let (x, y) = posisi memperlihatkan pembongkaran pola pada binding let, yaitu satu langkah yang sekaligus mengambil dua elemen dan memberi nama padanya. Fungsi fst dan snd hanya berlaku untuk pasangan berisi dua elemen, sedangkan tuple dengan tiga elemen atau lebih dibongkar lewat pola. Karena tidak berlabel, tuple cocok untuk gabungan nilai yang hanya digunakan di dalam satu fungsi, misalnya nilai balik berupa pasangan angka.

Tipe tuple tersirat dari penulisannya, dan tuple pada contoh di atas bertipe int * int. Simbol * pada tipe tuple bukan perkalian, melainkan penanda gabungan dua tipe. Satu fungsi juga dapat mengembalikan beberapa nilai sekaligus lewat tuple, dan pembongkaran pada sisi pemanggil memberi nama untuk setiap hasilnya. Seiring bertambahnya elemen, akses berbasis posisi semakin sulit diikuti karena arti setiap posisi harus diingat secara manual, dan batas inilah yang membuat tuple tepat untuk gabungan pendek daripada data domain.

Record: data berlabel dengan nama field#

Record adalah tipe gabungan yang setiap elemennya memiliki nama field dan tipe yang tetap. Deklarasinya menggunakan kata kunci type diikuti bentuk kurung kurawal yang menuliskan seluruh field. Saat membuat nilai record, setiap field wajib disebut nama dan isinya, sehingga susunan nilai tidak bergantung pada urutan.

Berkas buku.fsx mendeklarasikan record Buku untuk koleksi perpustakaan sekolah, membandingkan dua record yang isinya sama, membuat salinan dengan with, lalu memeriksa bahwa record asli tidak berubah.

fsharp buku.fsx
// Koleksi perpustakaan sekolah: record Buku beserta operasi dasarnya.
type Buku =
    { Judul: string
      Tahun: int
      Tersedia: bool }

let bukuA = { Judul = "Kamus Botani"; Tahun = 2018; Tersedia = true }
let bukuB = { Judul = "Kamus Botani"; Tahun = 2018; Tersedia = true }

// kesetaraan struktural: field demi field dibandingkan
printfn "dua record bernilai sama: %b" (bukuA = bukuB)

// copy-and-update dengan with
let bukuSetelahDipinjam = { bukuA with Tersedia = false }
printfn "%A" bukuSetelahDipinjam

// record aslinya tidak berubah karena imutabel
printfn "bukuA masih tersedia: %b" bukuA.Tersedia
text Keluaran
dua record bernilai sama: true
{ Judul = "Kamus Botani"
  Tahun = 2018
  Tersedia = false }
bukuA masih tersedia: true

Nilai true pada baris pertama berasal dari kesetaraan struktural: dua record dianggap sama apabila seluruh fieldnya bernilai sama. Keluaran %A menampilkan seluruh field beserta namanya, sehingga isi nilai mudah dibaca. Baris terakhir memastikan bukuA masih tersedia karena operasi with tidak menyentuh record aslinya.

Field juga dapat diakses dengan tanda titik, misalnya bukuA.Tahun, dan compiler menyimpulkan tipe record dari label yang digunakan saat konstruksi. Karena itu, pembuatan bukuA tidak perlu menyebut nama Buku secara eksplisit. Apabila kejelasan lebih diutamakan, nama tipe tetap dapat dituliskan sebagai anotasi, misalnya let bukuA: Buku = ....

Salinan baru dengan with#

Field record tidak dapat diubah nilainya secara langsung karena record bersifat imutabel. Ekspresi copy-and-update menyalin seluruh field dari record yang ada, mengganti sebagian field yang disebut, lalu menghasilkan record baru. Bentuknya { nilaiLama with Field = nilaiBaru }, dan beberapa field dapat diganti sekaligus dengan pemisah titik koma, misalnya { bukuA with Tahun = 2019; Tersedia = false }.

Mekanisme ini menjaga nilai lama tetap utuh, sehingga versi sebelumnya masih dapat digunakan oleh bagian program lain. Perubahan keadaan data domain dinyatakan sebagai nilai baru, bukan pengubahan nilai lama, dan pola tersebut menjadi kebiasaan yang menonjol pada kode F#. Compiler juga menuntut salinan memiliki seluruh field yang lengkap, sehingga tidak ada field yang tertinggal tanpa disengaja.

Kebiasaan memperbarui data lewat salinan membuat alur keadaan terbaca. Setiap fungsi menerima keadaan lama dan mengembalikan keadaan baru, sehingga tidak ada fungsi yang mengubah keadaan milik bagian lain secara diam-diam. Keadaan sebelumnya juga tetap tersedia untuk dibandingkan, misalnya ketika perubahan perlu ditampilkan atau diperiksa ulang.

Memilih antara tuple dan record#

Tuple dan record menawarkan kemampuan yang mirip, sehingga pilihan antara keduanya perlu kriteria yang jelas. Tuple menonjol pada gabungan sementara di dalam satu fungsi, sedangkan record menonjol pada data domain yang berpindah antar fungsi.

SituasiPilihan yang tepatAlasan
Dua atau tiga nilai sementara di dalam satu fungsituplecukup ditulis langsung tanpa deklarasi
Data domain yang berpindah antar fungsirecordnama field membuat pemanggilan terbaca tanpa membaca definisi tipe
Data yang fieldnya dapat bertambah seiring wakturecordpenambahan field tidak memecah seluruh pengguna
Data yang dibandingkan isi per isituple atau recordkeduanya memberi kesetaraan struktural

Satu batas praktis yang sering digunakan adalah jumlah penggunaan data. Gabungan nilai yang hanya muncul satu kali di satu tempat tidak membutuhkan deklarasi, sedangkan data yang sama digunakan di beberapa tempat lebih aman dibungkus record karena namanya menjadi kontrak yang diperiksa compiler.

Kesalahan umum#

Kesalahan yang paling sering terjadi pada record adalah salah menulis nama field. Compiler menolaknya dengan pesan error karena label tidak dikenal. Berkas berikut sengaja memuat label tahun dalam huruf kecil, sedangkan deklarasi mendefinisikan Tahun.

fsharp field-salah.fsx
// tidak-dijalankan: berkas ini sengaja memuat nama field yang tidak ada
type Buku =
    { Judul: string
      Tahun: int }

let buku = { Judul = "Atlas Awan"; tahun = 2019 }
printfn "%A" buku

Perintah berikut menjalankan berkas tersebut. Pesan error yang muncul memang diharapkan, dan bagian jalur folder pada pesan disingkirkan agar ringkas.

bash
# berkas sengaja gagal dikompilasi, lalu jalur folder disingkirkan dari pesan
dotnet fsi field-salah.fsx 2>&1 | sed "s|.*/||"
text Keluaran
field-salah.fsx(6,36): error FS0039: The record label 'tahun' is not defined. Maybe you want one of the following:
   Tahun

Pesan error FS0039 menyebut label tahun yang tidak dikenal sekaligus menawarkan kandidat yang ada, yaitu Tahun. Nama field pada record sensitif terhadap huruf besar dan huruf kecil, sehingga perbedaan satu huruf sudah menghasilkan error. Perbaikannya adalah menyamakan penulisan label dengan deklarasi, dan compiler akan menghitung ulang kelengkapan seluruh field setelah perbaikan ditulis.

Kesalahan lain yang mirip terjadi saat membandingkan dua record yang labelnya sama tetapi isinya berbeda. Kesetaraan struktural menuntut seluruh field bernilai sama, sehingga dua Buku dengan judul sama tetapi tahun berbeda dianggap tidak sama. Pemahaman ini penting karena kesetaraan pada tipe referensi di banyak bahasa lain justru membandingkan alamat memori, sedangkan record F# membandingkan isi secara baku.

Ringkasan#

  • Tuple menggabungkan beberapa nilai tanpa label dan paling cocok untuk gabungan sementara di dalam satu fungsi.
  • Record memberi nama pada setiap field, sehingga konstruksi dan pembacaan data domain tidak bergantung pada urutan.
  • Kesetaraan struktural membandingkan isi field demi field, baik pada tuple maupun record.
  • Ekspresi with menghasilkan record baru dengan sebagian field diganti, sedangkan record asli tetap utuh.

Referensi#