Operator pencocokan =

= sebagai asersi, destructuring tuple dan list.

Daftar isi

Operator = pada Elixir tidak sekadar mengisi variabel, melainkan mencocokkan pola (pattern matching) di sisi kiri dengan nilai di sisi kanan. Topik ini membahas = sebagai asersi, destructuring, yaitu pembongkaran nilai tuple dan list menjadi bagian-bagiannya, serta bentuk error yang muncul ketika pencocokan gagal. Setelah topik ini selesai, data berbentuk tuple dan list dapat dibongkar dengan tepat, termasuk penanganan kegagalannya.

Operator = sebagai asersi#

Pada sebagian besar bahasa pemrograman, = adalah operator penugasan yang menyalin nilai ke variabel. Pada Elixir, sisi kiri = diperlakukan sebagai pola dan sisi kanan sebagai nilai. Variabel yang muncul di sisi kiri terikat dengan bagian nilai yang sesuai, sedangkan bagian pola yang berupa nilai tetap, misalnya angka 1, wajib sama persis dengan nilai di sisi kanan. Karena aturan itu, 1 = 1 adalah pernyataan yang sah dan berhasil dieksekusi.

Sifat inilah yang menjadikan = sebagai asersi, yakni pernyataan yang wajib benar agar eksekusi dapat berlanjut. Apabila pola tidak cocok dengan nilainya, Elixir melempar MatchError pada saat eksekusi berlangsung. Error semacam ini bukan cacat yang harus selalu dihindari, melainkan sinyal bahwa bentuk data tidak sesuai dugaan. Contoh berikut memperlihatkan pencocokan yang berhasil.

Bagian pola yang berupa nilai tetap tidak terbatas pada angka. String, atom, dan tuple yang isinya bernilai tetap dapat menjadi bagian pola, dan seluruhnya wajib sama dengan nilai yang bersangkutan. Variabel yang belum terikat adalah satu-satunya bagian pola yang bebas menerima nilai apa pun.

elixir
# = mencocokkan pola di kiri dengan nilai di kanan
x = 1
IO.puts(x)

1 = 1
IO.puts("pola 1 cocok dengan nilai 1")
text Keluaran
1
pola 1 cocok dengan nilai 1

Pencocokan yang gagal menghentikan eksekusi dengan error mentah. Untuk mempelajarinya secara terkendali, contoh berikut membungkus pencocokan dalam try/rescue sehingga pesan errornya dapat dicetak alih-alih menghentikan program.

elixir
# angka 2 tidak cocok dengan nilai 1 sehingga MatchError dilempar
try do
  2 = 1
rescue
  e in MatchError -> IO.puts("MatchError: #{Exception.message(e)}")
end
text Keluaran
MatchError: no match of right hand side value:

    1

Pesan errornya menyebut nilai sisi kanan yang tidak cocok, yaitu 1. Pada program sungguhan, try/rescue digunakan hanya di tempat kegagalan memang diperkirakan, misalnya di batas antara komponen yang saling memanggil. Di tempat lain, kegagalan pencocokan dibiarkan menghentikan eksekusi karena bentuk data yang salah lebih baik terlihat sejak dini daripada mengalir diam-diam.

Destructuring tuple#

Destructuring membongkar sebuah nilai berstruktur menjadi beberapa variabel sekaligus dalam satu =. Untuk tuple, polanya ditulis dalam bentuk yang sama dengan nilainya. Pencocokan {:ok, isi} = {:ok, "berhasil"} mengikat isi ke string "berhasil" karena kedua tuple memiliki ukuran dan elemen pertama yang sama. Pola semacam ini mendasari konvensi yang berlaku luas di ekosistem Elixir, yaitu fungsi mengembalikan {:ok, nilai} ketika berhasil dan {:error, alasan} ketika gagal.

elixir
{:ok, isi} = {:ok, "berhasil"}
IO.puts(isi)

{:status, kode} = {:status, 200}
IO.inspect(kode, label: "kode")
text Keluaran
berhasil
kode: 200

Bentuk pola wajib sama dengan bentuk nilai. Tuple dengan elemen pertama :error tidak pernah cocok dengan pola yang menuntut :ok, seberapa pun mirip bentuk sisanya. Contoh berikut memperlihatkan kegagalan itu beserta pesannya.

elixir
# ukurannya cocok, tetapi tag :ok tidak sama dengan :error
try do
  {:ok, _} = {:error, "gagal"}
rescue
  e in MatchError -> IO.puts("MatchError: #{Exception.message(e)}")
end
text Keluaran
MatchError: no match of right hand side value:

    {:error, "gagal"}

Destructuring list#

List dibongkar dengan dua bentuk pola. Pola berukuran tetap seperti [a, b] menuntut jumlah elemen yang sama persis, sedangkan pola [kepala | ekor] memisahkan elemen pertama dari sisanya. Simbol | dibaca “serta sisanya”, sehingga [kepala | ekor] cocok dengan list apa pun yang tidak kosong. Pola ini menjadi fondasi pengolahan list pada gaya rekursif Elixir.

