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.
IO.inspect(:ok)
IO.inspect(:error)
IO.puts(:ok == :ok)
IO.puts(:ok == :error)
IO.inspect(is_atom(nil), label: "nil adalah atom"):ok
:error
true
false
nil adalah atom: trueTuple: 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.
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"){:ok, "data tersimpan"}
elem 0: :ok
elem 1: "data tersimpan"
ukuran: 2Fungsi 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.
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)}")
endput_elem: {1, 99}
ArgumentError: errors were found at the given arguments:
* 1st argument: out of rangeList: 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.
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")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.
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)")Enum.at(daftar, 2): 30
Enum.at(daftar, 9): nilPerbedaan ketiganya dapat diringkas sebagai berikut.
| Aspek | Atom | Tuple | List |
|---|---|---|---|
| Ukuran | satu nilai | tetap saat dibuat | bebas bertambah dan berkurang |
| Akses berdasar indeks | tidak berlaku | elem/2, waktu tetap | Enum.at/2, menyusuri rantai |
| Penambahan elemen | tidak berlaku | menyalin seluruh tuple | murah di depan, `[x |
| Kebiasaan penggunaan | label keadaan dan kunci | kelompok nilai tetap | deretan 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.
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")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/2danelem/2dapat saling menggantikan.Enum.at/2berlaku untuk list dan mengembalikannildi luar batas, sedangkanelem/2berlaku untuk tuple dan melemparArgumentError. Perbaikannya, sesuaikan fungsi dengan tipe datanya. - Memperbarui tuple besar di dalam perulangan.
put_elem/3menyalin 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"bernilaitrue. 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
:okdan:error. - Tuple berukuran tetap dengan akses
elem/2yang waktunya tetap, sedangkan pembaruannya menyalin seluruh tuple. - List adalah rantai tertaut yang murah disisipi di depan, sedangkan akses indeksnya menyusuri rantai dan
Enum.at/2mengembalikannildi luar batas. - List yang seluruh kodenya dapat dicetak dalam ASCII ditampilkan sebagai charlist
~c"...", yang nilainya memang list kode karakter.
Referensi#
- Basic types — HexDocs — Panduan resmi atom, tuple, list, dan charlist.
- Tuple — HexDocs
— Dokumentasi fungsi tuple, termasuk
elem/2danput_elem/3. - List — HexDocs
— Dokumentasi fungsi list serta operator
++dan--.