Alur kendali pada Elixir bertumpu pada pencocokan pola: case mencocokkan satu nilai terhadap serangkaian pola, cond mengevaluasi serangkaian syarat boolean, dan with merangkai pencocokan bertingkat untuk alur berhasil-gagal. Topik ini membahas ketiganya beserta error yang muncul ketika tidak ada klausanya yang cocok, lalu menutup dengan contoh utuh validasi nilai ujian. Setelah topik ini selesai, percabangan dapat ditulis dengan gaya pencocokan pola alih-alih tumpukan if yang bersarang.
case: satu nilai, banyak pola#
case mengevaluasi sebuah ekspresi lalu mencocokkan hasilnya dengan klausanya dari atas ke bawah. Klausa pertama yang cocok menentukan hasilnya, dan pencocokan berhenti di situ. Pola pada setiap klausa boleh bertingkat, misalnya tuple bertag yang membawa map di dalamnya, dan setiap klausa dapat diberi guard, yaitu syarat tambahan yang ditulis setelah kata when.
pesan = {:ok, %{status: 200, isi: "semua baik"}}
hasil = case pesan do
{:ok, %{status: 200, isi: isi}} -> "berhasil: #{isi}"
{:ok, %{status: kode}} -> "status tak terduga: #{kode}"
{:error, alasan} -> "gagal: #{alasan}"
end
IO.puts(hasil)
IO.puts(case {:error, :timeout} do
{:ok, %{status: 200, isi: isi}} -> "berhasil: #{isi}"
{:ok, %{status: kode}} -> "status tak terduga: #{kode}"
{:error, alasan} -> "gagal: #{alasan}"
end)berhasil: semua baik
gagal: timeoutGuard mempersempit klausa dengan syarat yang bukan pola, misalnya rentang angka. Klausa yang syaratnya tidak terpenuhi dilewati meskipun polanya cocok, sehingga urutan klausa dari yang paling khusus ke yang paling umum tetap berlaku.
klasifikasi = fn
nilai when nilai >= 85 -> "A (#{nilai})"
nilai when nilai >= 70 -> "B (#{nilai})"
nilai -> "C (#{nilai})"
end
IO.puts(klasifikasi.(92))
IO.puts(klasifikasi.(71))
IO.puts(klasifikasi.(60))A (92)
B (71)
C (60)case adalah ekspresi, sehingga hasilnya dapat diikat ke variabel seperti pada contoh pertama, dan seluruh badan klausanya dapat memuat beberapa ekspresi sekaligus. Klausa dievaluasi berurutan, dan klausa yang cocok lebih dahulu menang meskipun klausa lain juga cocok. Pola map parsial juga berlaku di sini, termasuk %{} yang cocok dengan map apa pun, sehingga klausa penangkap berbasis pola map wajib diletakkan paling akhir.
Apabila tidak ada klausa yang cocok, CaseClauseError dilempar pada saat program berjalan. Error ini tidak ditangkap saat kompilasi, karena nilai yang dicocokkan memang baru ada saat eksekusi. Klausa penangkap _ -> mencegahnya dengan menampung segala nilai yang tidak terduga.
try do
case 3 do
1 -> "satu"
2 -> "dua"
end
rescue
e in CaseClauseError -> IO.puts("CaseClauseError: #{Exception.message(e)}")
endCaseClauseError: no case clause matching:
3cond: syarat boolean berurutan#
cond dimaksudkan untuk syarat yang bukan pola, misalnya perbandingan rentang. Setiap syarat dievaluasi dari atas ke bawah, dan badan syarat pertama yang bernilai benar digunakan sebagai hasilnya. Nilai benar (truthy) berarti segala nilai selain false dan nil. Klausa true -> yang diletakkan terakhir berperan sebagai penangkap untuk segala keadaan yang belum tercakup.
nilai = 78
predikat = cond do
nilai >= 85 -> "sangat baik"
nilai >= 70 -> "baik"
nilai >= 55 -> "cukup"
true -> "kurang"
end
IO.puts(predikat)baikUrutan syarat pada cond bekerja seperti urutan klausa pada case, dan syarat yang lebih umum harus diletakkan setelah yang lebih khusus. Syarat nilai >= 70 yang diletakkan lebih dahulu akan cocok dengan nilai 90 sebelum syarat nilai >= 85 sempat diuji. Susunan dari yang paling sempit ke paling luas menjaga setiap rentang tetap bermakna.
Apabila seluruh syarat bernilai salah dan tidak ada klausa true ->, CondClauseError dilempar. Perilaku ini mendorong penulisan cond yang menutup semua kemungkinan sejak awal, alih-alih menunggu masukan yang tak terduga untuk memunculkannya.
try do
cond do
1 > 2 -> "mustahil"
end
rescue
e in CondClauseError -> IO.puts("CondClauseError: #{Exception.message(e)}")
endCondClauseError: no cond clause evaluated to a truthy valuewith: rangkaian pencocokan#
with menyambung beberapa tahap, dan setiap tahap menghasilkan nilai seperti {:ok, _} atau {:error, _}. Tanda panah <- mencocokkan hasil setiap tahap dengan pola di kirinya. Apabila cocok, eksekusi berlanjut ke tahap berikutnya dan variabel pada polanya terikat. Apabila seluruh tahap cocok, badan do dieksekusi, sedangkan nilai yang gagal dialihkan ke blok else.
defmodule Alur do
def jalankan(nilai) do
with {:ok, angka} <- to_angka(nilai),
{:ok, ganda} <- gandakan(angka) do
"hasil: #{ganda}"
else
{:error, alasan} -> "ditolak: #{alasan}"
end
end
defp to_angka(nilai) when is_integer(nilai), do: {:ok, nilai}
defp to_angka(_), do: {:error, "bukan bilangan bulat"}
defp gandakan(angka) when angka > 0, do: {:ok, angka * 2}
defp gandakan(_), do: {:error, "harus positif"}
end
IO.puts(Alur.jalankan(21))
IO.puts(Alur.jalankan("teks"))
IO.puts(Alur.jalankan(-5))hasil: 42
ditolak: bukan bilangan bulat
ditolak: harus positifKeluaran di atas menunjukkan tiga jalur yang berbeda, yaitu berhasil sampai akhir, gagal pada tahap pertama, dan gagal pada tahap kedua. Blok else menangkap kegagalan dari tahap mana pun tanpa struktur bersarang. Apabila else tidak disediakan, nilai yang gagal dikembalikan apa adanya, sehingga kegagalan mengalir diam-diam ke pemanggil.
hasil = with {:ok, x} <- :tidak_ok do
x
end
IO.inspect(hasil, label: "nilai with tanpa else")nilai with tanpa else: :tidak_okBlok else sendiri tunduk pada aturan pencocokan yang sama dengan case. Apabila nilai yang gagal tidak cocok dengan klausa else mana pun, WithClauseError dilempar dan eksekusi berhenti. Blok else yang lengkap menutup setiap bentuk kegagalan yang mungkin dihasilkan tahapan, misalnya klausa penangkap {:error, alasan} pada contoh utuh di bawah, yang mencakup {:error, alasan} dari tahap mana pun.
Contoh utuh: berkas proses_nilai.exs#
Berkas berikut merangkai pola with pada alur validasi nilai ujian. Tahap pertama memeriksa keberadaan kunci :nilai dan tipenya, tahap kedua menetapkan rentang yang sah beserta predikatnya, dan blok else menangkap {:error, alasan} dari tahap mana pun. Jalankan berkasnya dengan perintah elixir proses_nilai.exs dari direktori topik.
# proses_nilai.exs - alur validasi nilai ujian dengan with
defmodule ProsesNilai do
def proses(input) do
with {:ok, nilai} <- ambil_nilai(input),
{:ok, predikat} <- klasifikasi(nilai) do
"Nilai #{nilai}: #{predikat}"
else
{:error, alasan} -> "Ditolak: #{alasan}"
end
end
defp ambil_nilai(input) do
case Map.fetch(input, :nilai) do
{:ok, nilai} when is_integer(nilai) -> {:ok, nilai}
{:ok, _} -> {:error, "nilai bukan bilangan bulat"}
:error -> {:error, "kunci :nilai tidak ditemukan"}
end
end
defp klasifikasi(nilai) when nilai < 0 or nilai > 100, do: {:error, "di luar rentang 0-100"}
defp klasifikasi(nilai) when nilai >= 70, do: {:ok, "lulus"}
defp klasifikasi(nilai) when nilai >= 55, do: {:ok, "lulus bersyarat"}
defp klasifikasi(_nilai), do: {:ok, "tidak lulus"}
end
IO.puts(ProsesNilai.proses(%{nilai: 85}))
IO.puts(ProsesNilai.proses(%{nilai: 60}))
IO.puts(ProsesNilai.proses(%{nilai: 40}))
IO.puts(ProsesNilai.proses(%{nilai: 150}))
IO.puts(ProsesNilai.proses(%{nama: "Budi"}))Nilai 85: lulus
Nilai 60: lulus bersyarat
Nilai 40: tidak lulus
Ditolak: di luar rentang 0-100
Ditolak: kunci :nilai tidak ditemukanKelima baris keluaran mewakili lima jalur yang berbeda. Tiga jalur pertama mengalir sampai badan do, sedangkan dua jalur terakhir berhenti pada tahap yang menolaknya. Fungsi ambil_nilai mengembalikan {:error, alasan} untuk dua keadaan, yaitu kunci yang hilang dan tipe yang keliru, sehingga pesan penolakannya spesifik. Fungsi klasifikasi menggunakan guard untuk rentang dan predikat, dan klausa terakhirnya menampung nilai 0 sampai 54 tanpa menyebut batasnya secara eksplisit. Tanpa with, rantai ini biasanya ditulis sebagai case yang bersarang dua tingkat, dan setiap tambahan tahap memperdalam sarangnya.
Kesalahan umum#
- Menulis
casetanpa klausa penangkap. Nilai di luar dugaan melemparCaseClauseErrorsaat eksekusi. Perbaikannya, tambahkan_ ->sebagai klausa terakhir yang menyatakan perilaku bawaan secara eksplisit. - Menggunakan
withtanpaelse. Nilai yang gagal dikembalikan apa adanya dan kegagalan mengalir diam-diam. Perbaikannya, sediakanelseyang polanya menutup semua bentuk kegagalan tahapan. - Menyediakan
elseyang klausanya tidak menutup semua bentuk kegagalan. Nilai yang lolos melemparWithClauseError, seperti pada contoh berikut.
try do
hasil = with {:ok, x} <- :tidak_ok do
x
else
:lain -> "lain"
end
IO.inspect(hasil, label: "hasil")
rescue
e in WithClauseError -> IO.puts("WithClauseError: #{Exception.message(e)}")
endWithClauseError: no with clause matching:
:tidak_ok- Menulis
condtanpa klausatrue ->di akhir. Masukan di luar semua syarat melemparCondClauseError. Perbaikannya, akhiriconddengantrue ->yang menyatakan kasus sisanya.
Ringkasan#
casemencocokkan satu nilai dengan pola bertingkat yang dapat dipersempit dengan guard, dan melemparCaseClauseErrorapabila tidak ada klausanya yang cocok.condmengevaluasi syarat boolean dari atas ke bawah dan menuntuttrue ->sebagai penangkap terakhir.withmerangkai pencocokan bertingkat dengan<-, mengeksekusi badandopada jalur berhasil, dan mengalihkan kegagalan keelse.
Referensi#
- case, cond, and if — HexDocs
— Panduan resmi tentang
case,cond, dan perbandingannya denganif. - Kernel.SpecialForms — HexDocs
— Dokumentasi
withbeserta klausaelse-nya. - Pattern matching — HexDocs
— Dasar pencocokan pola yang menggerakkan
casedanwith.