elixir
[kepala | ekor] = [10, 20, 30]
IO.inspect(kepala, label: "kepala")
IO.inspect(ekor, label: "ekor")

[dua | _] = [1, 2, 3]
IO.inspect(dua, label: "dua")
text Keluaran
kepala: 10
ekor: [20, 30]
dua: 1

Pola berukuran tetap bersifat ketat terhadap panjang list. Pola [a, b] hanya cocok dengan list yang tepat memiliki dua elemen, bukan dua atau lebih. Perilaku ini berbeda dari beberapa bahasa lain yang membiarkan sisa elemen terabaikan diam-diam.

Kedua bentuk pola juga dapat digabungkan dalam satu pola. Penulisan [a, b | sisa] mengikat dua elemen pertama sekaligus sisanya, sedangkan pola bersarang seperti [{:ok, nilai} | _] membongkar tuple pertama dari sebuah list hasil kerja. Kedalaman pola mengikuti kedalaman datanya, sehingga satu = dapat menjangkau bagian dalam struktur tanpa langkah perantara.

elixir
[a, b] = [1, 2]
IO.puts(a + b)

# list tiga elemen tidak cocok dengan pola dua elemen
try do
  [_, _] = [1, 2, 3]
rescue
  e in MatchError -> IO.puts("MatchError: #{Exception.message(e)}")
end
text Keluaran
3
MatchError: no match of right hand side value:

    [1, 2, 3]

Contoh utuh: berkas profil.exs#

Berkas berikut menggabungkan ketiga bentuk destructuring pada satu alur data. Sumber data diasumsikan berupa tuple {:ok, data} sesuai konvensi pada bagian sebelumnya, isinya berupa map profil, dan salah satu lapangannya berupa list hobi. Berkas ini tidak memerlukan dependensi selain Elixir itu sendiri, dan seluruh barisnya dieksekusi dari atas ke bawah ketika dijalankan. Jalankan berkasnya dengan perintah elixir profil.exs dari direktori topik.

elixir profil.exs
# profil.exs - destructuring data profil dengan operator =
profil = {:ok, %{nama: "Citra", umur: 24, kota: "Bandung", hobi: ["membaca", "bersepeda"]}}

{:ok, data} = profil
%{nama: nama, umur: umur, kota: kota} = data
[hobi_utama | hobi_lain] = data.hobi

IO.puts("Nama: #{nama}")
IO.puts("Umur: #{umur}")
IO.puts("Kota: #{kota}")
IO.puts("Hobi utama: #{hobi_utama}")
IO.inspect(hobi_lain, label: "Hobi lainnya")
text Keluaran
Nama: Citra
Umur: 24
Kota: Bandung
Hobi utama: membaca
Hobi lainnya: ["bersepeda"]

Baris {:ok, data} = profil bekerja ganda, yaitu memastikan status berhasil sekaligus mengambil isinya. Pola map %{nama: nama, umur: umur, kota: kota} hanya mencantumkan kunci yang dibutuhkan, sedangkan list hobi dibongkar dengan [hobi_utama | hobi_lain]. Apabila pembungkusnya ternyata {:error, alasan}, baris pertama melempar MatchError karena berkas ini memang mengasumsikan data yang sah. Pada program nyata, asersi semacam ini biasanya berada di dalam fungsi yang mengendalikan sendiri bentuk keluarannya.

Kesalahan umum#

Empat kesalahan berikut paling sering muncul ketika operator = baru dipraktikkan.

  • Membaca = sebagai penugasan biasa. Penulisan 1 = 2 tidak mengganti nilai 1, melainkan berupa pernyataan yang gagal dengan MatchError. Perbaikannya, baca sisi kiri sebagai pola dan sisi kanan sebagai nilai yang harus cocok.
  • Menulis pola yang bentuknya berbeda dari data. Pola {:ok, isi} untuk data {:error, alasan} selalu gagal, sekecil apa pun perbedaan yang terlihat. Perbaikannya, samakan tag dan ukurannya, atau sediakan pola terpisah untuk setiap bentuk yang mungkin muncul.
  • Menggunakan pola list berukuran tetap untuk list yang panjangnya berubah. Pola [a, b] menolak list tiga elemen. Perbaikannya, gunakan [a | sisa] ketika panjang list tidak pasti.
  • Menganggap _ sebagai variabel yang dapat dibaca kembali. Garis bawah sama sekali tidak menyimpan nilai, sehingga pencocokan [_, _] = [1, 2] berhasil tanpa meninggalkan nilai yang dapat digunakan. Perbaikannya, beri nama variabel apabila nilainya masih dibutuhkan.

Ringkasan#

  • Operator = mencocokkan pola di kiri dengan nilai di kanan. Variabel pada pola terikat nilainya, sedangkan nilai tetap pada pola wajib sama.
  • Kegagalan pencocokan melempar MatchError pada saat eksekusi, bukan saat kompilasi.
  • Destructuring tuple menuntut tag dan ukuran yang sama, sedangkan list dibongkar dengan [kepala | ekor] atau pola berukuran tetap.
  • Garis bawah _ mengabaikan bagian nilai tanpa mengikat variabel.

Referensi#