Map dan struct

Data kunci-nilai dan disiplin bentuk melalui struct.

Daftar isi

Map menyimpan pasangan kunci-nilai dengan kunci yang boleh berupa nilai apa pun, sedangkan struct membatasi bentuk data pada daftar field, yaitu nama lapangan tetap yang ditetapkan sebuah modul. Topik ini membahas dua notasi penulisan map, operasi modul Map, pencocokan pola map beserta sifat %{} yang cocok dengan semua map, lalu struct dengan defstruct dan disiplin pembaruannya. Setelah topik ini selesai, pemilihan antara map dan struct dapat dilakukan dengan alasan yang jelas.

Dua notasi map#

Notasi %{"kunci" => nilai} menggunakan tanda => dan menerima kunci berupa nilai apa pun, sedangkan notasi %{kunci: nilai} adalah bentuk pendek yang hanya sah untuk kunci atom. Keduanya menghasilkan map, tetapi kunci string dan kunci atom dianggap berbeda, sehingga dua map berikut tidak sama meskipun tulisannya mirip. Kebiasaan yang umum adalah menempatkan kunci atom pada data internal program dan kunci string pada data dari luar, misalnya hasil penguraian JSON.

elixir
m1 = %{"kota" => "Bandung"}
m2 = %{kota: "Bandung"}
IO.inspect(m1)
IO.inspect(m2)
IO.inspect(m1 == m2, label: "sama")
text Keluaran
%{"kota" => "Bandung"}
%{kota: "Bandung"}
sama: false

Notasi pendek dan notasi => untuk kunci atom menghasilkan nilai yang sama persis, sehingga %{kunci: 1} dan %{:kunci => 1} adalah dua tulisan untuk satu map. Tanda => juga membolehkan tipe kunci yang berbeda dalam satu map, misalnya bilangan bulat dan atom sekaligus, keadaan yang tidak dapat dituliskan dengan notasi pendek. Perbedaan notasi juga terlihat pada tampilan inspeksi, yaitu map berkunci atom ditulis %{kunci: nilai} dan map berkunci string ditulis dengan tanda =>.

elixir
IO.inspect(%{kunci: 1} == %{:kunci => 1}, label: "notasi setara")
IO.inspect(%{1 => "satu", :dua => 2}, label: "kunci campur")
text Keluaran
notasi setara: true
kunci campur: %{1 => "satu", :dua => 2}

Operasi Map dan imutabilitas#

Modul Map menyediakan operasi kunci-nilai yang lengkap, dan dua yang paling sering digunakan adalah Map.get/3 serta Map.put/3. Fungsi Map.get/2 mengembalikan nilai untuk kunci yang ada dan nil untuk kunci yang tidak ada, sedangkan Map.get/3 menerima nilai pengganti untuk kunci yang tidak ditemukan. Setiap operasi mengembalikan map baru, dan map lama tetap utuh, sebagaimana janji imutabilitas data Elixir.

elixir
stok = %{apel: 10, jeruk: 5}
IO.inspect(Map.get(stok, :apel), label: "apel")
IO.inspect(Map.get(stok, :mangga, 0), label: "mangga")

stok_baru = Map.put(stok, :jeruk, 8)
IO.inspect(stok_baru, label: "stok baru")
IO.inspect(stok, label: "stok lama")
text Keluaran
apel: 10
mangga: 0
stok baru: %{apel: 10, jeruk: 8}
stok lama: %{apel: 10, jeruk: 5}

Nilai pengganti pada Map.get/3 menjadikan fungsi ini aman untuk kunci yang mungkin belum ada, misalnya pengaturan bawaan. Hasil Map.put/3 wajib diikat ke nama baru agar tidak hilang, karena map asal tidak pernah berubah. Map yang tidak lagi dirujuk nama mana pun akan dikumpulkan pemulung memori sebagaimana nilai lain yang tak terpakai.

Keluaran di atas memperlihatkan dua map terpisah, bukan satu map yang diubah. Gaya pemrograman yang mengalirkan data baru dari fungsi ke fungsi bertumpu pada perilaku ini, dan pola pencocokan pada bagian berikutnya membaca map tersebut tanpa mengubahnya.

Pencocokan pola map#

Pola map bersifat parsial, artinya hanya mencocokkan kunci yang disebutkan. Pencocokan %{nama: nama} = profil berhasil selama kunci :nama ada di dalam map, dan kunci lainnya diabaikan. Perilaku ini berbeda dari tuple dan list yang menuntut bentuk utuh. Konsekuensi yang paling dikenal adalah %{}, yaitu pola map kosong, yang cocok dengan semua map karena tidak menuntut kunci apa pun.

elixir
%{nama: nama} = %{nama: "Dewi", umur: 30}
IO.puts(nama)

IO.inspect(match?(%{}, %{a: 1}), label: "%{} dengan map")
IO.inspect(match?(%{}, "teks"), label: "%{} dengan string")
text Keluaran
Dewi
%{} dengan map: true
%{} dengan string: false

Sifat %{} wajib diperhitungkan ketika pola map digunakan di dalam case. Pola yang spesifik harus diletakkan lebih dahulu, dan %{} hanya cocok sebagai klausa penangkap terakhir. Urutan terbalik membuat klausa penangkap menampung semua map sebelum pola yang spesifik sempat dicoba.

