Atom, tuple, dan list

Tipe data inti Elixir beserta pertimbangan akses dan ukurannya.

Daftar isi

Atom, tuple, dan list adalah tipe data inti yang muncul di hampir setiap baris program Elixir. Topik ini membahas sifat masing-masing tipe, cara mengakses elemennya, serta pertimbangan ukuran dan waktu akses di antara ketiganya. Setelah topik ini selesai, pemilihan tipe data untuk sebuah kebutuhan dapat dinyatakan dengan alasan yang terukur, bukan sekadar kebiasaan.

Atom sebagai konstanta bernama#

Atom adalah konstanta yang nilainya adalah namanya sendiri. Tulisan :ok adalah nilai atom bernama ok, dan dua atom dengan tulisan yang sama selalu bernilai sama. Atom digunakan untuk melabeli keadaan, misalnya :ok dan :error, atau sebagai kunci map seperti :nama. Perbandingan dua atom berlangsung cepat karena yang dibandingkan hanyalah identitas, bukan isi.

Nilai true, false, dan nil sebenarnya atom dengan penulisan khusus, sehingga is_atom(nil) mengembalikan true. Atom yang telah dibuat tidak pernah dibersihkan oleh pemulung memori, sehingga atom baru sebaiknya hanya dibuat dari nama yang tercantum di dalam kode, bukan dari masukan pengguna yang bebas. Karena identitasnya itulah atom sering digunakan sebagai penanda cabang dalam pola, misalnya tag :ok dan :error pada tuple hasil kerja.

elixir
IO.inspect(:ok)
IO.inspect(:error)
IO.puts(:ok == :ok)
IO.puts(:ok == :error)
IO.inspect(is_atom(nil), label: "nil adalah atom")
text Keluaran
:ok
:error
true
false
nil adalah atom: true

Tuple: kelompok data berukuran tetap#

Tuple menyimpan sejumlah elemen dengan ukuran yang ditetapkan saat dibuat dan tidak berubah. Tuple {:ok, "data tersimpan"} mengelompokkan tag dan isinya menjadi satu nilai, dan bentuk ber-tag inilah yang paling sering dibongkar dengan destructuring. Akses elemen tuple melalui elem/2 berlangsung dalam waktu yang tetap, tidak bergantung pada banyaknya elemen, karena setiap elemen menempati posisi yang bisa dihitung langsung. Ukuran yang kecil dan tetap juga menjadikan tuple pilihan alami untuk mengelompokkan beberapa nilai yang selalu muncul bersama.

elixir
status = {:ok, "data tersimpan"}
IO.inspect(status)

IO.inspect(elem(status, 0), label: "elem 0")
IO.inspect(elem(status, 1), label: "elem 1")
IO.inspect(tuple_size(status), label: "ukuran")
text Keluaran
{:ok, "data tersimpan"}
elem 0: :ok
elem 1: "data tersimpan"
ukuran: 2

Fungsi put_elem/3 menghasilkan tuple baru dengan satu elemen yang diganti. Karena data imutabel, tuple lama tetap utuh dan tuple baru dibuat dengan menyalin seluruh isinya, sehingga pembaruan tuple yang besar sebaiknya jarang dilakukan. Akses indeks di luar batas tidak ditoleransi, melainkan melempar ArgumentError.

elixir
IO.inspect(put_elem({1, 2}, 1, 99), label: "put_elem")

try do
  elem({:a, :b}, 5)
rescue
  e in ArgumentError -> IO.puts("ArgumentError: #{Exception.message(e)}")
end
text Keluaran
put_elem: {1, 99}
ArgumentError: errors were found at the given arguments:

  * 1st argument: out of range

List: rantai sel yang mudah dikembangkan#

List pada Elixir adalah rantai tertaut yang setiap selnya menyimpan satu elemen dan rujukan ke sel berikutnya. Kepala list diakses langsung, dan penyisipan di depan melalui [x | list] murah karena hanya menautkan satu sel baru. Sebaliknya, akses indeks ke tengah list berjalan menyusuri rantai sel, sehingga waktunya bertambah seiring panjang list. Tuple pada BEAM diletakkan sebagai satu blok memori berukuran tetap, sedangkan list tumbuh sel demi sel, dan perbedaan tata letak inilah yang menyebabkan waktu akses keduanya berbeda. Operator ++ menyambung dua list dan -- mengurangkan elemen list kanan dari list kiri.

elixir
angka = [1, 2, 3]
IO.inspect(angka ++ [4, 5], label: "gabung")
IO.inspect(angka -- [2, 3], label: "kurangi")
IO.inspect([0 | angka], label: "sisip depan")
IO.inspect(hd(angka), label: "kepala")
IO.inspect(tl(angka), label: "ekor")
text Keluaran
gabung: [1, 2, 3, 4, 5]
kurangi: [1]
sisip depan: [0, 1, 2, 3]
kepala: 1
ekor: [2, 3]