Akses kunci yang tidak ada melalui Map.fetch!/2 melempar KeyError dengan pesan yang menyebut kunci dan map-nya. Fungsi Map.fetch/2 tanpa tanda seru tidak melempar apa pun, melainkan mengembalikan {:ok, nilai} atau :error.

elixir
pengguna = %{nama: "Ali"}
try do
  Map.fetch!(pengguna, :umur)
rescue
  e in KeyError -> IO.puts("KeyError: #{Exception.message(e)}")
end
text Keluaran
KeyError: key :umur not found in:

    %{nama: "Ali"}

Struct: bentuk data yang disiplin#

Struct didefinisikan dengan makro defstruct di dalam sebuah modul, dan daftar field-nya menjadi kontrak bentuk data. Nilai struct adalah map yang membawa kunci __struct__ sebagai penanda modul asalnya, sehingga field di luar definisi ditolak. Dalam berkas skrip yang mendefinisikan modul dan menggunakan struct-nya di tempat yang sama, pembentukan nilai dilakukan dengan fungsi struct/1.

elixir
defmodule Pengguna do
  defstruct nama: "tanpa nama", umur: 0
end

u1 = struct(Pengguna)
IO.inspect(u1, label: "u1")

u2 = %{u1 | umur: 24}
IO.inspect(u2, label: "u2")

IO.inspect(u2.nama, label: "u2.nama")
text Keluaran
u1: %Pengguna{nama: "tanpa nama", umur: 0}
u2: %Pengguna{nama: "tanpa nama", umur: 24}
u2.nama: "tanpa nama"

Pola map biasa tetap berlaku terhadap struct karena struct pada dasarnya adalah map. Pencocokan %{x: xv, y: yv} membaca field struct tanpa memerlukan literal %Titik{}, sedangkan fungsi is_struct/2 memeriksa keanggotaan struct terhadap modul tertentu. Kedua perkakas ini berguna pada kode yang menerima data dari fungsi lain tanpa perlu mengetahui cara pembentukannya.

elixir
defmodule Titik do
  defstruct x: 0, y: 0
end

t = struct(Titik)
%{x: xv, y: yv} = t
IO.inspect({xv, yv}, label: "koordinat")
IO.inspect(is_struct(t, Titik), label: "is_struct Titik")
text Keluaran
koordinat: {0, 0}
is_struct Titik: true

Pembaruan struct menggunakan sintaks %{s | kunci: nilai} yang hanya menerima kunci yang sudah tercantum pada defstruct. Percobaan memperbarui kunci baru melempar KeyError, dan inilah disiplin yang membedakan struct dari map biasa. Sebagai perbandingan, Map.put/3 tidak memeriksa definisi struct, sehingga kunci baru menempel dan hasilnya tercetak kembali sebagai map biasa, bukan %Titik{} yang rapi.

elixir
defmodule Titik do
  defstruct x: 0, y: 0
end

t = struct(Titik)
try do
  %{t | z: 1}
rescue
  e in KeyError -> IO.puts("KeyError: #{Exception.message(e)}")
end

IO.inspect(Map.put(t, :baru, 1), label: "Map.put kunci baru")
IO.inspect(%{t | x: 9}, label: "pembaruan sintaks |")
text Keluaran
KeyError: key :z not found
Map.put kunci baru: %{x: 0, y: 0, __struct__: Titik, baru: 1}
pembaruan sintaks |: %Titik{x: 9, y: 0}

Kesalahan umum#

  • Menganggap %{kunci: nilai} dan %{"kunci" => nilai} sama. Kedua map tidak sama karena tipenya berbeda. Perbaikannya, sepakati satu tipe kunci untuk setiap lapisan data dan lakukan konversi di batasnya.
  • Menambah field struct melalui Map.put/3. Kunci baru menempel di luar kontrak defstruct dan tampilan nilainya kembali menjadi map biasa. Perbaikannya, tambahkan field pada defstruct atau gunakan sintaks pembaruan %{s | kunci: nilai}.
  • Mengira pola map wajib utuh seperti tuple. Pola map bersifat parsial, dan justru %{} yang terlalu longgar. Perbaikannya, sebutkan kunci yang dibutuhkan dan jadikan %{} penangkap terakhir.
  • Menganggap akses titik dan Map.get/2 selalu dapat dipertukarkan. Akses titik seperti u2.nama berlaku untuk struct dan map berkunci atom, sedangkan Map.get/2 menerima kunci apa pun tanpa membedakan tipe. Perbaikannya, pilih satu gaya akses dan gunakan secara konsisten dalam satu berkas.

Ringkasan#

  • Notasi %{"kunci" => nilai} menerima kunci apa pun, sedangkan %{kunci: nilai} khusus kunci atom, dan keduanya tidak sama.
  • Map.get/3 dan Map.put/3 tidak mengubah map lama karena data Elixir imutabel.
  • Pola map bersifat parsial dan %{} cocok dengan semua map, sehingga urutan klausanya menentukan.
  • Struct adalah map dengan defstruct yang diperbarui melalui %{s | kunci: nilai} untuk kunci yang sudah ada, sedangkan kunci baru ditolak dengan KeyError.

Referensi#