Akses indeks list menggunakan Enum.at/2, yang menyusuri rantai sel hingga indeks yang diminta. Pada indeks di luar batas, Enum.at/2 mengembalikan nil, berbeda dari elem/2 yang melempar ArgumentError. Fungsi hd/1 dan tl/1 adalah versi primitif dari pola [kepala | ekor], dan keduanya gagal ketika menerima list kosong. Perbedaan perilaku ini mengikuti sifat masing-masing struktur, yaitu list yang panjangnya bebas dan tuple yang batasnya tetap.

elixir
daftar = [10, 20, 30, 40]
IO.inspect(Enum.at(daftar, 2), label: "Enum.at(daftar, 2)")
IO.inspect(Enum.at(daftar, 9), label: "Enum.at(daftar, 9)")
text Keluaran
Enum.at(daftar, 2): 30
Enum.at(daftar, 9): nil

Perbedaan ketiganya dapat diringkas sebagai berikut.

AspekAtomTupleList
Ukuransatu nilaitetap saat dibuatbebas bertambah dan berkurang
Akses berdasar indekstidak berlakuelem/2, waktu tetapEnum.at/2, menyusuri rantai
Penambahan elementidak berlakumenyalin seluruh tuplemurah di depan, `[x
Kebiasaan penggunaanlabel keadaan dan kuncikelompok nilai tetapderetan sejenis yang panjangnya berubah

Charlist: list kode karakter#

Charlist adalah list yang setiap elemennya berupa kode karakter, sehingga ~c"hi" sama persis dengan list [104, 105]. Ketika sebuah list hanya memuat kode karakter ASCII yang dapat dicetak, fungsi inspeksi menampilkannya dalam notasi charlist ~c"...". Inilah sebabnya tampilan list angka bisa terlihat seperti teks, sedangkan list yang memuat satu kode yang tidak dapat dicetak ditampilkan sebagai daftar angka biasa.

elixir
IO.inspect([104, 105], label: "list kode ascii")
IO.inspect([104, 105] == ~c"hi", label: "sama dengan charlist")
IO.inspect([104, 105, 300], label: "kode 300")
IO.inspect([104, 105], charlists: :as_lists, label: "tanpa inferensi charlist")
text Keluaran
list kode ascii: ~c"hi"
sama dengan charlist: true
kode 300: [104, 105, 300]
tanpa inferensi charlist: [104, 105]

Charlist ditulis dengan sigil ~c, yaitu notasi khusus berawalan tanda tilde. Bentuk lama dengan tanda kutip tunggal seperti 'abc' sudah tidak dianjurkan dan memunculkan peringatan pada versi Elixir terkini. Perbedaan tampilan ini murni persoalan inspeksi, karena nilai di memori tetap berupa list bilangan bulat pada kedua kasus. Dalam pekerjaan sehari-hari, charlist jarang ditulis secara manual karena teks pada Elixir dikelola sebagai string UTF-8, sedangkan charlist kebanyakan muncul saat berhadapan dengan API Erlang.

Kesalahan umum#

  • Menganggap Enum.at/2 dan elem/2 dapat saling menggantikan. Enum.at/2 berlaku untuk list dan mengembalikan nil di luar batas, sedangkan elem/2 berlaku untuk tuple dan melempar ArgumentError. Perbaikannya, sesuaikan fungsi dengan tipe datanya.
  • Memperbarui tuple besar di dalam perulangan. put_elem/3 menyalin seluruh tuple pada setiap panggilan. Perbaikannya, pilih list atau struktur data lain ketika isi sering berubah.
  • Membaca ~c"abc" sebagai string. Charlist adalah list bilangan bulat, dan [97, 98, 99] == ~c"abc" bernilai true. Perbaikannya, gunakan string "abc" untuk teks dan batasi charlist untuk API Erlang.
  • Mengira -- mengubah list aslinya. Operator -- menghasilkan list baru, sedangkan list kiri tetap utuh karena data imutabel. Perbaikannya, ikat hasilnya ke nama baru sebagaimana operasi lain pada data imutabel.

Ringkasan#

  • Atom adalah konstanta yang nilainya adalah namanya, digunakan untuk label seperti :ok dan :error.
  • Tuple berukuran tetap dengan akses elem/2 yang waktunya tetap, sedangkan pembaruannya menyalin seluruh tuple.
  • List adalah rantai tertaut yang murah disisipi di depan, sedangkan akses indeksnya menyusuri rantai dan Enum.at/2 mengembalikan nil di luar batas.
  • List yang seluruh kodenya dapat dicetak dalam ASCII ditampilkan sebagai charlist ~c"...", yang nilainya memang list kode karakter.

Referensi#